Python——正则表达式
此篇文章结合小甲鱼的笔记和视频整理。
1 编译
Python 通过 re 模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配。
正则表达式被编译为模式对象,该对象拥有各种方法供你操作字符串,如查找模式匹配或者执行字符串替换。
>>> import re
>>> p = re.compile('ab*', re.IGNORECASE)
正则表达式作为一个字符串参数传给 re.compile()。由于正则表达式并不是 Python 的核心部分,因此没有为它提供特殊的语法支持,所以正则表达式只能以字符串的形式表示。相反,re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和 zlib 模块。当你将正则表达式编译之后,你就得到一个模式对象。那你拿他可以用来做什么呢?模式对象拥有很多方法和属性,我们下边列举最重要的几个来讲:
方法 | 功能 |
match() | 判断一个正则表达式是否从开始处匹配一个字符串 |
search() | 遍历字符串,找到正则表达式匹配的第一个位置 |
findall() | 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回 |
finditer() | 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回 |
如果没有找到任何匹配的话,match() 和 search() 会返回 None;如果匹配成功,则会返回一个匹配对象(match object),包含所有匹配的信息:例如从哪儿开始,到哪儿结束,匹配的子字符串等等。
举例说明如下:
>>> import re
>>> p = re.compile('[a-z]+')
>>> print(p.match(""))
None
>>> m = p.match('fishc')
>>> m
<_sre.SRE_Match object; span=(0, 5), match='fishc'>
在这个例子中,match() 返回一个匹配对象,我们将其存放在变量 m 中,以便日后使用。接下来让我们来看看匹配对象里边有哪些信息吧。匹配对象包含了很多方法和属性,以下几个是最重要的:
方法 | 功能 |
group() | 返回匹配的字符串 |
start() | 返回匹配的开始位置 |
end() | 返回匹配的结束位置 |
span() | 返回一个元组表示匹配位置(开始,结束) |
如上述例子中的m:
>>> m.group()
'fishc'
>>> m.start()
0
>>> m.end()
5
>>> m.span()
(0, 5)
由于 match() 只检查正则表达式是否在字符串的起始位置匹配,所以 start() 总是返回 0。然而,search() 方法可就不一样了:
>>> print(p.match('^_^fishc'))
None
>>> m = p.search('^_^fishc')
KeyboardInterrupt
>>> m = p.search('^_^fishc')
>>> print(m)
<_sre.SRE_Match object; span=(3, 8), match='fishc'>
>>> m.group()
'fishc'
>>> m.span()
(3, 8)
有两个方法可以返回所有的匹配结果,一个是 findall(),findall() 返回的是一个列表;另一个是 finditer(),findall() 需要在返回前先创建一个列表,而 finditer() 则是将匹配对象作为一个迭代器返回。
使用正则表达式也并非一定要创建模式对象,然后调用它的匹配方法。因为,re 模块同时还提供了一些全局函数,例如 match(),search(),findall(),sub() 等等。这些函数的第一个参数是正则表达式字符串,其他参数跟模式对象同名的方法采用一样的参数;返回值也一样,同样是返回 None 或者匹配对象。其实,这些函数只是帮你自动创建一个模式对象,并调用相关的函数(上一篇的内容,还记得吗?)。它们还将编译好的模式对象存放在缓存中,以便将来可以快速地直接调用。
那我们到底是应该直接使用这些模块级别的函数呢,还是先编译一个模式对象,再调用模式对象的方法呢?这其实取决于正则表达式的使用频率,如果说我们这个程序只是偶尔使用到正则表达式,那么全局函数是比较方便的;如果我们的程序是大量的使用正则表达式(例如在一个循环中使用),那么建议你使用后一种方法,因为预编译的话可以节省一些函数调用。但如果是在循环外部,由于得益于内部缓存机制,两者效率相差无几。
2 编译标志
编译标志让你可以修改正则表达式的工作方式。在 re 模块下,编译标志均有两个名字:完整名和简写。)。另外,多个标志还可以同时使用(通过“|”),如:re.I | re.M 就是同时设置 I 和 M 标志。
下边列举一些支持的编译标志(详解解释参考《Python3 如何优雅地使用正则表达式(详解三)》):
标志 | 含义 |
ASCII, A | 使得转义符号如 \w,\b,\s 和 \d 只能匹配 ASCII 字符 |
DOTALL, S | 使得 . 匹配任何符号,包括换行符 |
IGNORECASE, I | 匹配的时候不区分大小写 |
LOCALE, L | 支持当前的语言(区域)设置 |
MULTILINE, M | 多行匹配,影响 ^ 和 $ |
VERBOSE, X (for 'extended') | 启用详细的正则表达式 |
3 分组
通常在实际的应用过程中,我们除了需要知道一个正则表达式是否匹配之外,还需要更多的信息。对于比较复杂的内容,正则表达式通常使用分组的方式分别对不同内容进行匹配。在正则表达式中,使用元字符 ( ) 来划分组。( ) 元字符跟数学表达式中的小括号含义差不多;它们将包含在内部的表达式组合在一起,所以你可以对一个组的内容使用重复操作的元字符,例如 *,+,? 或者 {m, n}。
例如,(ab)* 会匹配零个或者多个 ab:
>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)
使用 ( ) 表示的子组我们还可以对它进行按层次索引,可以将索引值作为参数传递给这些方法:group(),start(),end() 和 span()。序号 0 表示第一个分组(这个是默认分组,一直存在的,所以不传入参数相当于默认值 0):
>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'
子组的索引值是从左到右进行编号,子组也允许嵌套,因此我们可以通过从左往右来统计左括号 ( 来确定子组的序号。
>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'
group() 方法可以一次传入多个子组的序号:
>>> m.group(2,1,2)
('b', 'abc', 'b')
通过 groups() 方法可以一次性返回所有的子组匹配的字符串:
>>> m.groups()
('abc', 'b')
还有一个反向引用的概念需要介绍。反向引用指的是你可以在后面的位置使用先前匹配过的内容,用法是反斜杠加上数字。例如 \1 表示引用前边成功匹配的序号为 1 的子组。
>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'
4 非捕获命名组
精心设计的正则表达式可能会划分很多组,这些组不仅可以匹配相关的子串,还能够对正则表达式本身进行分组和结构化。在复杂的正则表达式中,由于有太多的组,因此通过组的序号来跟踪和使用会变得困难。有两个新的功能可以帮你解决这个问题——非捕获组和命名组——它们都使用了一个公共的正则表达式扩展语法。
有时候你只是需要用一个组来表示部分正则表达式,你并不需要这个组去匹配任何东西,这时你可以通过非捕获组来明确表示你的意图。非捕获组的语法是 (?:...),这个 ... 你可以替换为任何正则表达式。
接着举一个找到网站中的IP地址并打印出来的例子:
import urllib.request
import re
def open_url(url):
req = urllib.request.Request(url)
req.add_header('User-Agent','Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36')
page = urllib.request.urlopen(req)
html = page.read().decode('utf-8')
return html
def get_ip(html):
p = r'(?:(?:[0,1]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:2[0-4]\d|25[0-5]|[0,1]?\d?\d)'
iplist = re.findall(p,html)
for each in iplist:
print(each)
if __name__ =='__main__':
url = "http://www.data5u.com/"
get_ip(open_url(url))
上述例子中如果不适用非捕获命名组,打印出来的结果将会是:
('174.', '174', '213')
('145.', '145', '45')
('117.', '117', '11')
('137.', '137', '137')
('133.', '133', '127')
('130.', '130', '222')
('226.', '226', '101')
('240.', '240', '254')
('191.', '191', '5')
('134.', '134', '57')
('17.', '17', '33')
('131.', '131', '117')
('4.', '4', '82')
('226.', '226', '31')
('141.', '141', '193')
('192.', '192', '101')
('25.', '25', '116')
('18.', '18', '35')
('18.', '18', '60')
('93.', '93', '250')
使用了非捕获命名组后的结果如下:
113.236.174.213
52.166.145.45
139.59.117.11
203.189.137.137
1.238.133.127
114.27.130.222
52.21.226.101
159.192.240.254
66.70.191.5
190.147.134.57
101.129.17.33
116.197.131.117
39.46.4.82
149.56.226.31
203.189.141.193
202.162.192.101
125.77.25.116
122.72.18.35
122.72.18.60
40.71.93.250
除了你不能从非捕获组获得匹配的内容之外,其他的非捕获组跟普通子组没有什么区别了。你可以在里边放任何东西,使用重复功能的元字符,或者跟其他子组进行嵌套(捕获的或者非捕获的子组都可以)。
当你需要修改一个现有的模式的时候,(?:...) 是非常有用的。原始是添加一个非捕获组并不会影响到其他(捕获)组的序号。值得一提的是,在搜索的速度上,捕获组和非捕获组的速度是没有任何区别的。
5 命名组
命名组。普通子组我们使用序列来访问它们,命名组则可以使用一个有意义的名字来进行访问。
命名组的语法是 Python 特有的扩展语法:(?P<name>)。很明显,< > 里边的 name 就是命名组的名字啦。命名组除了有一个名字标识之外,跟其他捕获组是一样的。
匹配对象的所有方法不仅可以处理那些由数字引用的捕获组,还可以处理通过字符串引用的命名组。除了使用名字访问,命名组仍然可以使用数字序号进行访问:
>>> import re
>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'
>>> m
<_sre.SRE_Match object; span=(5, 9), match='Lots'>
正则表达式中,反向引用的语法像 (...)\1 是使用序号的方式来访问子组;在命名组里,显然也是有对应的变体:使用名字来代替序号。其扩展语法是 (?P=name),含义是该 name 指向的组需要在当前位置再次引用。那么搜索两个单词的正则表达式可以写成 (\b\w+)\s+\1,也可以写成 (?P<word>\b\w+)\s+(?P=word):
>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'
6 前向断言
前向断言可以分为前向肯定断言和前向否定断言两种形式。
(?=...)
前向肯定断言。如果当前包含的正则表达式(这里以 ... 表示)在当前位置成功匹配,则代表成功,否则失败。一旦该部分正则表达式被匹配引擎尝试过,就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。
(?!...)
前向否定断言。这跟前向肯定断言相反(不匹配则表示成功,匹配表示失败)。
为了使大家更易懂,我们举个例子来证明这玩意是真的很有用。大家考虑一个简单的正则表达式模式,这个模式的作用是匹配一个文件名。我们都知道,文件名是用 . 将名字和扩展名分隔开的。例如在 fishc.txt 中,fishc 是文件的名字,.txt 是扩展名。这个正则表达式其实挺简单的:.*[.].*$
注意,这里用于分隔的 . 是一个元字符,所以我们使用 [.] 剥夺了它的特殊功能。还有 $,我们使用 $ 确保字符串剩余的部分都包含在扩展名中。所以这个正则表达式可以匹配 fishc.txt,foo.bar,autoexec.bat,sendmail.cf,printers.conf 等。现在我们来考虑一种复杂一点的情况,如果你想匹配扩展名不是 bat 的文件,你的正则表达式应该怎么写呢?
我们先来看下你有可能写错的尝试:
.*[.][^b].*$
这里为了排除 bat,我们先尝试排除扩展名的第一个字符为非 b。但这是错误的开始,因为 foo.bar 后缀名的第一个字符也是 b。
为了弥补刚刚的错误,我们试了这一招:
.*[.]([^b]..|.[^a].|..[^t])$
我们不得不承认,这个正则表达式变得很难看......但这样第一个字符不是 b,第二个字符不是 a,第三个字符不是 t......这样正好可以接受 foo.bar,排除 autoexec.bat。但问题又来了,这样的正则表达式要求扩展名必须是三个字符,比如sendmail.cf 就会被排除掉。
好吧,我们接着修复问题:
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在第三次尝试中,我们让第二个和第三个字符变成可选的。这样就可以匹配稍短的扩展名,比如 sendmail.cf。
不得不承认,我们把事情搞砸了,现在的正则表达式变得艰涩难懂外加奇丑无比!!
更惨的是如果需求改变了,例如你想同时排除 bat 和 exe 扩展名,这个正则表达式模式就变得更加复杂了......
当当当当!主角登场,其实,一个前向否定断言就可以解决你的难题:
.*[.](?!bat$).*$
我们来解释一下这个前向否定断言的含义:如果正则表达式 bat 在当前位置不匹配,尝试剩下的部分正则表达式;如果 bat匹配成功,整个正则表达式将会失败(因为是前向否定断言嘛^_^)。(?!bat$) 末尾的 $ 是为了确保可以正常匹配像sample.batch 这种以 bat 开始的扩展名。
同样,有了前向否定断言,要同时排除 bat 和 exe 扩展名,也变得相当容易:
.*[.](?!bat$|exe$).*$
7 修改字符串的几种方法
正则表达式使用以下方法修改字符串:
方法 | 用途 |
split() | 在正则表达式匹配的地方进行分割,并返回一个列表 |
sub() | 找到所有匹配的子字符串,并替换为新的内容 |
subn() | 跟 sub() 干一样的勾当,但返回新的字符串以及替换的数目 |
详细用法参考《Python3 如何优雅地使用正则表达式(详解六)》