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

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

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. DWR3.0 dwr 返回值(数组,集合,Map)

    首先导入项目所需要的包,如下:dwr.jar,commons-logging-1.0.4.jar,版本可以调整 1.web.xml<?xml version="1.0" en ...

  2. DeepLearning常用库简要介绍与对比

    网上近日流传一张DL相关库在Github上的受关注度对比(数据应该是2016/03/15左右统计的): 其中tensorflow,caffe,keras和Theano排名比较靠前. 今日组会报告上tj ...

  3. Database: Normal form

    refer to wikipedia--- 1NF(first normal form): 1. There's no top-to-bottom ordering to the rows. 2. T ...

  4. 【Tech】android真机测试——小米3

    开始学习android了,自带的虚拟AVD慢的不忍直视,只能拿自己的小米3开刀了.弄了好久,记录如下. 首先,我承认到现在我不知道小米3的驱动到底是怎么安装的,我按照网上的方法自己下载过小米的驱动,但 ...

  5. Java 全角字符转半角字符

    1.java代码里有时候会遇到代码注入的安全问题,为了防止这种问题,增加了一个过滤功能.主要是过滤全角字符,把url不能识别的全角字符转换成半角字符 public class Test { publi ...

  6. 下拉刷新控件(1)PullToRefreshList示例

    有很多控件都可以下拉刷新如,ListView,ExpandableListView,GridView,ScrollView,ViewPager,WebView等, 其中最常见的是ListView.本文 ...

  7. HibernateTools的使用

    1. 到 Hibernate.org官网上 下载最新版的 Hibernate Tools,我用的是 HibernateTools-3.2.4.GA版 2. 将 下载下来的压缩包解压缩,里面会有 plu ...

  8. d is undefined错误

    如图这样的错误:TypeError : d is undefined,今天早上在写代码的时候也是遇见这样的错误,报告的错误是jquery.js中错误,但是这样的问题经常是由于什么名字不对(个人见解), ...

  9. D3D中深度测试和Alpha混合的关系

    我在学习D3D的深度测试和Alpha混合的时候,有一些遗憾.书上提供的例子里说一定要先渲染不透明物体,再渲染透明物体,对渲染状态的设置也有特殊要求.我看的很晕.自己查图形学的书,上网找资料,结果还是糊 ...

  10. HTML中多媒体的应用_Flash/MP3/设置可以活动的文字

    一.HTML中多媒体的应用_falsh动画(往网页中插入Flash动画) 1. Flash动画插入第一种方法:使用<embed>...</embed>标记动画会自动缩小 属性: ...