正则表达式介绍
正则表达式(Regular expressions 也称为 REs,或 regexes 或
regex patterns)本质上是一个微小的且高度专业化的编程语言。它被嵌入到 Python 中,并通过 re
模块提供给程序猿使用。使用正则表达式,你需要指定一些规则来描述那些你希望匹配的字符串集合。这些字符串集合可能包含英语句子、 e-mail 地址、TeX
命令,或任何你想要的东东。
正则表达式模式被编译成一系列的字节码,然后由一个 C
语言写的匹配引擎所执行。对于高级的使用,你可能需要更关注匹配引擎是如何执行给定的 RE,并通过一定的方式来编写
RE,以便产生一个可以运行得更快的字节码。本文暂不讲解优化的细节,因为这需要你对匹配引擎的内部机制有一个很好的理解。但本文的例子均是符合标准的正则表达式语法。
小甲鱼注释:Python 的正则表达式引擎是用 C 语言写的,所以效率是极高的。另,所谓的正则表达式,这里说的
RE,就是上文我们提到的“一些规则”。
正则表达式语言相对较小,并且受到限制,所以不是所有可能的字符串处理任务都可以使用正则表达式来完成。还有一些特殊的任务,可以使用正则表达式来完成,但是表达式会因此而变得非常复杂。在这种情况下,你可能通过自己编写
Python 代码来处理会更好些;尽管 Python 代码比一个精巧的正则表达式执行起来会慢一些,但可能会更容易理解。
小甲鱼注释:这可能是大家常说的“丑话说在前”吧,大家别管他,正则表达式非常优秀,她可以处理你 98.3%
的文本任务,一定要好好学哦~~~~~
简单的模式
我们将从最简单的正则表达式学习开始。由于正则表达式常用于操作字符串的,因此我们从最常见的任务下手:字符匹配。
字符匹配
大多数字母和字符会匹配它们自身。举个例子,正则表达式 FishC 将完全匹配字符串 "FishC"。(你可以启用不区分大小写模式,这将使得
FishC 可以匹配 "FISHC" 或 "fishc",我们会在后边讨论这个话题。)
当然这个规则也有例外。有少数特殊的字符我们称之为元字符(metacharacter),它们并不能匹配自身,它们定义了字符类、子组匹配和模式重复次数等。本文用很大的篇幅专门讨论了各种元字符及其作用。
下边是元字符的完整列表(我们将在后边逐一讲解):
.
^ $ * + ? { } [ ] \ | ( )
小甲鱼注释:如果没有这些元字符,正则表达式就变得跟字符串的 find()
方法一样平庸了......
我们先来看下方括号 [
],它们指定一个字符类用于存放你需要匹配的字符集合。可以单独列出需要匹配的字符,也可以通过两个字符和一个横杆 - 指定匹配的范围。例如 [abc] 会匹配字符 a,b 或 c;[a-c]
可以实现相同的功能。后者使用范围来表示与前者相同的字符集合。如果你想只匹配小写字母,你的 RE 可以写成 [a-z]。
需要注意的一点是:元字符在方括号中不会触发“特殊功能”,在字符类中,它们只匹配自身。例如
[akm$] 会匹配任何字符 'a','k','m' 或 '$','$' 是一个元字符,但在方括号中它不表示特殊含义,它只匹配
'$'
字符本身。
你还可以匹配方括号中未列出的所有其他字符。做法是在类的开头添加一个脱字符号 ^ ,例如 [^5] 会匹配除了 '5'
之外的任何字符。
或许最重要的元字符当属反斜杠 \ 了。跟 Python
的字符串规则一样,如果在反斜杠后边紧跟着一个元字符,那么元字符的“特殊功能”也不会被触发。例如你需要匹配符号 [ 或 \,你可以在它们前面加上一个反斜杠,以消除它们的特殊功能:\[,\\。
反斜杠后边跟一些字符还可以表示特殊的意义,例如表示十进制数字,表示所有的字母或者表示非空白的字符集合。
小甲鱼解释:反斜杠真牛逼,反斜杠后边跟元字符去除特殊功能,反斜杠后边跟普通字符实现特殊功能。
让我们来举个例子:\w
匹配任何单词字符。如果正则表达式以字节的形式表示,这相当于字符类 [a-zA-Z0-9_];如果正则表达式是一个字符串,\w 会匹配所有 Unicode
数据库(unicodedata 模块提供)中标记为字母的字符。你可以在编译正则表达式的时候,通过提供 re.ASCII 表示进一步限制 \w 的定义。
小甲鱼解释:re.ASCII 标志使得 \w 只能匹配 ASCII 字符,不要忘了,Python3 是 Unicode
的。
下边列举一些反斜杠加字符构成的特殊含义:
特殊字符
|
含义
|
\d
|
匹配任何十进制数字;相当于类 [0-9] |
\D
|
与 \d 相反,匹配任何非十进制数字的字符;相当于类 [^0-9] |
\s
|
匹配任何空白字符(包含空格、换行符、制表符等);相当于类 [ \t\n\r\f\v] |
\S
|
与 \s 相反,匹配任何非空白字符;相当于类 [^ \t\n\r\f\v] |
\w
|
匹配任何单词字符,见上方解释 |
\W
|
于 \w 相反 |
\b
|
匹配单词的开始或结束 |
\B
|
与 \b 相反 |
它们可以包含在一个字符类中,并且一样拥有特殊含义。例如 [\s,.] 是一个字符类,它将匹配任何空白字符(/s 的特殊含义),',' 或 '.'。
最后我们要讲的一个元字符是
.,它匹配除了换行符以外的任何字符。如果设置了
re.DOTALL 标志,.
将匹配包括换行符在内的任何字符。
重复的事情
使用正则表达式能够轻松的匹配不同的字符集合,但 Python
字符串现有的方法却无法实现。然而,如果你认为这是正则表达式的唯一优势,那你就 too young too native
了。正则表达式有另一个强大的功能,就是你可以指定 RE 部分被重复的次数。
我们来看看 * 这个元字符,当然它不是匹配 '*'
字符本身(我们说过元字符都是有特殊能力的),它用于指定前一个字符匹配零次或者多次。
例如 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
亿)。
小甲鱼解释:如果是 {,n} 相当于 {0,n};如果是 {m,} 相当于
{m,+无穷};如果是 {n},则是重复前一个字符 n 次。另外还有一个超容易出错的是写成 {m,
n},看着挺美,但注意,正则表达式里边不能随意添加空格,不然会改变原来的含义。
聪明的鱼油应该已经发现了,其实
*、+ 和 ? 都可以使用 {m,n} 来代替。{0,} 跟 * 是一样的;{1,} 跟 + 是一样的;{0,1} 跟 ? 是一样的。不过还是鼓励大家记住并使用 *、+ 和 ?,因为这些字符更短并且更容易阅读。
小甲鱼解释:还有一个原因是匹配引擎对 * + ? 做了优化,效率要更高些。
使用正则表达式
现在我们开始来写一些简单的正则表达式吧。Python 通过 re
模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配。
小甲鱼解释:re 模块是使用 C
语言编写,所以效率比你用普通的字符串方法要高得多;将正则表达式进行编译(compile)也是为了进一步提高效率;后边我们会经常提到“模式”,指的就是正则表达式被编译成的模式对象。
编译正则表达式
正则表达式被编译为模式对象,该对象拥有各种方法供你操作字符串,如查找模式匹配或者执行字符串替换。
- >>> import re
- >>> p = re.compile('ab*')
- >>> p
- <_sre.SRE_Pattern object at 0x...>
re.compile() 也可以接受 flags
参数,用于开启各种特殊功能和语法变化,我们会在后边一一介绍。
现在我们先来看个简单的例子:
- >>> p = re.compile('ab*', re.IGNORECASE)
正则表达式作为一个字符串参数传给
re.compile()。由于正则表达式并不是 Python
的核心部分,因此没有为它提供特殊的语法支持,所以正则表达式只能以字符串的形式表示。(有些应用根本就不需要使用到正则表达式,所以 Python
社区的小伙伴们认为没有必要将其纳入 Python 的核心。)相反,re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和
zlib 模块。
使用字符串来表示正则表达式保持了 Python
简洁的一贯风格,但也因此有一些负面影响,下边我们就来谈一谈。
麻烦的反斜杠
上一篇中我们已经提到了,正则表达式使用 '\' 字符来使得一些普通的字符拥有特殊的能力(例如
\d
表示匹配任何十进制数字),或者剥夺一些特殊字符的能力(例如 \[ 表示匹配左方括号 '[')。这会跟 Python
字符串中实现相同功能的字符发生冲突。
小甲鱼解释:挺拗口,接着看例子你就懂了~
现在的情况是,你需要在 LaTeX
文件中使用正则表达式匹配字符串
'\section'。因为反斜杠作为需要匹配的特殊字符,所以你需要再它前边加多一个反斜杠来剥夺它的特殊功能。所以我们会把正则表达式的字符写成
'\\section'。
但不要忘了,Python 在字符串中同样使用反斜杠来表示特殊意义。因此,如果我们想将 '\\section' 完整地传给
re.compile(),我们需要再次添加两个反斜杠......
匹配字符 | 匹配阶段 |
\section | 需要匹配的字符串 |
\\section | 正则表达式使用 '\\' 表示匹配字符 '\' |
"\\\\section" | 不巧,Python 字符串也使用 '\\' 表示字符 '\' |
简而言之,为了匹配反斜杠这个字符,我们需要在字符串中使用四个反斜杠才行。所以,在正则表达式中频繁地使用反斜杠,会造成反斜杠风暴,进而导致你的字符串极其难懂。
解决方法是使用
Python 的原始字符串来表示正则表达式(就是在字符串前边加上 r,大家还记得吧...):
正则字符串 | 原始字符串 |
"ab*" | r"ab*" |
"\\\\section" | r"\\section" |
"\\w+\\s+\\1" | r"\w+\s+\1" |
小甲鱼解释:强烈建议使用原始字符串来表达正则表达式。
实现匹配
当你将正则表达式编译之后,你就得到一个模式对象。那你拿他可以用来做什么呢?模式对象拥有很多方法和属性,我们下边列举最重要的几个来讲:
方法 | 功能 |
match() | 判断一个正则表达式是否从开始处匹配一个字符串 |
search() | 遍历字符串,找到正则表达式匹配的第一个位置 |
findall() | 遍历字符串,找到正则表达式匹配的所有位置,并以列表的形式返回 |
finditer() | 遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回 |
如果没有找到任何匹配的话,match()
和 search() 会返回 None;如果匹配成功,则会返回一个匹配对象(match
object),包含所有匹配的信息:例如从哪儿开始,到哪儿结束,匹配的子字符串等等。
接下来我们一步步讲解:
- >>> import re
- >>> p = re.compile('[a-z]+')
- >>> p
- re.compile('[a-z]+')
现在,你可以尝试使用正则表达式
- >>> print(p.match(""))
- None
复制代码
因为 +
表示匹配一次或者多次,所以空字符串不能被匹配。因此,match() 返回 None。
我们再尝试一个可以匹配的字符串:
- >>> m = p.match('fishc')
- >>> m
- <_sre.SRE_Match object; span=(0, 5), match='fishc'>
在这个例子中,match()
返回一个匹配对象,我们将其存放在变量 m
中,以便日后使用。
接下来让我们来看看匹配对象里边有哪些信息吧。匹配对象包含了很多方法和属性,以下几个是最重要的:
方法 | 功能 |
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')
有两个方法可以返回所有的匹配结果,一个是
findall(),另一个是 finditer()。
findall() 返回的是一个列表:
- >>> p = re.compile('\d+')
- >>> p.findall('3只小甲鱼,15条腿,多出的3条在哪里?')
- ['3', '15', '3']
findall() 需要在返回前先创建一个列表,而
finditer() 则是将匹配对象作为一个迭代器返回:
- >>> iterator = p.finditer('3只小甲鱼,15条腿,还有3条去了哪里?')
- >>> iterator
- <callable_iterator object at 0x10511b588>
- >>> for match in iterator:
- print(match.span())
-
- (0, 1)
- (6, 8)
- (13, 14)
小甲鱼解释:如果列表很大,那么返回迭代器的效率要高很多。迭代器的相关知识请看:《零基础入门学习Python》048 |
魔法方法:迭代器
模块级别的函数
使用正则表达式也并非一定要创建模式对象,然后调用它的匹配方法。因为,re
模块同时还提供了一些全局函数,例如 match(),search(),findall(),sub()
等等。这些函数的第一个参数是正则表达式字符串,其他参数跟模式对象同名的方法采用一样的参数;返回值也一样,同样是返回 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(如果你是 Perl 的粉丝,那么你有福了,因为这些简写跟 Perl 是一样的,例如 re.VERBOSE 的简写是
re.X)。另外,多个标志还可以同时使用(通过“|”),如:re.I | re.M 就是同时设置 I 和 M
标志。
下边列举一些支持的编译标志:
标志 | 含义 |
ASCII, A | 使得转义符号如 \w,\b,\s 和 \d 只能匹配 ASCII 字符 |
DOTALL, S | 使得 . 匹配任何符号,包括换行符 |
IGNORECASE, I | 匹配的时候不区分大小写 |
LOCALE, L | 支持当前的语言(区域)设置 |
MULTILINE, M | 多行匹配,影响 ^ 和 $ |
VERBOSE, X (for 'extended') | 启用详细的正则表达式 |
下面我们来详细讲解一下它们的含义:
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"""
- &[#] # 开始数字引用
- (
- 0[0-7]+ # 八进制格式
- | [0-9]+ # 十进制格式
- | x[0-9a-fA-F]+ # 十六进制格式
- )
- ; # 结尾分号
- """, re.VERBOSE)
如果没有设置 VERBOSE
标志,那么同样的正则表达式会写成:
- charref = re.compile("&#(0[0-7]+|[0-9]+|x[0-9a-fA-F]+);")
哪个可读性更加?相信大家心里有底了。
更多强大的功能
到目前为止,我们只是介绍了正则表达式的一部分功能。在这一篇中,我们会学习到一些新的元字符,然后再教大家如何使用组来获得被匹配的部分文本。
更多元字符
还有一些元字符我们没有讲到,接下来小甲鱼一一为大家讲解。
有些元字符它们不匹配任何字符,只是简单地表示成功或失败,因此这些字符也称之为零宽断言。例如
\b 表示当前位置位于一个单词的边界,但
\b
并不能改变位置。因此,零宽断言不应该被重复使用,因为 \b 并不会修改当前位置,所以 \b\b 跟 \b 是没什么两样的。
小甲鱼解释:很多人可能不理解“改变位置”和“零宽断言”的意思?我尝试解释下,比如 abc 匹配完 a 之后,咱的当前位置就会移动,才能继续匹配 b,依次类推...但是 \babc 的话,\b
表示当前位置在单词的边界(单词的第一个字母或者最后一个字母),这时候当前位置不会发生改变,接着将 a
与当前位置的字符进行匹配......
|
或操作符,对两个正则表达式进行或操作。如果 A 和 B
是正则表达式,A | B 会匹配 A 或 B 中出现的任何字符。为了能够更加合理的工作,| 的优先级非常低。例如 Fish|C 应该匹配 Fish 或 C,而不是匹配 Fis,然后一个 'h' 或 'C'。
同样,我们使用 \| 来匹配 '|' 字符本身;或者包含在一个字符类中,像这样 [|]。
^
匹配字符串的起始位置。如果设置了
MULTILINE 标志,就会变成匹配每一行的起始位置。在 MULTILINE
中,每当遇到换行符就会立刻进行匹配。
举个例子,如果你只希望匹配位于字符串开头的单词 From,那么你的正则表达式可以写为 ^From:
- >>> print(re.search('^From', 'From Here to Eternity'))
- <_sre.SRE_Match object; span=(0, 4), match='From'>
- >>> print(re.search('^From', 'Reciting From Memory'))
- None
$
匹配字符串的结束位置,每当遇到换行符也会离开进行匹配。
>>>
print(re.search('}$', '{block}'))
<_sre.SRE_Match object; span=(6, 7),
match='}'>
>>> print(re.search('}$', '{block}
'))
None
>>> print(re.search('}$',
'{block}\n'))
<_sre.SRE_Match object; span=(6, 7),
match='}'>
同样,我们使用 \$ 来匹配 '$' 字符本身;或者包含在一个字符类中,像这样 [$]。
\A
只匹配字符串的起始位置。如果没有设置
MULTILINE 标志的时候,\A 和
^ 的功能是一样的;但如果设置了
MULTILINE 标志,则会有一些不同:\A
还是匹配字符串的起始位置,但 ^
会对字符串中的每一行都进行匹配。
\Z
只匹配字符串的结束位置。
\b
单词边界,这是一个只匹配单词的开始和结尾的零宽断言。“单词”定义为一个字母数字的序列,所以单词的结束指的是空格或者非字母数字的字符。
下边例子中,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
只是用来表示退格符。
\B
另一个零宽断言,与 \b 的含义相反,\B
表示非单词边界的位置。
分组
通常在实际的应用过程中,我们除了需要知道一个正则表达式是否匹配之外,还需要更多的信息。对于比较复杂的内容,正则表达式通常使用分组的方式分别对不同内容进行匹配。
下边的例子,我们将
RFC-822 头用“:”号分成名字和值分别匹配:
- From: author@example.com
- User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
- MIME-Version: 1.0
- To: editor@example.com
像这种情况,我们就可以写一个正则表达式先来匹配一整个
RFC-822 头,然后利用分组功能,使用一个组来匹配头的名字,另一个组匹配名字对应的值。
小甲鱼解释:RFC-822
是电子邮件的标准格式,当然看到这里你还不知道分组要怎么分,不急,请接着往下看......
在正则表达式中,使用元字符
( ) 来划分组。( )
元字符跟数学表达式中的小括号含义差不多;它们将包含在内部的表达式组合在一起,所以你可以对一个组的内容使用重复操作的元字符,例如 *,+,? 或者 {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')
小甲鱼解释:start() 是获得参数子组的开始位置;end() 是获得对应子组的结束位置;span()
是获得对应子组的范围。
我们还特么可以通过 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'
如果只是搜索字符串,反向引用不会被用到,因为很少有文本格式会这样来重复字符。但是,你很快会发现,在字符串替换的时候,反向引用是非常有用的(深井冰)!
小甲鱼注释:注意,在 Python 的字符串中会使用反斜杠加数字的方式来表示数字的值对应的 ASCII
字符,所以在使用反向索引的正则表达式中,我们依然强调要使用原始字符串。
修改字符串
我们已经介绍完如何对字符进行搜索,接下来我们讲讲正则表达式如何修改字符串。
正则表达式使用以下方法修改字符串:
方法 | 用途 |
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*')
- >>> 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 引用匹配模式中的 ([^}]*)
匹配的字符串内容。
还可以使用 Python 的扩展语法 (?P<name>...)
指定命名组,引用命名组的语法是 \g<name> 会将名字为 \g<2> 其实就相当于 \g<2>0 的含义是引用序号为 2 的组,然后后边匹配一个字符 '0',而你写成 \20 就会被认为是引用序号为 20 的组了。
- >>> p = re.compile('section{ (?P<name> [^}]* ) }',
re.VERBOSE)
- >>> p.sub(r'subsection{\1}','section{First}')
- 'subsection{First}'
- >>> p.sub(r'subsection{\g<1>}','section{First}')
- 'subsection{First}'
- >>> p.sub(r'subsection{\g<name>}','section{First}')
- 'subsection{First}'
有时候你可能不满足简单的字符串替换,你可能需要在替换的过程中动点“手脚”......没关系,一样可以满足你!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'。
常见问题
正则表达式是一个非常强大的工具,但在有些时候它并不能直观地按照你的意愿来运行。本篇我们将指出一些最常见的错误。
使用字符串方法
有时使用 re 模块是个错误!如果你匹配一个固定的字符串或者单个字符类,并且你没有使用 re
的任何标志(像 IGNORECASE
标志),那么就没有必要使用正则表达式了。字符串有一些方法是对固定字符串进行操作的,并且它们通常比较快。因为它们都是独立优化的 C
语言小循环,目的是在简单的情况下代替功能更加强大、更具通用性的正则表达式引擎。
举个例子,例如你想把字符串中所有的 dead 替换成 word,你会想到使用正则表达式的 re.sub()
方法来实现,但这么简单的替换,还是考虑直接使用字符串的 replace() 方法吧。但有一点你需要注意,就是 replace() 会在单词里边进行替换,像
swordfish 会变成 sdeedfish,这显然不是你想要的!replace()
没办法识别单词的边界,因此你才来考虑使用正则表达式。只需要将 RE 的模式写成 \bword\b
即可胜任此任务。
另一个常见的情况是从一个字符串中删除单个字符或者用另一个字符替代它。你也许会想到用 re.sub('\n', ' ', S)
这样的正则表达式来实现,但其实字符的 translate() 方法完全能够胜任这个任务,并且比任何正则表达式操作起来更快些。
简而言之,在使用 re
模块之前,先考虑一下你的问题是否可以用更快速、简单的字符串自带方法来解决。
match() VS
search()
match() 函数只会检查 RE 是否在字符串的开始处匹配,而 search()
会遍历整个字符串搜索匹配的内容。记住这一区别很重要。再次强调一下,match()
只会报告一次成功的匹配,并且匹配的位置必须是从字符串的第一个字符开始:
- >>> print(re.match('super', 'superstition').span())
- (0, 5)
- >>> print(re.match('super', 'insuperable'))
- None
另一方面,search()
函数将遍历整个字符串,并报告它找到的第一个匹配:
- >>> print(re.search('super', 'superstition').span())
- (0, 5)
- >>> print(re.search('super', 'insuperable').span())
- (2, 7)
有时候你可能会耍点小聪明,使用 re.match()
然后在 RE 的前边加上 .*。但尽量不要这么做,最好采用 re.search()
代替。正则表达式编译器会对 REs 做一些分析,以便可以在搜索匹配时提高速度。一般分析会先找到匹配的第一个字符是什么。举个例子,模式 Crow 必须从字符 'C' 开始匹配,那么匹配引擎分析后会快速遍历字符串,然后在
'C'
被找到之后才开始全部匹配。
按照上面的分析,你添加一个 .*
会导致这个优化失败,这就需要从头到尾扫描一遍,然后再回溯匹配 RE 剩余的部分。所以,请使用 re.search() 代替。
贪婪 VS 非贪婪
当重复一个正则表达式时,如果使用
a*,那么结果是尽可能多地去匹配。当你尝试匹配一对对称的定界符,例如 HTML
标志中的尖括号,默认的贪婪模式会使得你很困扰。
我们来看下例子:
- >>> s =
'<html><head><title>Title</title>'
- >>> len(s)
- 32
- >>> print(re.match('<.*>', s).span())
- (0, 32)
- >>> print(re.match('<.*>', s).group())
- <html><head><title>Title</title>
RE 匹配在 < 后,.*
消耗掉字符串的剩余部分。由于正则表达式默认是贪婪的原因,RE 必须从字符串的尾部一个字符一个字符地回溯,直到找到匹配的 >。大家看到,按照这种方法,最后找到匹配内容竟是
< 开始,到 >
结束。显然这不是你想要的结果。
在这种情况下,解决方案是使用非贪婪的限定符 *?、+?、?? 或 {m,n}?,尽可能地匹配小的文本。
- >>> print(re.match('<.*?>', s).group())
- <html>
在上边的例子中,<
被匹配后立刻尝试匹配,如果失败,匹配引擎前进一步,尝试下一个字符,直到第一次匹配 >,这样就得到了我们想要的结果。
注意,使用正则表达式分析
HTML 和 XML 是很痛苦的。当你编写一个正则表达式去处理所有可能的情况时,你会发现 HTML 和 XML
总会打破你的“规则”,这让你很头疼......像这样的话,建议使用 HTML 和 XML 解析器来处理更合适。
使用
re.VERBOSE
现在你应该意识到了,正则表达式的表示非常紧凑。这也带来了一个问题,就是不好阅读。中等复杂的正则表达式可能包含许多反斜杠、圆括号和元字符,以至于难以读懂。
在这些
REs 中,当编译正则表达式时指定 re.VERBOSE
标志是非常有帮助的。因为它允许你可以编辑正则表达式的格式,使之更清楚。
re.VERBOSE
标志有几个作用。在正则表达式中不在字符类中的空白字符将被忽略。这就意味着像 I love FishC 这样的表达式和可读性较差的 IloveFishC 相同。但
[a b] 将匹配字符 'a'、'b' 或 '
';另外,你也可以把注释放到 RE 中,注释是从 # 开始到下一行。当使用三引号字符串时,会使得 REs 的格式更整洁:
pat =
re.compile(r"""
\s* # Skip leading
whitespace
(?P<header>[^:]+) # Header name
\s* :
# Whitespace, and a colon
(?P<value>.*?) # The
header's value -- *? used to
# lose the
following trailing whitespace
\s*$ # Trailing
whitespace to end-of-line
""", re.VERBOSE)
同样的内容,下边这个要难读得多:
pat
=
re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")
正则表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合、子组匹配、模式重复次数。正是这些特殊符号使得一个正则表达式可以匹配字符串集合而不只是一个字符串。
注1:为了便于理解,难点的地方均用斜体举了栗子。
注2:如果你对正则表达式一头雾水或者看不懂下边这个列表,那么小甲鱼极力推荐你先学习这个:Python3
如何优雅地使用正则表达式
字符
|
含义
|
.
|
表示匹配除了换行符外的任何字符 注:通过设置 re.DOTALL 标志可以使 . 匹配任何字符(包含换行符) |
|
|
A | B,表示匹配正则表达式 A 或者 B |
^
|
1. (脱字符)匹配输入字符串的开始位置 2. 如果设置了 re.MULTILINE 标志,^ 也匹配换行符之后的位置 |
$
|
1. 匹配输入字符串的结束位置 2. 如果设置了 re.MULTILINE 标志,$ 也匹配换行符之前的位置 |
\
|
1. 将一个普通字符变成特殊字符,例如 \d 表示匹配所有十进制数字 2. 解除元字符的特殊功能,例如 \. 表示匹配点号本身 3. 引用序号对应的子组所匹配的字符串 4. 详见下方列举 |
[...]
|
字符类,匹配所包含的任意一个字符 注1:连字符 - 如果出现在字符串中间表示字符范围描述;如果如果出现在首位则仅作为普通字符 注2:特殊字符仅有反斜线 \ 保持特殊含义,用于转义字符。其它特殊字符如 *、+、? 等均作为普通字符匹配 注3:脱字符 ^ 如果出现在首位则表示匹配不包含其中的任意字符;如果 ^ 出现在字符串中间就仅作为普通字符匹配 |
{M,N}
|
M 和 N 均为非负整数,其中 M <= N,表示前边的 RE 匹配 M ~ N 次 注1:{M,} 表示至少匹配 M 次 注2:{,N} 等价于 {0,N} 注3:{N} 表示需要匹配 N 次 |
*
|
匹配前面的子表达式零次或多次,等价于 {0,} |
+
|
匹配前面的子表达式一次或多次,等价于 {1,} |
?
|
匹配前面的子表达式零次或一次,等价于 {0,1} |
*?, +?, ??
|
默认情况下 *、+ 和 ?
的匹配模式是贪婪模式(即会尽可能多地匹配符合规则的字符串);*?、+? 和 ??
表示启用对应的非贪婪模式。 举个栗子:对于字符串 "FishCCC",正则表达式 FishC+ 会匹配整个字符串,而 FishC+? 则匹配 "FishC"。 |
{M,N}?
|
同上,启用非贪婪模式,即只匹配 M 次 |
(...)
|
匹配圆括号中的正则表达式,或者指定一个子组的开始和结束位置 注:子组的内容可以在匹配之后被 \数字 再次引用 举个栗子:(\w+) \1 可以字符串 "FishC FishC.com" 中的 "FishC FishC"(注意有空格) |
(?...)
|
(? 开头的表示为正则表达式的扩展语法(下边这些是 Python 支持的所有扩展语法) |
(?aiLmsux)
|
1. (? 后可以紧跟着
'a','i','L','m','s','u','x' 中的一个或多个字符,只能在正则表达式的开头使用 2. 每一个字符对应一种匹配标志:re-A(只匹配 ASCII 字符),re-I(忽略大小写),re-L(区域设置),re-M(多行模式), re-S(. 匹配任何符号),re-X(详细表达式),包含这些字符将会影响整个正则表达式的规则 3. 当你不想通过 re.compile() 设置正则表达式标志,这种方法就非常有用啦 注意,由于 (?x) 决定正则表达式如何被解析,所以它应该总是被放在最前边(最多允许前边有空白符)。如果 (?x) 的前边是非空白字符,那么 (?x) 就发挥不了作用了。 |
(?:...)
|
非捕获组,即该子组匹配的字符串无法从后边获取 |
(?P<name>...)
|
命名组,通过组的名字(name)即可访问到子组匹配的字符串 |
(?P=name)
|
反向引用一个命名组,它匹配指定命名组匹配的任何内容 |
(?#...)
|
注释,括号中的内容将被忽略 |
(?=...)
|
前向肯定断言。如果当前包含的正则表达式(这里以 ...
表示)在当前位置成功匹配,则代表成功,否则失败。一旦该部分正则表达式被匹配引擎尝试过,就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。 举个栗子:love(?=FishC) 只匹配后边紧跟着 "FishC" 的字符串 "love" |
(?!...)
|
前向否定断言。这跟前向肯定断言相反(不匹配则表示成功,匹配表示失败)。 举个栗子:FishC(?!\.com) 只匹配后边不是 ".com" 的字符串 "FishC" |
(?<=...)
|
后向肯定断言。跟前向肯定断言一样,只是方向相反。 举个栗子:(?<=love)FishC 只匹配前边紧跟着 "love" 的字符串 "FishC" |
(?<!...)
|
后向否定断言。跟前向肯定断言一样,只是方向相反。 举个栗子:(?<!FishC)\.com 只匹配前边不是 "FishC" 的字符串 ".com" |
(?(id/name)yes-pattern|no-pattern)
|
1. 如果子组的序号或名字存在的话,则尝试 yes-pattern 匹配模式;否则尝试 no-pattern 匹配模式 2. no-pattern 是可选的 举个栗子:(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$) 是一个匹配邮件格式的正则表达式,可以匹配 <user@fishc.com> 和 'user@fishc.com',但是不会匹配 '<user@fishc.com' 或 'user@fishc.com>' |
\
|
下边列举了由字符 '\' 和另一个字符组成的特殊含义。注意,'\' + 元字符的组合可以解除元字符的特殊功能 |
\序号
|
1. 引用序号对应的子组所匹配的字符串,子组的序号从 1 开始计算 2. 如果序号是以 0 开头,或者 3 个数字的长度。那么不会被用于引用对应的子组,而是用于匹配八进制数字所表示的 ASCII 码值对应的字符 举个栗子:(.+) \1 会匹配 "FishC FishC" 或 "55 55",但不会匹配 "FishCFishC"(注意,因为子组后边还有一个空格) |
\A
|
匹配输入字符串的开始位置 |
\Z
|
匹配输入字符串的结束位置 |
\b
|
匹配一个单词边界,单词被定义为 Unidcode 的字母数字或下横线字符 举个栗子:\bFishC\b 会匹配字符串 "love FishC"、FishC." 或 "(FishC)" |
\B
|
匹配非单词边界,其实就是与 \b 相反 举个栗子:py\B 会匹配字符串 "python"、"py3" 或 "py2",但不会匹配 "py "、"py." 或 "py!" |
\d
|
1. 对于 Unicode(str 类型)模式:匹配任何一个数字,包括 [0-9] 和其他数字字符;如果开启了 re.ASCII 标志,就只匹配
[0-9] 2. 对于 8 位(bytes 类型)模式:匹配 [0-9] 中任何一个数字 |
\D
|
匹配任何非 Unicode 的数字,其实就是与 \d 相反;如果开启了 re.ASCII 标志,则相当于匹配 [^0-9] |
\s
|
1. 对于 Unicode(str 类型)模式:匹配 Unicode 中的空白字符(包括 [ \t\n\r\f\v] 以及其他空白字符);如果开启了
re.ASCII 标志,就只匹配 [ \t\n\r\f\v] 2. 对于 8 位(bytes 类型)模式:匹配 ASCII 中定义的空白字符,即 [ \t\n\r\f\v] |
\S
|
匹配任何非 Unicode 中的空白字符,其实就是与 \s 相反;如果开启了 re.ASCII 标志,则相当于匹配 [^ \t\n\r\f\v] |
\w
|
1. 对于 Unicode(str 类型)模式:匹配任何 Unicode 的单词字符,基本上所有语言的字符都可以匹配,当然也包括数字和下横线;如果开启了
re.ASCII 标志,就只匹配 [a-zA-Z0-9_] 2. 对于 8 位(bytes 类型)模式:匹配 ASCII 中定义的字母数字,即 [a-zA-Z0-9_] |
\W
|
匹配任何非 Unicode 的单词字符,其实就是与 \w 相反;如果开启了 re.ASCII 标志,则相当于 [^a-zA-Z0-9_] |
转义符号
|
正则表达式还支持大部分 Python 字符串的转义符号:\a,\b,\f,\n,\r,\t,\u,\U,\v,\x,\\ 注1:\b 通常用于匹配一个单词边界,只有在字符类中才表示“退格” 注2:\u 和 \U 只有在 Unicode 模式下才会被识别 注3:八进制转义(\数字)是有限制的,如果第一个数字是 0,或者如果有 3 个八进制数字,那么就被认为是八进制数;其他情况则被认为是子组引用;至于字符串,八进制转义总是最多只能是 3 个数字的长度 |