正则表达式是从信息中搜索特定的模式的一把瑞士军刀。它们是一个巨大的工具库,其中的一些功能经常被忽视或未被充分利用。今天我将向你们展示一些正则表达式的高级用法。

  举个例子,这是一个我们可能用来检测电话美国电话号码的正则表达式:

r'^(1[-\s.])?(\()?\d{3}(?(2)\))[-\s.]?\d{3}[-\s.]?\d{4}$'

  我们可以加上一些注释和空格使得它更具有可读性。

r'^'
r'(1[-\s.])?' # optional '1-', '1.' or '1'
r'(\()?' # optional opening parenthesis
r'\d{3}' # the area code
r'(?(2)\))' # if there was opening parenthesis, close it
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{3}' # first 3 digits
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{4}$' # last 4 digits

  让我们把它放到一个代码片段里:

import re

numbers = [ "123 555 6789",
"1-(123)-555-6789",
"(123-555-6789",
"(123).555.6789",
"123 55 6789" ] for number in numbers:
pattern = re.match(r'^'
r'(1[-\s.])?' # optional '1-', '1.' or '1'
r'(\()?' # optional opening parenthesis
r'\d{3}' # the area code
r'(?(2)\))' # if there was opening parenthesis, close it
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{3}' # first 3 digits
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{4}$\s*',number) # last 4 digits if pattern:
print '{0} is valid'.format(number)
else:
print '{0} is not valid'.format(number)

  输出,不带空格:

123 555 6789 is valid
1-(123)-555-6789 is valid
(123-555-6789 is not valid
(123).555.6789 is valid
123 55 6789 is not valid

  正则表达式是 python 的一个很好的功能,但是调试它们很艰难,而且正则表达式很容易就出错。

  幸运的是,python 可以通过对 re.compile 或 re.match 设置 re.DEBUG (实际上就是整数 128) 标志就可以输出正则表达式的解析树。

import re

numbers = [ "123 555 6789",
"1-(123)-555-6789",
"(123-555-6789",
"(123).555.6789",
"123 55 6789" ] for number in numbers:
pattern = re.match(r'^'
r'(1[-\s.])?' # optional '1-', '1.' or '1'
r'(\()?' # optional opening parenthesis
r'\d{3}' # the area code
r'(?(2)\))' # if there was opening parenthesis, close it
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{3}' # first 3 digits
r'[-\s.]?' # followed by '-' or '.' or space
r'\d{4}$', number, re.DEBUG) # last 4 digits if pattern:
print '{0} is valid'.format(number)
else:
print '{0} is not valid'.format(number)

  解析树

at_beginning
max_repeat 0 1
subpattern 1
literal 49
in
literal 45
category category_space
literal 46
max_repeat 0 2147483648
in
category category_space
max_repeat 0 1
subpattern 2
literal 40
max_repeat 0 2147483648
in
category category_space
max_repeat 3 3
in
category category_digit
max_repeat 0 2147483648
in
category category_space
subpattern None
groupref_exists 2
literal 41
None
max_repeat 0 2147483648
in
category category_space
max_repeat 0 1
in
literal 45
category category_space
literal 46
max_repeat 0 2147483648
in
category category_space
max_repeat 3 3
in
category category_digit
max_repeat 0 2147483648
in
category category_space
max_repeat 0 1
in
literal 45
category category_space
literal 46
max_repeat 0 2147483648
in
category category_space
max_repeat 4 4
in
category category_digit
at at_end
max_repeat 0 2147483648
in
category category_space
123 555 6789 is valid
1-(123)-555-6789 is valid
(123-555-6789 is not valid
(123).555.6789 is valid
123 55 6789 is not valid

  贪婪和非贪婪

  在我解释这个概念之前,我想先展示一个例子。我们要从一段 html 文本寻找锚标签:

import re

html = 'Hello <a href="http://pypix.com" title="pypix">Pypix</a>'

m = re.findall('<a.*>.*<\/a>', html)
if m:
print m

  结果将在意料之中:

['<a href="http://pypix.com" title="pypix">Pypix</a>']

  我们改下输入,添加第二个锚标签:

import re

html = 'Hello <a href="http://pypix.com" title="pypix">Pypix</a>' \
'Hello <a href="http://example.com" title"example">Example</a>' m = re.findall('<a.*>.*<\/a>', html)
if m:
print m

  结果看起来再次对了。但是不要上当了!如果我们在同一行遇到两个锚标签后,它将不再正确工作:

['<a href="http://pypix.com" title="pypix">Pypix</a>Hello <a href="http://example.com" title"example">Example</a>']

  这次模式匹配了第一个开标签和最后一个闭标签以及在它们之间的所有的内容,成了一个匹配而不是两个 单独的匹配。这是因为默认的匹配模式是“贪婪的”。

当处于贪婪模式时,量词(比如 * 和 +)匹配尽可能多的字符。

  当你加一个问号在后面时(.*?)它将变为“非贪婪的”。

import re

html = 'Hello <a href="http://pypix.com" title="pypix">Pypix</a>' \
'Hello <a href="http://example.com" title"example">Example</a>' m = re.findall('<a.*?>.*?<\/a>', html)
if m:
print m

  现在结果是正确的。

['<a href="http://pypix.com" title="pypix">Pypix</a>', '<a href="http://example.com" title"example">Example</a>']

  前向界定符和后向界定符

  一个前向界定符搜索当前的匹配之后搜索匹配。通过一个例子比较好解释一点。

  下面的模式首先匹配 foo,然后检测是否接着匹配 bar:

import re

strings = [  "hello foo",         # returns False
"hello foobar" ] # returns True for string in strings:
pattern = re.search(r'foo(?=bar)', string)
if pattern:
print 'True'
else:
print 'False'

  这看起来似乎没什么用,因为我们可以直接检测 foobar 不是更简单么。然而,它也可以用来前向否定界定。 下面的例子匹配foo,当且仅当它的后面没有跟着 bar。

import re

strings = [  "hello foo",         # returns True
"hello foobar", # returns False
"hello foobaz"] # returns True for string in strings:
pattern = re.search(r'foo(?!bar)', string)
if pattern:
print 'True'
else:
print 'False'

  后向界定符类似,但是它查看当前匹配的前面的模式。你可以使用 (?> 来表示肯定界定,(?<! 表示否定界定。

  下面的模式匹配一个不是跟在 foo 后面的 bar。

import re

strings = [  "hello bar",         # returns True
"hello foobar", # returns False
"hello bazbar"] # returns True for string in strings:
pattern = re.search(r'(?<!foo)bar',string)
if pattern:
print 'True'
else:
print 'False'

  条件(IF-Then-Else)模式

  正则表达式提供了条件检测的功能。格式如下:

(?(?=regex)then|else)

  条件可以是一个数字。表示引用前面捕捉到的分组。

  比如我们可以用这个正则表达式来检测打开和闭合的尖括号:

import re

strings = [  "<pypix>",    # returns true
"<foo", # returns false
"bar>", # returns false
"hello" ] # returns true for string in strings:
pattern = re.search(r'^(<)?[a-z]+(?(1)>)$', string)
if pattern:
print 'True'
else:
print 'False'

  在上面的例子中,1 表示分组 (<),当然也可以为空因为后面跟着一个问号。当且仅当条件成立时它才匹配关闭的尖括号。

  条件也可以是界定符。

  无捕获组

  分组,由圆括号括起来,将会捕获到一个数组,然后在后面要用的时候可以被引用。但是我们也可以不捕获它们。

  我们先看一个非常简单的例子:

import re
string = 'Hello foobar'
pattern = re.search(r'(f.*)(b.*)', string)
print "f* => {0}".format(pattern.group(1)) # prints f* => foo
print "b* => {0}".format(pattern.group(2)) # prints b* => bar

  现在我们改动一点点,在前面加上另外一个分组 (H.*):

import re
string = 'Hello foobar'
pattern = re.search(r'(H.*)(f.*)(b.*)', string)
print "f* => {0}".format(pattern.group(1)) # prints f* => Hello
print "b* => {0}".format(pattern.group(2)) # prints b* => bar

  模式数组改变了,取决于我们在代码中怎么使用这些变量,这可能会使我们的脚本不能正常工作。 现在我们不得不找到代码中每一处出现了模式数组的地方,然后相应地调整下标。 如果我们真的对一个新添加的分组的内容没兴趣的话,我们可以使它“不被捕获”,就像这样:

import re
string = 'Hello foobar'
pattern = re.search(r'(?:H.*)(f.*)(b.*)', string)
print "f* => {0}".format(pattern.group(1)) # prints f* => foo
print "b* => {0}".format(pattern.group(2)) # prints b* => bar

  通过在分组的前面添加 ?:,我们就再也不用在模式数组中捕获它了。所以数组中其他的值也不需要移动。

  命名组

  像前面那个例子一样,这又是一个防止我们掉进陷阱的方法。我们实际上可以给分组命名, 然后我们就可以通过名字来引用它们,而不再需要使用数组下标。格式是:(?Ppattern) 我们可以重写前面那个例子,就像这样:

import re
string = 'Hello foobar'
pattern = re.search(r'(?P<fstar>f.*)(?P<bstar>b.*)', string)
print "f* => {0}".format(pattern.group('fstar')) # prints f* => foo
print "b* => {0}".format(pattern.group('bstar')) # prints b* => bar

  现在我们可以添加另外一个分组了,而不会影响模式数组里其他的已存在的组:

import re
string = 'Hello foobar'
pattern = re.search(r'(?P<hi>H.*)(?P<fstar>f.*)(?P<bstar>b.*)', string)
print "f* => {0}".format(pattern.group('fstar')) # prints f* => foo
print "b* => {0}".format(pattern.group('bstar')) # prints b* => bar
print "h* => {0}".format(pattern.group('hi')) # prints b* => Hello

  使用回调函数

  在 Python 中 re.sub() 可以用来给正则表达式替换添加回调函数。

  让我们来看看这个例子,这是一个 e-mail 模板:

import re
template = "Hello [first_name] [last_name], \
Thank you for purchasing [product_name] from [store_name]. \
The total cost of your purchase was [product_price] plus [ship_price] for shipping. \
You can expect your product to arrive in [ship_days_min] to [ship_days_max] business days. \
Sincerely, \
[store_manager_name]"
# assume dic has all the replacement data
# such as dic['first_name'] dic['product_price'] etc...
dic = {
"first_name" : "John",
"last_name" : "Doe",
"product_name" : "iphone",
"store_name" : "Walkers",
"product_price": "$500",
"ship_price": "$10",
"ship_days_min": "1",
"ship_days_max": "5",
"store_manager_name": "DoeJohn"
}
result = re.compile(r'\[(.*)\]')
print result.sub('John', template, count=1)

  注意到每一个替换都有一个共同点,它们都是由一对中括号括起来的。我们可以用一个单独的正则表达式 来捕获它们,并且用一个回调函数来处理具体的替换。

  所以用回调函数是一个更好的办法:

import re
template = "Hello [first_name] [last_name], \
Thank you for purchasing [product_name] from [store_name]. \
The total cost of your purchase was [product_price] plus [ship_price] for shipping. \
You can expect your product to arrive in [ship_days_min] to [ship_days_max] business days. \
Sincerely, \
[store_manager_name]"
# assume dic has all the replacement data
# such as dic['first_name'] dic['product_price'] etc...
dic = {
"first_name" : "John",
"last_name" : "Doe",
"product_name" : "iphone",
"store_name" : "Walkers",
"product_price": "$500",
"ship_price": "$10",
"ship_days_min": "1",
"ship_days_max": "5",
"store_manager_name": "DoeJohn"
}
def multiple_replace(dic, text):
pattern = "|".join(map(lambda key : re.escape("["+key+"]"), dic.keys()))
return re.sub(pattern, lambda m: dic[m.group()[1:-1]], text)
print multiple_replace(dic, template)

  不要重复发明轮子

  更重要的可能是知道在什么时候不要使用正则表达式。在许多情况下你都可以找到 替代的工具。

  解析 [X]HTML

  Stackoverflow 上的一个答案用一个绝妙的解释告诉了我们为什么不应该用正则表达式来解析 [X]HTML。

  你应该使用使用 HTML 解析器,Python 有很多选择:

  后面两个即使是处理畸形的 HTML 也能很优雅,这给大量的丑陋站点带来了福音。

  ElementTree 的一个例子:

from xml.etree import ElementTree
tree = ElementTree.parse('filename.html')
for element in tree.findall('h1'):
print ElementTree.tostring(element)

  其他

  在使用正则表达式之前,这里有很多其他可以考虑的工具。

  感谢阅读!

高级正则表达式技术(Python版)的更多相关文章

  1. 《大话设计模式》Python版代码实现

    上一周把<大话设计模式>看完了,对面向对象技术有了新的理解,对于一个在C下写代码比较多.偶尔会用到一些脚本语言写脚本的人来说,很是开阔眼界.<大话设计模式>的代码使用C#写成的 ...

  2. 自己动手实现智能家居之树莓派GPIO简介(Python版)

    [前言] 一个热爱技术的人一定向往有一个科技感十足的环境吧,那何不亲自实践一下属于技术人的座右铭:“技术改变世界”. 就让我们一步步动手搭建一个属于自己的“智能家居平台”吧(不要对这个名词抬杠啦,技术 ...

  3. [原创]上海好买基金招高级Java技术经理/运维主管/高级无线客户端开发等职位(内推)

    [原创]上海好买基金招高级Java技术经理/运维主管/高级无线客户端开发等职位(内推) 内部推荐职位 高级JAVA技术经理: 岗位职责: 负责项目管理(技术方向),按照产品开发流 ,带领研发团队,制定 ...

  4. 常见查找算法之php, js,python版

    常用算法 >>>1. 顺序查找, 也叫线性查找, 它从第一个记录开始, 挨个进行对比, 是最基本的查找技术 javaScript 版顺序查找算法: // 顺序查找(线性查找) 只做找 ...

  5. 豆瓣top250(go版以及python版)

      最近学习go,就找了一个例子练习[go语言爬虫]go语言爬取豆瓣电影top250,思路大概就是获取网页,然后根据页面元素,用正则表达式匹配电影名称.评分.评论人数.原文有个地方需要修改下patte ...

  6. 《UNIX环境高级编程(第3版)》

    <UNIX环境高级编程(第3版)> 基本信息 原书名:Advanced Programming in the UNIX Environment (3rd Edition) (Addison ...

  7. 数据结构之队列(Python 版)

    数据结构之队列(Python 版) 队列的特点:先进先出(FIFO) 使用链表技术实现 使用单链表技术,在表首尾两端分别加入指针,就很容易实现队列类. 使用顺序表list实现 # 队列类的实现 cla ...

  8. 数据结构之 栈 (Python 版)

    数据结构之 栈 (Python 版) -- 利用线性表实现栈 栈的特性: 后进先出 基于顺序表实现栈 class SStack(): ''' 基于顺序表 实现的 栈类 ''' def __init__ ...

  9. 【Python】《大话设计模式》Python版代码实现

    <大话设计模式>Python版代码实现 上一周把<大话设计模式>看完了,对面向对象技术有了新的理解,对于一个在C下写代码比较多.偶尔会用到一些脚本语言写脚本的人来说,很是开阔眼 ...

随机推荐

  1. MySQL 数据库性能优化之索引优化

    接着上一篇 MySQL 数据库性能优化之表结构,这是 MySQL数据库性能优化专题 系列的第三篇文章:MySQL 数据库性能优化之索引优化 大家都知道索引对于数据访问的性能有非常关键的作用,都知道索引 ...

  2. 人脸识别算法准确率最终超过了人类 The Face Recognition Algorithm That Finally Outperforms Humans

    Everybody has had the experience of not recognising someone they know—changes in pose, illumination ...

  3. 大规模视觉识别挑战赛ILSVRC2015各团队结果和方法 Large Scale Visual Recognition Challenge 2015

    Large Scale Visual Recognition Challenge 2015 (ILSVRC2015) Legend: Yellow background = winner in thi ...

  4. [原]poj-2488-water-DFS

    题目大意: 输入一个p*q的棋盘, 行用数字表示, 列用大写字母表示 , 1 <= p*q <= 26, 输出能够把棋盘全部空格走完且每个空格只走一次的字典序最小的路径.不存在则输出“im ...

  5. 成功的GIT开发分支模型和策略

    详细图文并茂以及git flow工具解释参考: http://danielkummer.github.io/git-flow-cheatsheet/index.zh_CN.html 原文地址:http ...

  6. core--作业

    线程被封装在进程中,进程能不能被封装? 当有多个进程协调一起来完成一项任务的时候,就使用"作业"来完成 作业将进程组合在一起,并创建一个"盒子"来限制进程能够做 ...

  7. CSS3之边框样式(动画过渡)

    简述 CSS3中transition属性定义了过渡,我们可以使用它来辅助我们实现一个边框样式的动画过渡. 简述 transition 定义和用法 语法 实现 效果 源码 transition 定义和用 ...

  8. EASYUI+MVC4通用权限管理平台--前言

    经过多年的管理信息系统的开发工作,吸取了工作中遇到的一些问题,经过自己的总结,形成了一套比较完整的管理信息系统的通用权限管理基础开发平台. 在软件的开发过程中我们首先需要解决的是UI问题,其次是浏览器 ...

  9. 另类方法解决设计Web页面出现:Error Creating Control

    在B/S开发的过程中,经常会遇到这样的提示:Error Creating Control ,而这些页面明明之前是可以打开的,但还是出现如下图所示: 网上找到的方法是把控件初始化放在OnInit里去写, ...

  10. Codeforces 279 B Books

    题意:给出n本书,总的时间t,每本书的阅读时间a[i],必须按照顺序来阅读,问最多能够阅读多少本书 有点像紫书的第七章讲的那个滑动区间貌似 维护一个区间的消耗的时间小于等于t,然后维护一个区间的最大值 ...