这段时间因项目需要用正则表达式,初始打算花两三个小时学习一下,结果经过3天疯狂度娘,敲代码验证才拿下。现学习过程中使用过的实例整理成文分享。
正则表达式(Regular expressions 也称为 REs,或 regexes 或 regex patterns)本质上是一个微小的且高度专业化的编程语言。它被嵌入到 Python 中,并通过 re 模块提供给程序猿使用。使用正则表达式,你需要指定一些规则来描述那些你希望匹配的字符串集合。这些字符串集合可能包含英语句子、 e-mail 地址、TeX 命令,或任何你想要的东东。
正则表达式模式被编译成一系列的字节码,然后由一个 C 语言写的匹配引擎所执行。对于高级的使用,你可能需要更关注匹配引擎是如何执行给定的 RE,并通过一定的方式来编写 RE,以便产生一个可以运行得更快的字节码。本文暂不讲解优化的细节,因为这需要你对匹配引擎的内部机制有一个很好的理解。但本文的例子均是符合标准的正则表达式语法。
大多数字母和字符会匹配它们自身。举个例子,正则表达式 FishC 将完全匹配字符串 "FishC"。
当然这个规则也有例外。有少数特殊的字符我们称之为元字符(metacharacter),它们并不能匹配自身,它们定义了字符类、子组匹配和模式重复次数等。本文用很大的篇幅专门讨论了各种元字符及其作用。
正则表达式所有的元字符
名称 |
通配符 |
起始 |
结尾 |
0次或多次 |
1次或多次 |
0次或1次 |
匹配次数 |
字符类 |
转义符 |
或者 |
分组 |
符号 |
. |
^ |
$ |
* |
+ |
? |
{} |
[] |
\ |
| |
() |
方括号 [ ]指定一个字符类用于存放你需要匹配的字符集合。可以单独列出需要匹配的字符,也可以通过两个字符和一个横杆 - 指定匹配的范围。例如 [abc]会匹配字符 a,b 或 c;[a-c] 可以实现相同的功能。后者使用范围来表示与前者相同的字符集合。如果你想只匹配小写字母,你的 RE 可以写成 [a-z]。
需要注意的一点是:元字符在方括号中不会触发“特殊功能”,在字符类中,它们只匹配自身。例如 [akm$] 会匹配任何字符 'a','k','m' 或 '$','$' 是一个元字符,但在方括号中它不表示特殊含义,它只匹配 '$' 字符本身。
你还可以匹配方括号中未列出的所有其他字符。做法是在类的开头添加一个脱字符号 ^ ,例如 [^5] 会匹配除了 '5' 之外的任何字符。
或许最重要的元字符当属反斜杠 \ 了。跟 Python 的字符串规则一样,如果在反斜杠后边紧跟着一个元字符,那么元字符的“特殊功能”也不会被触发。例如你需要匹配符号 [ 或 \,你可以在它们前面加上一个反斜杠,以消除它们的特殊功能:\[,\\。
反斜杠后边跟一些字符还可以表示特殊的意义,例如\d表示十进制数字,\S表示所有的字母或者表示非空白的字符集合。反斜杠转义字符可以包含在一个字符类中,并且一样拥有特殊含义。例如 [\s,.] 是一个字符类,它将匹配任何空白字符(\s 的特殊含义),',' 或 '.'。
让我们来举个例子:\w 匹配任何单词字符。如果正则表达式以字节的形式表示,这相当于字符类 [a-zA-Z0-9_];如果正则表达式是一个字符串,\w 会匹配所有 Unicode 数据库(unicodedata 模块提供)中标记为字母的字符。
下边列举一些反斜杠加字符构成的特殊含义:
特殊字符 |
含义 |
示例 |
备注 |
\d |
匹配任何十进制数字;相当于类 [0-9] |
示例:search(r'\d',"No.12345") 结果:span=(3, 4), match='1' 示例:search(r'N\d',"No.12345") 结果:None |
匹配单个数字字符 |
\D |
与 \d 相反,匹配任何非十进制数字的字符;相当于类 [^0-9] |
示例:search(r'\D',"No.12345") 结果:span=(0, 1), match='N' 示例:search(r' No.\D',"No.12345") 结果:None |
|
\s |
匹配任何空白字符(包含空格、换行符、制表符等);相当于类 [ \t\n\r\f\v] |
示例:search(r'No\s',"No 12345") 结果:span=(0, 3), match='No\t' 示例:search(r'No\s',"No.12345") 结果:None |
|
\S |
与 \s 相反,匹配任何非空白字符;相当于类 [^ \t\n\r\f\v] |
示例:search(r'No\S',"No.12345") 结果:span=(0, 3), match='No.' |
|
\w |
匹配任何单词字符。 ASCII模式:相当于 [a-zA-Z0-9_] Unicode模式:unicodedata 模块标记为字母的字符。 |
示例:search(r'\w'," python is vary good!") 结果:span=(2, 3), match='p' 示例:search(r'\w'," 大熊猫") 结果:span=(2, 3), match='大' |
|
\W |
与 \w 相反 |
示例:search(r'\W',"大熊猫:中国国宝") 结果:span=(3, 4), match=':' |
|
\b |
检测单词的开始或结束位置 |
示例:search(r'\b'," what is you name?") 结果:span=(2, 2), match=’’ 示例:search(r'\w+?\b.+?\b\w+?\b'," what is you name?") 结果:match='what is' 示例:search(r'\w+?\b.+?\b\w+?'," what is you name?") 结果:match='what i' |
\b不匹配单词内容,一般与通配符配套使用 |
\B |
非单词边界的位置,与 \b 相反 |
示例:search(r'python\B.',"python3 is vary good!") 结果:span=(0, 7), match='python3' 示例:search(r'python\B.',"python is vary good!") 结果:None |
\B不匹配单词内容,一般与通配符配套使用 |
这里详细解释一下单词边界: \b,这是一个只匹配单词的开始和结尾的零宽断言。“单词”定义为一个字母数字的序列,单词的结束指的是空格或者非字母数字的字符。
有些元字符它们不匹配任何字符,只是简单地表示成功或失败,因此这些字符也称之为零宽断言。例如 \b 表示当前位置位于一个单词的边界,但 \b 并不能改变位置。因此,零宽断言不应该被重复使用,因为 \b 并不会修改当前位置,所以 \b\b 跟 \b 是没什么两样的。 “改变位置”与“零宽断言”的意思:比如 abc 匹配完 a 之后,当前位置就会移动,才能继续匹配 b,依次类推...但是 \babc 的话,\b 表示当前位置在单词的边界(单词的第一个字母或者最后一个字母),这时候当前位置不会发生改变,接着将 a 与当前位置的字符进行匹配。
下边例子中,class 只有在出现一个完整的单词 class 时才匹配;如果出现在别的单词中,并不会匹配。
>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<_sre.SRE_Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None
在使用这些特殊的序列的时候,有两点是需要注意的:第一点需要注意的是,Python 的字符串跟正则表达式在有些字符上是有冲突的(回忆之前反斜杠的例子)。比如说在 Python 中,\b 表示的是退格符(ASCII 码值是 8)。所以,你如果不使用原始字符串,Python 会将 \b 转换成退格符处理,这样就肯定跟你的预期不一样了。
下边的例子中,我们故意不写表示原始字符串的 'r',结果确实大相庭径:
>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<_sre.SRE_Match object; span=(0, 7), match='\x08class\x08'>
第二点需要注意的是,在字符类中不能使用这个断言。跟 Python 一样,在字符类中,\b 只是用来表示退格符。
我们来看看 * 这个元字符,当然它不是匹配 '*' 字符本身,它用于指定前一个字符匹配零次或者多次。
例如 ca*t 将匹配 ct(0 个字符 a),cat(1 个字符 a),caaat(3 个字符 a),等等。需要注意的是,由于受到 C 语言的 int 类型大小的内部限制,正则表达式引擎会限制字符 'a' 的重复个数不超过 20 亿个。
正则表达式默认的重复规则是贪婪的,当你重复匹配一个 RE 时,匹配引擎会尝试尽可能多的去匹配。直到 RE 不匹配或者到了结尾,匹配引擎就会回退一个字符,然后再继续尝试匹配。
我们通过例子一步步的给大家讲解什么叫“贪婪”:先考虑一下表达式 a[bcd]*b,首先需要匹配字符 'a',然后是零个到多个 [bcd],最后以 'b' 结尾。那现在想象一下,这个 RE 匹配字符串 abcbd 会怎样?
步骤 |
匹配 |
说明 |
1 |
a |
匹配 RE 的第一个字符 'a' |
2 |
abcbd |
引擎在符合规则的情况下尽可能地匹配 [bcd]*,直到该字符串的结尾 |
3 |
失败 |
引擎尝试匹配 RE 最后一个字符 'b',但当前位置已经是字符串的结尾,所以失败告终 |
4 |
abcb |
回退,所以 [bcd]* 匹配少一个字符 |
5 |
失败 |
再一次尝试匹配 RE 最后一个字符 'b',但字符串最后一个字符是 'd',所以失败告终 |
6 |
abc |
再次回退,所以 [bcd]* 这次只匹配 'bc' |
7 |
abcb |
再一次尝试匹配字符 'b',这一次字符串当前位置指向的字符正好是 'b',匹配成功 |
最终,RE 匹配的结果是 abcb。
另一个实现重复的元字符是 +,用于指定前一个字符匹配一次或者多次。
要特别注意 * 和 + 的区别:* 匹配的是零次或者多次,所以被重复的内容可能压根儿不会出现;+ 至少需要出现一次。例如 ca+t 会匹配 cat 和 caaat,但不会匹配 ct。
还有两个表示重复的元字符,其中一个是问号 ?,用于指定前一个字符匹配零次或者一次。
最灵活的应该是元字符 {m,n}(m 和 n 都是十进制整数),上边讲到的几个元字符都可以使用它来表达,它的含义是前一个字符必须匹配 m 次到 n 次之间。例如 a/{1,3}b 会匹配 a/b,a//b 和 a///b。但不会匹配 ab(没有斜杠);也不会匹配 a////b(斜杠超过三个)。
你可以省略 m 或者 n,这样的话,引擎会假定一个合理的值代替。省略 m,将被解释为下限 0;省略 n 则会被解释为无穷大(事实上是上边我们提到的 20 亿)。
重复规则特殊字符汇总
特殊字符 |
含义 |
示例 |
备注 |
{m,n} |
m,n是非负整数,m≤n,含义是前一个字符或子组必须匹配 m 次到 n 次之间。省略 m,将被解释为下限 0;省略 n 则会被解释为无穷大(int的极限20 亿) |
示例:search(r'ab{1,3}end',"abc abend abbbend") 结果:span=(4, 9), match='abend' 示例:search(r'ab{1,3}end',"abc abend abbbend") 结果:span=(10, 17), match='abbbend' 示例:search(r'ab{1, 3}end',"abc abend abbbend") 结果:None |
正则表达式里边不能多余的空格 |
* |
前一个字符或子组匹配0次或多次,等价于{0,} |
示例:search(r'a[bcd]*b',"abcbbccbd") 结果:span=(0, 8), match='abcbbccb' 示例:search(r'\w*',"python is vary good!") 结果:span=(0, 6), match='python' |
优先使用 *、+和 ?,因为这些字符更短并且更容易阅读,而且匹配引擎对 * + ? 做了优化,效率要更高些。 |
+ |
前一个字符或子组匹配1次或多次,等价于{1,} |
示例:search(r'ab+end',"abc abend abbbend") 结果:span=(4, 9), match='abend' 示例:search(r'ab+',"abc abend abbbend") 结果:span=(0, 2), match='ab' |
|
? |
前一个字符或子组匹配0次或1次,等价于{0,1} |
示例:search(r'ab?end',"abc abend abbbend") 结果:span=(4, 9), match='abend' 示例:search(r'ae?bend',"abc abend abbbend") 结果:span=(4, 9), match='abend' |
|
{m,n}? *? +? ?? |
启用非贪婪模式 |
贪婪模式:search(r'ab.*',"abc abend abbbend") 结果:span=(0, 17), match='abc abend abbbend' 非贪婪模式:search(r'ab.*?',"abc abend abbbend") 结果:span=(0, 2), match='ab' 非贪婪模式:search(r'ab.*?\b',"abc abend abbbend") 结果:span=(0, 3), match='abc' |
贪婪模式与非贪婪模式后面详细讲解 |
Python 通过 re 模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配。
re 模块是使用 C 语言编写,所以效率比你用普通的字符串方法要高得多;将正则表达式进行编译(compile)也是为了进一步提高效率;后边我们会经常提到“模式”,指的就是正则表达式被编译成的模式对象。
正则表达式被编译为模式对象,该对象拥有各种方法供你操作字符串,如查找模式匹配或者执行字符串替换。
>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')
re.compile() 也可以接受 flags 参数,用于开启各种特殊功能和语法变化,我们会在后边一一介绍。
现在我们先来看个简单的例子:
p=re.compile(r'ab.*?\b')
src=p.search("abc abend abbbend")
print("results:",src)
all=p.findall("abc abend abbbend")
print("findall:",all)
执行结果:
results: <_sre.SRE_Match object; span=(0, 3), match='abc'>
findall: ['abc', 'abend', 'abbbend']
正则表达式作为一个字符串参数传给 re.compile()。由于正则表达式并不是 Python 的核心部分,因此没有为它提供特殊的语法支持,所以正则表达式只能以字符串的形式表示。相反,re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和 zlib 模块。
当你将正则表达式编译之后,你就得到一个模式对象。那你拿他可以用来做什么呢?模式对象拥有很多方法和属性,我们下边列举最重要的几个来讲:
方法 |
功能 |
match() |
从字符串开始处匹配正则表达式 |
search() |
遍历字符串,找到正则表达式匹配的第一个位置 |
findall() |
遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回 |
finditer() |
遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回 |
sub() |
遍历字符串,找到正则表达式匹配的所有位置,并替换,返回替换后的字符串 |
subn() |
遍历字符串,找到正则表达式匹配的所有位置,并替换,返回替换后的字符串与替换的次数 |
如果没有找到任何匹配的话,match() 和 search() 会返回 None;如果匹配成功,则会返回一个匹配对象(match object),包含所有匹配的信息:例如从哪儿开始,到哪儿结束,匹配的子字符串等等。
举个例子:
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
现在,你可以尝试使用正则表达式 [a-z]+ 去匹配各种字符串。
例如:
>>> p.match("")
>>> print(p.match(""))
None
因为 + 表示匹配一次或者多次,所以空字符串不能被匹配。因此,match() 返回 None。
我们再尝试一个可以匹配的字符串:
>>> m = p.match('fishc')
>>> m
<_sre.SRE_Match object; span=(0, 5), match='fishc'>
在这个例子中,match() 返回一个匹配对象,我们将其存放在变量 m 中,以便日后使用。
有两个方法可以返回所有的匹配结果,一个是 findall(),另一个是 finditer()。
findall() 返回的是一个列表:
>>> p = re.compile('\d+')
>>> p.findall('3只小甲鱼,15条腿,多出的3条在哪里?')
['3', '15', '3']
findall() 需要在返回前先创建一个列表,而 finditer() 则是将匹配对象作为一个迭代器返回:
>>> iterator = p.finditer('3只小甲鱼,15条腿,还有3条去了哪里?')
>>> iterator
>>> for match in iterator:
print(match.span())
(0, 1)
(6, 8)
(13, 14)
方法 |
功能 |
group() |
返回匹配的字符串 |
start() |
返回匹配的开始位置 |
end() |
返回匹配的结束位置 |
span() |
返回一个元组表示匹配位置(开始,结束) |
大家看:
>>> 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')
>>> print(m)
<_sre.SRE_Match object; span=(3, 8), match='fishc'>
>>> m.group()
'fishc'
>>> m.span()
(3, 8)
在实际应用中,最常用的方式是将匹配对象存放在一个局部变量中,并检查其返回值是否为 None。
形式通常如下:
p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
print('Match found: ', m.group())
else:
print('No match')
使用正则表达式也并非一定要创建模式对象,然后调用它的匹配方法。因为,re 模块同时还提供了一些全局函数,例如
match(pattern,string,flags=0)
search(pattern,string,flags=0)
findall(pattern,string,flags=0)
sub(pattern, repl, string, count=0, flags=0)
subn(pattern, repl, string, count=0, flags=0)
这些函数的第一个参数是正则表达式字符串,其他参数跟模式对象同名的方法采用一样的参数;返回值也一样,同样是返回 None 或者匹配对象。
>>> print(re.match(r'From\s+', 'From_FishC.com'))
None
>>> re.match(r'From\s+', 'From FishC.com')
<_sre.SRE_Match object; span=(0, 5), match='From '>
其实,这些函数只是帮你自动创建一个模式对象,并调用相关的函数。它们还将编译好的模式对象存放在缓存中,以便将来可以快速地直接调用。
那我们到底是应该直接使用这些模块级别的函数呢,还是先编译一个模式对象,再调用模式对象的方法呢?这其实取决于正则表达式的使用频率,如果说我们这个程序只是偶尔使用到正则表达式,那么全局函数是比较方便的;如果我们的程序是大量的使用正则表达式(例如在一个循环中使用),那么建议你使用后一种方法,因为预编译的话可以节省一些函数调用。但如果是在循环外部,由于得益于内部缓存机制,两者效率相差无几。
编译标志让你可以修改正则表达式的工作方式。在 re 模块下,编译标志均有两个名字:完整名和简写,例如 IGNORECASE 简写是 I。另外,多个标志还可以同时使用(通过“|”),如:re.I | re.M 就是同时设置 I 和 M 标志。
下边列举一些支持的编译标志:
标志 |
含义 |
示例 |
ASCII, A |
使得转义符号如 \w,\b,\s 和 \d 只能匹配 ASCII 字符 |
示例: search(r'\w',"学习 python",re.A) 结果: span=(3, 4), match='p' 示例: search(r'\w',"学习 python") 结果: span=(0, 1), match='学' |
UNICODE,U |
使用unicode字符集,python3默认 |
示例: search(r'\w',"学习 python",re.U) 结果: span=(0, 1), match='学' |
DOTALL, S |
使得 . 匹配任何符号,包括换行符 |
示例: search(r'python.',"python\n is vary good!",re.S) 结果: span=(0, 7), match='python\n' 示例: search(r'python.',"python\n is vary good!") 结果: None |
IGNORECASE, I |
匹配的时候不区分大小写 |
示例: search(r'PYTHON',"python is vary good!",re.I) 结果: span=(0, 6), match='python' 示例: search(r'PYTHON',"python is vary good!") 结果: None |
LOCALE, L |
支持当前的语言(区域)设置 |
后面详细讲解 |
MULTILINE, M |
多行匹配,影响 ^ 和 $ |
示例: findall(r'python.$',"python2\npython3\n",re.M) 结果: ['python2', 'python3'] 示例: findall(r'python.$',"python2\npython3\n") 结果: ['python3'] |
VERBOSE, X (for 'extended') |
启用详细的正则表达式,空格会被忽略(除了出现在字符类中和使用反斜杠转义的空格);允许注释# |
re.compile(r""" [#] ( [1-9][0-9]* # 十进制格式 | x[0-9a-fA-F]+ # 十六进制格式 )""", re.VERBOSE) |
(?a/i/L/m/s/u/x) |
内嵌编译标志,必须放在正则表达式开头。 (?a)对应于re.A (?i)对应于re.I (?L)对应于re.L (?m)对应于re.M (?s)对应于re.S (?u)对应于re.U (?x)对应于re.X |
示例: search(r'(?a)\w',"学习 python") 结果: span=(3, 4), match='p' 示例: search(r'(?i)PYTHON',"python is vary good!") 结果: span=(0, 6), match='python' |
下面我们来详细讲解一下它们的含义:
A
ASCII
使得 \w,\W,\b,\B,\s 和 \S 只匹配 ASCII 字符,而不匹配完整的 Unicode 字符。这个标志仅对 Unicode 模式有意义,并忽略字节模式。
S
DOTALL
使得 . 可以匹配任何字符,包括换行符。如果不使用这个标志,. 将匹配除了换行符的所有字符。
I
IGNORECASE
字符类和文本字符串在匹配的时候不区分大小写。举个例子,正则表达式 [A-Z] 也将会匹配对应的小写字母,像 FishC 可以匹配 FishC,fishc 或 FISHC 等。如果你不设置 LOCALE,则不会考虑语言(区域)设置这方面的大小写问题。
L
LOCALE
使得 \w,\W,\b 和 \B 依赖当前的语言(区域)环境,而不是 Unicode 数据库。
区域设置是 C 语言的一个功能,主要作用是消除不同语言之间的差异。例如你正在处理的是法文文本,你想使用 \w+ 来匹配单词,但是 \w 只是匹配 [A-Za-z] 中的单词,并不会匹配 'é' 或 'ç'。如果你的系统正确的设置了法语区域环境,那么 C 语言的函数就会告诉程序 'é' 或 'ç' 也应该被认为是一个字符。当编译正则表达式的时候设置了 LOCALE 的标志,\w+ 就可以识别法文了,但速度多少会受到影响。
M
MULTILINE
通常 ^ 只匹配字符串的开头,而 $ 则匹配字符串的结尾。当这个标志被设置的时候,^ 不仅匹配字符串的开头,还匹配每一行的行首;& 不仅匹配字符串的结尾,还匹配每一行的行尾。
X
VERBOSE
这个标志使你的正则表达式可以写得更好看和更有条理,因为使用了这个标志,空格会被忽略(除了出现在字符类中和使用反斜杠转义的空格);这个标志同时允许你在正则表达式字符串中使用注释,# 符号后边的内容是注释,不会递交给匹配引擎(除了出现在字符类中和使用反斜杠转义的 #)。
下边是使用 re.VERBOSE 的例子,大家看下正则表达式的可读性是不是提高了不少:
charref = re.compile(r"""
&[#] # 数字标志字符,如N M z
(
0[0-7]+ # 八进制格式
| [1-9][0-9]* # 十进制格式
| x[0-9a-fA-F]+ # 十六进制格式
)
; # 结尾分号
""", re.VERBOSE)
如果没有设置 VERBOSE 标志,那么同样的正则表达式会写成:
charref = re.compile("(0[0-7]+| [1-9][0-9]*|x[0-9a-fA-F]+);")
元字符 |
含义 |
示例 |
. |
元字符.匹配除了换行符以外的任何字符。如果设置了 re.DOTALL 标志,. 将匹配包括换行符在内的任何字符。 |
示例:search(r'py.',"python is vary good!") 结果:span=(0, 3), match='pyt' |
^ |
检测字符串的起始位置。如果设置了 MULTILINE 标志,就会变成检测每一行的起始位置。 |
示例:search(r'vary',"python is vary good!") 结果:span=(10, 14), match='vary' 示例:search(r'^vary',"python is vary good!") 结果:None 示例:search(r'^py',"python is vary good!") 结果:span=(0, 2), match='py' |
\A |
仅仅检测字符串的起始位置,MULTILINE标志无效。 |
示例:search(r'\Apy',"python is vary good!") 结果:span=(0, 2), match='py' 示例:findall(r'\Apython.',"python2\npython3",re.M) 结果:['python2'] |
$ |
检测字符串的结束位置,如果设置了 MULTILINE 标志,就会变成检测每一行的结束位置。 |
示例:findall(r'python.$',"python2\npython3",re.M) 结果:['python2', 'python3'] |
\Z |
仅仅检测字符串的结束位置,MULTILINE标志无效。 |
示例:findall(r'python.\Z',"python2\npython3",re.M) 结果:['python3'] |
| |
或操作符,对两个正则表达式进行或操作。 使用 \| 来匹配 '|' 字符本身;或者包含在一个字符类中,像这样 [|]。 |
示例:findall(r'python2|python3',"python2 and python3") 结果:['python2', 'python3'] 示例:search(r'python3|python2',"python2 and python3") 结果:span=(0, 7), match='python2' 示例:search(r'python(3|2)',"python2 and python3") 结果:span=(0, 7), match='python2' 示例:findall(r'python(3|2)',"python2 and python3") 结果:['2', '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'
有几对小括号就是分成了几个子组,例如 (a)(b) 和 (a(b)) 都是由两个子组构成的。
子组的索引值是从左到右进行编号,子组也允许嵌套,因此我们可以通过从左往右来统计左括号 ( 来确定子组的序号。
>>> 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')
反向引用指的是可以在后面的位置使用先前子组匹配成功的内容,用法是反斜杠加上数字或者\g
下面的例子中,(\b\w+)会匹配“the”替换到\1的位置,组成新的正则表达式r'(\b\w+)\s+the'。
>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'
如果只是搜索字符串,反向引用不会被用到,因为很少有文本格式会这样来重复字符。但是,你很快会发现,在字符串替换的时候,反向引用是非常有用的!
注意,在 Python 的字符串中会使用反斜杠加数字的方式来表示数字的值对应的 ASCII 字符,所以在使用反向索引的正则表达式中,需要使用原始字符串。
众所周知,Perl 5 为标准的正则表达式增加了许多强大的功能。Perl 的开发者们并不能选择一个新的元字符或者通过反斜杠构造一个新的特殊序列来实现扩展的功能。因为这样会和标准的正则表达式发生冲突。比如你想选择 & 作为扩展功能的元字符(在标准正则表达式中,& 没有特殊意义),但这样的话,已经按照标准语法写出来的正则表达式就不得不修改,因为它们中包含的 '&' 意愿上只是把它当做普通字符来匹配而已。
最终,Perl 的开发者们决定使用 (?...) 作为扩展语法。问号 ? 紧跟在左小括号 ( 后边,本身是一个语法错误的写法,因为 ? 前边没有东西可以重复,所以这样就解决了兼容性的问题(理由是语法正确的正则表达式肯定不会这么写)。然后,紧跟在 ? 后边的字符则表示哪些扩展语法会被使用。例如 (?=foo) 表示一种新的扩展功能(前向断言),(?:foo) 则表示另一种扩展功能(一个包含子串 foo 的非捕获组)。
Python 支持 Perl 的一些扩展语法,并且在此基础上还增加了一个扩展语法。如果紧跟在问号 ? 后边的是 P,那么可以肯定这是一个 Python 的扩展语法。
第一个我们要讲的是非捕获组。有时候需要用一个组来表示部分正则表达式,但并不需要反向引用这个组,这时可以通过非捕获组来实现。非捕获组的语法是 (?:...),这个 ... 可以替换为任何正则表达式。捕获组就是普通的子组,有默认组编号,非捕获组不参与编号,没有组编号。
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
除了你不能从非捕获组获得匹配的内容之外,其他的非捕获组跟普通子组没有什么区别了。你可以在里边放任何东西,使用重复功能的元字符,或者跟其他子组进行嵌套(捕获的或者非捕获的子组都可以)。
当你需要修改一个现有的模式的时候,(?:...) 是非常有用的。原因是添加一个非捕获组并不会影响到其他(捕获)组的序号。值得一提的是,在搜索的速度上,捕获组和非捕获组的速度是没有任何区别的。
我们再来看另外一个重要功能:命名组。普通子组我们使用序号来访问它们,命名组则可以使用一个有意义的名字来进行访问。
命名组的语法是 Python 特有的扩展语法:(?P
匹配对象的所有方法不仅可以处理那些由数字引用的捕获组,还可以处理通过字符串引用的命名组。除了使用名字访问,命名组仍然可以使用数字序号进行访问,捕获组默认编号不区分命名组与非命名组,统一按照左括号的顺序:
>>> p = re.compile(r'(?P
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'
正则表达式中,反向引用的语法像\1 是使用序号的方式来访问子组;在命名组里,使用扩展语法 (?P=name) 反向引用命名组,或者使用\g
>>> p = re.compile(r'(?P
>>> p.search('Paris in the the spring').group()
'the the'
前向断言可以分为前向肯定断言和前向否定断言两种形式,属于零宽断言。
符号...表示包含的子组正则表达式。前向肯定断言一般使用格式如下:
表达式一(?<=子组表达式)表达式二
表达式一可选,对前向肯定断言没有影响。首先匹配后面的表达式二捕获内容二,然后用子组表达式从开始位置匹配内容二(相当于子组表达式前面固定添加^),如果匹配成功,则断言成功,否则断言失败。断言成功整条表达式返回成功。断言失败整条表达式返回失败。
示例 |
执行结果 |
注释 |
search(r'(?=abc)abc',"fishc@abcch") |
span=(6, 9), match='abc' |
|
search(r'@(?=abc)abcch',"fishc@abcch") |
span=(5, 11), match='@abcch' |
|
search(r'(?=abc)@abc',"fishc@abcch") |
None |
|
search(r'fishc@abc(?=abc)ch',"fishc@abcch") |
None |
|
search(r'(?=.abc)@abc',"fishc@abcch") |
span=(5, 9), match='@abc' |
|
search(r'fishc@(?=abc)',"fishc@abcch") |
span=(0, 6), match='fishc@' |
没有表达式二断言结果依赖于实现,应避免 |
search(r'fish(?=abc)',"fishc@abcch") |
None |
|
search(r'(?=abc)',"fishc@abcch") |
span=(6, 6), match='' |
符号...表示包含的子组正则表达式。前向否定断言一般使用格式如下:
表达式一(?<=子组表达式)表达式二
表达式一可选,对前向否定断言没有影响。首先匹配后面的表达式二捕获内容二,然后用子组表达式从开始位置匹配内容二(相当于子组表达式前面固定添加^),如果匹配成功,则断言失败,否则断言成功。断言成功整条表达式返回成功。断言失败整条表达式返回失败。
示例 |
执行结果 |
注释 |
search(r'(?!abc)@abc',"fishc@abcch") |
span=(5, 9), match='@abc' |
|
search(r'fishc@abc(?=abc)ch',"fishc@abcch") |
span=(0, 11), match='fishc@abcch' |
|
search(r'(?!abc)abc',"fishc@abcch") |
None |
|
search(r'@(?!abc)abcch',"fishc@abcch") |
None |
|
search(r'fishc@(?!abc)',"fishc@abcch") |
None |
没有表达式二断言结果依赖于实现,应避免 |
match(r'fishc@(?!abc)',"fishc@abcch") |
None |
|
search(r'(?!abc)',"fishc@abcch") |
span=(0, 0), match='' |
后向断言可以分为后向肯定断言和后向否定断言两种形式,属于零宽断言。
符号...表示包含的子组正则表达式。后向肯定断言一般使用格式如下:
表达式一(?<=子组表达式)表达式二
首先匹配表达式一捕获内容一,然后用子组表达式从结束位置匹配内容一(相当于子组表达式后面固定添加$),如果匹配成功,则断言成功,否则断言失败。断言成功如果有表达式二则执行表达式二,如果没有整条表达式返回成功。如果断言失败,则整条表达式返回失败。
示例 |
执行结果 |
注释 |
search(r'fishc@abc(?<=abc)',"fishc@abcch") |
span=(0, 9), match='fishc@abc' |
|
search(r'fishc@abcch(?<=abc)',"fishc@abcch" |
None |
表达式一捕获的内容’ fishc@abcch’包含的’abc’没有在结束位置 |
search(r'fishc@(?<=abc)abc',"fishc@abcch") |
None |
表达式一捕获的内容’ fishc@’没有包含的’abc’ |
search(r'fishc@abc(?<=abc)ch',"fishc@abcch" |
span=(0, 11), match='fishc@abcch' |
|
search(r'(?<=abc)abc',"fishc@abcch") |
None |
没有表达式一断言结果依赖于实现,应避免 |
search(r'(?<=abc)ch',"fishc@abcch") |
span=(9, 11), match='ch' |
|
search(r'(?<=abc)',"fishc@abcch") |
span=(9, 9), match='' |
符号...表示包含的子组正则表达式。后向否定断言一般使用格式如下:
表达式一(?<=子组表达式)表达式二
首先匹配表达式一捕获内容一,然后用子组表达式从结束位置匹配内容一(相当于子组表达式后面固定添加$),如果匹配成功,则断言失败,否则断言成功。断言成功如果有表达式二则执行表达式二,如果没有整条表达式返回成功。如果断言失败,则整条表达式返回失败。
示例 |
执行结果 |
注释 |
src=re.search(r'fishc@abcch(? |
span=(0, 11), match='fishc@abcch' |
|
search(r'fishc@(? |
span=(0, 9), match='fishc@abc' |
|
search(r'fishc@abc(? |
None |
|
search(r'fishc@abc(? |
None |
|
search(r'(? |
span=(6, 11), match='abcch' |
没有表达式一断言结果依赖于实现,应避免 |
search(r'(? |
span=(2, 5), match='shc' |
|
search(r'(? |
span=(0, 0), match='' |
(id/name)是子组的序号或名称,如果子组匹配到的内容长度大于0,执行yes-pattern,否则执行no-pattern,no-pattern可以为空。
示例 |
执行结果 |
注释 |
match(r'(yes)?.*,(?(1)python|java)',"yes,python") |
结果: span=(0, 10), match='yes,python' 子组1: span=(0, 3), match='yes' |
子组1匹配到“yes” |
src=re.match(r'(yes)?.*,(?(1)python|java)',"no,java") |
结果: span=(0, 7), match='no,java' 子组1: None |
|
match(r'(y*)?.*,(?(1)python|java)',"no,java") |
结果: span=(0, 7), match='no,java' 子组1: span=(0, 0), match='' |
子组1是空匹配 |
match(r'(y*)?.*,(?(1)python)',"yes,python") |
结果: span=(0, 10), match='yes,python' 子组1: span=(0, 1), match='y' |
|
match(r'(yes)?.*,(?(1)python)',"no,java") |
结果: span=(0, 3), match='no,' 子组1: None |
no-pattern为空,不会返回失败,而是返回匹配成功的内容 |
正则表达式使用以下方法修改字符串:
方法 |
用途 |
split() |
在正则表达式匹配的地方进行分割,并返回一个列表 |
sub() |
找到所有匹配的子字符串,并替换为新的内容 |
subn() |
跟 sub() 干一样的勾当,但返回新的字符串以及替换的数目 |
正则表达式的 split() 方法将字符串在匹配的地方进行分割,并将分割后的结果作为列表返回。它的做法其实很像字符串的 split() 方法,但这个可以使用更加广泛的分隔符。你猜的没错,它同时提供了一个模块级别的函数:re.split()
.split(string[, maxsplit=0])
通过正则表达式匹配来分割字符串。如果在 RE 中,你使用了捕获组,那么它们的内容会作为一个列表返回。你可以通过传入一个 maxsplit 参数来设置分割的数量。如果 maxsplit 的值是非 0,表示至多有 maxsplit 个分割会被处理,剩下的内容作为列表的最后一个元素返回。
下边例子中,分隔符是任何非字母数字字符:
>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']
有时候你可能不仅对分隔符之间的内容感兴趣,你可能对分隔符本身(就是正则表达式匹配的内容)也同样感兴趣。如果使用了捕获组,那么作为分隔符的值也会被返回:
>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']
模块级别的函数 re.split() 除了将 RE 作为第一个参数外,其他参数是一样的:
>>> re.split('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']
另一个常见的任务就是找到所有的匹配部分,并替换成不同的字符串。sub 方法可以帮你实现这个愿望!sub 方法有一个 replacement 参数,它可以是一个待替换的字符串,或者一个处理字符串的函数。
.sub(replacement, string[, count=0])
返回一个字符串,这个字符串从最左边开始,所有 RE 匹配的地方都替换成 replacement。如果没有找到任何匹配,那么返回原字符串。
可选参数 count 指定最多替换的次数,必须是一个非负值。默认值是 0,意思是替换所有找到的匹配。
下边是使用 sub() 方法的例子,它会将所有的颜色替换成 color:
>>> p = re.compile( '(blue|white|red)')
>>> p.sub( 'colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub( 'colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'
subn() 方法跟 sub() 方法功能一样,但区别是返回值为一个包含有两个元素的元组:一个是替换后的字符串,一个是替换的数目。
>>> p = re.compile( '(blue|white|red)')
>>> p.subn( 'colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn( 'colour', 'no colours at all')
('no colours at all', 0)
空匹配只有在它们没有紧挨着前一个匹配时才会被替换掉:
>>> p = re.compile('x*') #x*等于x{0,} 0次匹配是空匹配
>>> p.sub('-', 'abxd')
'-a-b-d-'
如果 replacement 参数是一个字符串,那么里边的反斜杠都会被处理。比如 \n 将会被转换成一个换行符,\r 转换成回车,等等。未知的转义如 \j 保持原样。逆向引用如 \6,则被 RE 中相应的捕获组匹配的内容所替换。这使你可以在替换后的字符串中插入一部分原字符串。
下边例子中,将匹配被 { 和 } 括起来的单词 section,并将 section 替换成 subsection:
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
注意:
1.这里开启了 re.VERBOSE,空格将被忽略。
2. 这里 r'subsection{\1}' 使用 \1 引用匹配模式中的 ([^}]*) 匹配的字符串内容。
有时候你可能不满足简单的字符串替换,需要在替换的过程中调用格式转换函数,replacement 参数还可以是一个函数,该函数将会在正则表达式模式每次匹配的时候被调用。在每次调用时,函数会收到一个匹配对象的参数,因此你就可以利用这个对象去计算出新的字符串并返回它。
下边的例子中,替换函数将十进制数替换为十六进制数:
>>> def hexrepl(match):
... "Return the hex string for a decimal number"
... value = int(match.group())
... return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'
当使用模块级的 re.sub() 函数时,正则表达式模式作为第一个参数。该模式可以是一个字符串或一个编译好的对象。如果你需要指定正则表达式标志,那么你必须使用后者;或者使用模式内嵌修正器,例如 sub("(?i)b+", "x", "bbbb BBBB") 返回 'x x'。