参考:
re --- 正则表达式操作 — Python 3.12.0 文档
正则表达式 – 教程 | 菜鸟教程 (runoob.com)
正则表达式——7种免费测试工具_正则表达式测试工具-CSDN博客
正则表达式中的“模式”在 Python 代码中通常都使用原始字符串表示法,即带有 'r'
前缀的字符串字面值。这个做法的背景是:例如,要匹配一个反斜杠字面值,用户将必须写成 '\\\\'
因为正则表达式必须为 \\
,而每个反斜杠在普通 Python 字符串字面值中又必须表示为 \\;
4个反斜杠才能在正则表达式中表示一个反斜杠字符,这样的写法太复杂。
正则表达式语法
正则表达式可以拼接;如果 A 和 B 都是正则表达式,则 AB 也是正则表达式。通常,如果字符串 p 匹配 A,并且另一个字符串 q 匹配 B,那么 pq 可以匹配 AB。除非 A 或者 B 包含低优先级操作,A 和 B 存在边界条件;或者命名组引用。对此,我的看法是,初学者对“除非”的那几个条件可能不太熟悉,因此,尽量避免使用拼接。
正则表达式可以包含普通或者特殊字符。对于普通字符,简单来说就是匹配自身。比如,last匹配字符串'last'。对于特殊字符,既可以表示它的普通含义, 也可以影响它旁边的正则表达式的解释,需要单独学习。
下面介绍的特殊字符中,大多数都是标准正则表达式(参考自“正则表达式 – 教程 | 菜鸟教程 (runoob.com)”)具有的内容,但有些内容属于Python专有。对于Python专有的内容,用褐色标出。另外,对于容易理解的特殊字符,都不展开描述,直接拷贝原文。
特殊字符有:
.:
(点号) 在默认模式下,匹配除换行符以外的任意字符。 如果指定了旗标 DOTALL ,它将匹配包括换行符在内的任意字符。
^:
(插入符) 匹配字符串的开头, 并且在 MULTILINE 模式下也匹配换行后的首个符号。
$:匹配字符串尾或者在字符串尾的换行符的前一个字符,在 MULTILINE 模式下也会匹配换行符之前的文本。
说明:教材上的例子,经过我的测试,和标准的正则表达式有些不一样。 在'foo1\nfoo2\n'中搜索'foo.$',会匹配到'foo2',这个是Python中匹配的规则,在标准正则表达式的测试中,是匹配不了的,标准正则表达式只会在'foo1\nfoo2'匹配到'foo2'。另外,标准正则表达式在 'foo\n'
中搜索$,也只会匹配一个,而不是python中的2个(一个在换行符之前,一个在字符串的末尾)。Python中的运行结果如下,关于findall()和search()的用法,可以参考后文《Python标准库 - re -- 正则表达式 (2)》
>>> import re
>>> x = re.findall('foo.$', "foo1\nfoo2\n")
>>> print(x)
['foo2']
>>> x = re.search('foo.$', "foo1\nfoo2\n", re.MULTILINE)
>>> print(x)
<re.Match object; span=(0, 4), match='foo1'>
>>> x = re.findall('foo.$', "foo1\nfoo2\n", re.MULTILINE)
>>> print(x)
['foo1', 'foo2']
>>> x = re.findall('$', "foo\n")
>>> print(x)
['', '']
对于这个差异,我的看法是:要知道有这个差异,对于类似的情况,要考虑到这种可能存在的差异;使用时可以先测试一下。
*:对它前面的正则式匹配0到任意次重复, 尽量多的匹配字符串。
+:对它前面的正则式匹配1到任意次重复。
?:对它前面的正则式匹配0到1次重复。
*?, +?, ??:*, +, ?数量限定符都是 贪婪的;它们会匹配尽可能多的文本。而在其后加上'?'之后,会将其转换为“非贪婪的”。看下面的例子,来体会“贪婪”和“非贪婪”的区别。
str = "<a>b<b>"
<.*> # 1 matched: <a>b<b>
<.*?> # 2 matched: <a>,<b>
<.+> # 1 matched: <a>b<b>
<.+?> # 2 matched: <a>,<b>
str = "<>>"
<.?> # 1 matched: <>>
<.??> # 1 matched: <>
可以看出,“贪婪”时,会在可以匹配的情况下,会在一次匹配中尽可能多地包含字符;而“非贪婪”时则相反,会在一次匹配中尽可能少地包含字符。
*+, ++, ?+:理解这几个符号,需要理解什么是“反向追溯”(giving back)。用教材上的例子说明:例如,a*a
将匹配 'aaaa'
因为 a*
将匹配所有的 4 个 'a'
,但是,当遇到最后一个 'a'
时,表达式将执行反向追溯以便最终 a*
最后变为匹配总计 3 个 'a'
,而第四个 'a'
将由最后一个 'a'
来匹配。而后面带上"+"之后,就会变成“贪婪地、非反向追溯”,即教材上提到的占有型 数量限定符。例如,当使用 a*+a
时如果要匹配 'aaaa'
,a*+
将匹配所有的 4 个 'a'
,但是在最后一个 'a'
无法找到更多字符来匹配时,表达式将无法被反向追溯并将因此匹配失败。
下面这个例子,证明了“反向追溯”的效果。关于group的用法,见后文《Python标准库 - re -- 正则表达式 (2)》
>>> x = re.search('a*a', "aaaa")
>>> print(x)
<re.Match object; span=(0, 4), match='aaaa'>
>>> x = re.search('(a*)a', "aaaa")
>>> print(x.group(1)) # a* will match "aaa"
aaa
{m}:对其之前的正则式指定匹配 m 个重复;少于 m 的话就会导致匹配失败。
{m,n}:对正则式进行 m 到 n 次匹配,在 m 和 n 之间取尽量多。忽略 m 意为指定下界为0,忽略 n 指定上界为无限次。逗号不能省略,否则无法辨别修饰符应该忽略哪个边界。逗号两侧也不能有空格。
{m,n}+: 将导致结果 RE 匹配之前 RE 的 m 至 n 次重复,尝试匹配尽可能多的重复而 不会 建立任何反向追溯点。 这是上述数量限定符的占有型版本。通过前面对“*+, ++, ?+”的讲解,理解什么是占有型、反向追溯,这个就容易理解了。例如,在 6 个字符的字符串 'aaaaaa'
上,a{3,5}+aa
将尝试匹配 5 个 'a'
字符,然后,要求再有 2 个 'a'
,这将需要比可用的更多的字符因而会失败。
\:转义特殊字符,或者表示一个特殊序列;特殊序列之后进行讨论。
[]:用于表示一个字符集合。在一个集合中:
- 字符可以单独列出
- 可以表示字符范围,通过用
'-'
将两个字符连起来。 - 特殊字符在集合中会失去其特殊意义。比如
[(+*)]
只会匹配这几个字面字符之一'('
,'+'
,'*'
, or')'
。 - 字符类如
\w
或者\S
(如下定义) 在集合内可以接受,它们可以匹配的字符由 ASCII 或者 LOCALE 模式决定 - 不在集合范围内的字符可以通过 取反 来进行匹配。取反操作如下:如果集合首字符是'^',所有 不 在集合内的字符将会被匹配。
- 要在集合内匹配一个
']'
字面值,可以在它前面加上反斜杠,或是将它放到集合的开头。 例如,[()[\]{}]
和[]()[{}]
都可以匹配右方括号,以及左方括号,花括号和圆括号。
|:A|B
, A 和 B 可以是任意正则表达式,创建一个正则表达式,匹配 A 或者 B。
():(组合),匹配括号内的任意正则表达式,并标识出组合的开始和结尾。匹配完成后,组合的内容可以被获取,并可以在之后用 \number
转义序列进行再次匹配,之后进行详细说明。
(?...):这是个扩展标记法 (一个 '?'
跟随 '('
并无含义)。 '?'
后面的第一个字符决定了这个构建采用什么样的语法。这种扩展通常并不创建新的组合; (?P<name>...)
是唯一的例外。 以下是目前支持的扩展。
(?aiLmsux):
('a'
,'i'
,'L'
,'m'
,'s'
,'u'
,'x'
中的一个或多个) 这个组合匹配一个空字符串;这些字符对正则表达式设置以下标记 re.A (只匹配ASCII字符), re.I (忽略大小写), re.L (语言依赖), re.M (多行模式), re.S (点dot匹配全部字符), re.U (Unicode匹配), and re.X (冗长模式)。这个是Python特有用法,相当于在模式(正则表达式)里面直接携带了python的flag,就不用单独传递flag参数了
>>> x = re.findall(r'(?i)b', 'Big and big') # (?i) ingore bigger or lower case
>>> print(x)
['B', 'b']
>>> x = re.findall('foo.$', "foo1\nfoo2\n")
>>> print(x)
['foo2']
>>> x = re.findall(r'(?m)foo.$', "foo1\nfoo2\n") # (?m) multiline mode
>>> print(x)
['foo1', 'foo2']
(?:…):
正则括号的非捕获版本。(?aiLmsux-imsx:…)
:('a'
,'i'
,'L'
,'m'
,'s'
,'u'
,'x'
中的0或者多个, 之后可选跟随'-'
在后面跟随'i'
,'m'
,'s'
,'x'
中的一到多个 .) 这些字符为表达式的其中一部分 设置 或者 去除 相应标记 re.A (只匹配ASCII), re.I (忽略大小写), re.L (语言依赖), re.M (多行), re.S (点匹配所有字符), re.U (Unicode匹配), and re.X (冗长模式)。(标记描述在 模块内容 .)'a'
,'L'
and'u'
作为内联标记是相互排斥的, 所以它们不能结合在一起,或者跟随'-'
。 当他们中的某个出现在内联组中,它就覆盖了括号组内的匹配模式。在Unicode样式中,(?a:...)
切换为 只匹配ASCII,(?u:...)
切换为Unicode匹配 (默认). 在byte样式中(?L:...)
切换为语言依赖模式,(?a:...)
切换为 只匹配ASCII (默认)。这种方式只覆盖组合内匹配,括号外的匹配模式不受影响。注意:和(?aiLmsux)
相比,后面多个冒号;其实(?aiLmsux)
中加一个冒号也是可以的。(?>...)
:尝试匹配...
就像它是一个单独的正则表达式,如果匹配成功,则继续匹配在它之后的剩余表达式。 如果之后的表达式匹配失败,则栈只能回溯到(?>...)
之前 的点,因为一旦退出,这个被称为 原子化分组 的表达式将会丢弃其自身所有的栈点位。因此,"(?>.*).
"将永远不会匹配任何东西因为首先.*
将匹配所有可能的字符,然后,由于没有任何剩余的字符可供匹配,最后的.
将匹配失败。 由于原子化分组中没有保存任何栈点位,并且在它之前也没有任何栈点位,因此整个表达式将匹配失败。
说明:(?>...)这种表达式称为原子组。关于原子组的更多理解,可以先参考这个链接:关于正则表达式:原子组和非捕获组 | 码农家园 (codenong.com)
(?P<name>…):()组的另一种写法,给出了组的名称
(?P=name):()组的另一种应用方法,使用组的名称
下表对所有组的使用形式进行了总结。
引用组合 "quote" 的上下文 | 引用方法 |
---|---|
在正则式自身内 |
|
处理匹配对象 m |
|
传递到 |
|
(?#…):
注释;里面的内容会被忽略。
>>> x = re.findall(r'(?# beginning "b"s)b+(?# last "b")', 'bag boy')
>>> print(x)
['b', 'b']
可以看出,注释(?#...)中的内容,对正则表达式本身"b+"没有任何影响
(?=...):
当…
匹配时,匹配成功,但不消耗字符串中的任何字符。这个叫做 前视断言 (lookahead assertion)。比如,Isaac (?=Asimov)
将会匹配'Isaac '
,仅当其后紧跟'Asimov'
。- (?!..):当
…
不匹配时,匹配成功。这个叫 否定型前视断言 (negative lookahead assertion)。例如,Isaac (?!Asimov)
将会匹配'Isaac '
,仅当它后面 不是'Asimov'
。 - (?<=...):如果
...
的匹配内容出现在当前位置的左侧,则匹配。这叫做 肯定型后视断言 (positive lookbehind assertion)。注意,以肯定型后视断言开头的正则表达式,匹配项一般不会位于搜索字符串的开头。很可能你应该使用 search() 函数,而不是 match() 函数: - (?<!...):如果
...
的匹配内容没有出现在当前位置的左侧,则匹配。这个叫做 否定型后视断言 (negative lookbehind assertion)。
注意:(?=...),(?!...), (?<=...), (?<!...)这几个符号不要理解为一般的组了
(?(id/name)yes-pattern|no-pattern)
:如果给定的 id 或 name 存在,将会尝试匹配yes-pattern
,否则就尝试匹配no-pattern
,no-pattern
可选,也可以被忽略。比如,(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)
是一个email样式匹配,将匹配'<user@host.com>'
或'user@host.com'
,但不会匹配'<user@host.com'
,也不会匹配'user@host.com>'
。
说明:实测发现,无论标准正则表达式还是python中的正则表达式,对于'<user@host.com'
,是可以匹配的,匹配了其中的字符串"user@host.com
",但是'user@host.com>'
不匹配的。
>>> x = re.search(r"(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)", "<user@host.com")
>>> print(x)
<re.Match object; span=(1, 14), match='user@host.com'>
>>> x = re.search(r"^(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)", "<user@host.com")
>>> print(x)
None
由 '\'
和一个字符组成的特殊序列在以下列出。
\number
:匹配数字代表的组合。每个括号是一个组合,组合从1开始编号。这个特殊序列只能用于匹配前面99个组合。如果 number 的第一个数位是0, 或者 number 是三个八进制数,它将不会被看作是一个组合,而是八进制的数字值。- \A:只匹配字符串开始。注意:\A不同于^, 它永远匹配目标字符串的开始,而不会受模式修饰符,比如Multiline的限制。例如:
>>> x = re.findall(r"\Aseg", "segment\nsegment\n", re.MULTILINE)
>>> print(x)
['seg']
>>> x = re.findall(r"^seg", "segment\nsegment\n", re.MULTILINE)
>>> print(x)
['seg', 'seg']
\b
:匹配空字符串,但只在单词开始或结尾的位置。我认为这里理解为匹配边界字符更合理。\B
:匹配空字符串,但 不 能在词的开头或者结尾。我认为这里理解为匹配非边界字符更合理。\d
:匹配一个数字[0-9]- \D:匹配一个非数字,也就是[^0-9]
- \s:配置任意一个空白字符。(包括
[ \t\n\r\f\v]
,还有很多其他字符,比如不同语言排版规则约定的不换行空格)。如果 ASCII 被设置,就只匹配[ \t\n\r\f\v]
。 - \S:匹配任何非空白字符。就是
\s
取非。如果设置了 ASCII 标志,就相当于[^ \t\n\r\f\v]
。 - \w:匹配字母数字下划线。在Python中,匹配 Unicode 单词类字符;这包括字母数字字符 (如 str.isalnum() 所定义的) 以及下划线 (
_
)。 如果使用了 ASCII 旗标,则将只匹配[a-zA-Z0-9_]
。 - \W:匹配非字母数字下划线,相当于非\w。
- \Z:只匹配字符串尾。注意:\Z不同于$, 它永远匹配目标字符串的结尾,而不会受模式修饰符,比如Multiline的限制。例如:
>>> x = re.findall(r"ment\Z", "segment\nsegment", re.MULTILINE)
>>> print(x)
['ment']
>>> x = re.findall(r"ment$", "segment\nsegment", re.MULTILINE)
>>> print(x)
['ment', 'ment']
模块内容
下面的内容,只摘取其中一部分。
标志
re.X / re.VERBOSE
这个旗标允许你通过在视觉上分隔表达式的逻辑段落和添加注释来编写更为友好并更具可读性的正则表达式。看下面的例子,下面两个正则表达式是一样的,只是第一种写法带上了注释,空白等内容,以便增强阅读性。
a = re.compile(r"""\d + # the integral part
\. # the decimal point
\d * # some fractional digits""", re.X)
b = re.compile(r"\d+\.\d*")
可以,表达式中的空白符会被忽。当一个行内包含不在字符类中并且前面没有未转义反斜杠的 #
时,则从最左边的此 #
直至行尾的所有字符都会被忽略。
对应内联标记 (?x)
。
函数
re.split(pattern, string, maxsplit=0, flags=0)
用 pattern 分开 string 。 如果在 pattern 中捕获到括号,那么所有的组里的文字(包含pattern匹配的字符串)也会包含在列表里。如果 maxsplit 非零, 最多进行 maxsplit 次分隔, 剩下的字符全部返回到列表的最后一个元素。
>>> import re
>>> re.split(r'\W+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'(\W+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'\W+', 'Words, words, words.',1)
['Words', 'words, words.']
>>> re.split('[a-f]+', '0a3B9', flags=re.IGNORECASE)
['0', '3', '9']
说明:
- '\W+'表示非字母数字下划线的字符,在'Words, words, words.'有3个: ‘, ’ (逗号空格) ', ' (逗号空格) '.'(句点)。
- flags=re.IGNORECASE,表示忽略大小写,于是,B也匹配'[a-f]+',因此也是一个分隔符
样式的空匹配(单词边界匹配)仅在与前一个空匹配不相邻时才会拆分字符串。
>>> re.split(r'\b', 'Words, words, words.')
['', 'Words', ', ', 'words', ', ', 'words', '.']
>>> re.split(r'\W*', '...words...')
['', '', 'w', 'o', 'r', 'd', 's', '', '']
>>> re.split(r'(\W*)', '...words...')
['', '...', '', '', 'w', '', 'o', '', 'r', '', 'd', '', 's', '...', '', '', '']
>>> re.split(r'\W+', '...words...')
['', 'words', '']
>>> re.split(r'(\W+)', '...words...')
['', '...', 'words', '...', '']
说明:
- \b表示单词边界。对于'Words, words, words.'这个字符串而言,有3个单词,每个单词有2个边界,因此一共有6个边界。将边界处增加一个灰色空格:' Words , words , words .',用于指示匹配的位置,注意:实际匹配的时候,是没有这个空格的。
- 模式中带括号会包含匹配的字符串和分隔出来的字符串,因此将其和不带括号的结果比较,可以看出模式到底匹配了哪些字符串
- \W*可以表示0到多个非字母数字下划线字符。注意:因为可以表示0个,所以words中每个字母之间都有匹配。如果将*换成+,words中每个字母之间就不存在匹配了。
re.findall(pattern, string, flags=0)
返回 pattern 在 string 中的所有非重叠匹配,以字符串列表或字符串元组列表的形式。如果没有组或者仅有1个组,返回字符串列表;如果有多个组,则返回字符串元组列表。
>>> re.findall(r'\bf[a-z]*', 'which foot or hand fell fastest')
['foot', 'fell', 'fastest']
>>> re.findall(r'(\w+)=(\d+)', 'set width=20 and height=10')
[('width', '20'), ('height', '10')]
说明:
- 第二个例子中,'(\w+)=(\d+)'有两个圆括号构成的组,因此返回字符串元组列表。每个元组中的元素分别取自两个组。
re.finditer(pattern, string, flags=0)
针对正则表达式 pattern 在 string 里的所有非重叠匹配返回一个产生 Match 对象的 iterator。通过下面的例子,看看iterator,以及re.Match对象的具体形式。
>>> x = re.finditer(r'\bf[a-z]*', 'which foot or hand fell fastest')
>>> for a in x:
... print(a)
...
<re.Match object; span=(6, 10), match='foot'>
<re.Match object; span=(19, 23), match='fell'>
<re.Match object; span=(24, 31), match='fastest'>
re.sub(pattern, repl, string, count=0, flags=0)
教材说法太抽象,通俗点说:用repl替换string中匹配pattern的字符串。repl可以是字符串或函数;如为字符串,则其中任何反斜杠转义序列都会被处理。 也就是说,\n会被转换为一个换行符, \r会被转换为一个回车符,依此类推。未知转义序列例如\&会保持原样。支持向后引用(也叫反向引用),例如\6会用样式中第 6 组所匹配到的子字符串来替换。
看例子,下面这个例子将python函数头转化为c语言的函数头
>>> import re
>>>
>>> re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
... r'static PyObject*\npy_\1(void)\n{',
... 'def myfunc():')
'static PyObject*\npy_myfunc(void)\n{'
>>> # more clearly in this way
>>> x = re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
... r'static PyObject*\npy_\1(void)\n{',
... 'def myfunc():')
>>>
>>> print(x)
static PyObject*
py_myfunc(void)
{
说明:
- \s表示空白字符。'def\s+'表示"def"后面跟至少1个空白字符。
- '([a-zA-Z_][a-zA-Z_0-9]*)'表示以字母或下划线开头、后跟字母下划线数字的名称,正好是函数名的命名要求。用圆括号括起来,因此会产生一个组,且是第一个组,故可以用'\1'引用。在repl对应的表达式中可以看到引用了这个组'\1'
- '\(\s*\)'这里的圆括号用了转义,因此不会产生组了,而是直接使用字符'圆括号'本身
- repl中的2个'\n'是回车符的转义。如果我们用print打印re.sub()的返回值,可以更清楚地看到这点
如果repl是一个函数,则它会在每次和pattern匹配时被调用。该函数接受单个 Match 参数,并且需要返回替换字符串。例如:
>>> import re
>>> def dashrepl(matchobj):
... if matchobj.group(0) == '-':
... return ''
... else:
... return '-'
...
>>> re.sub('-{1,2}', dashrepl, 'pro----gram-files')
'pro--gramfiles'
说明:
- re.sub()函数的repl参数传递了一个函数。该函数在匹配到'-'时,返回空字符串;匹配到其余情况时,返回'-'
- pattern是:匹配1或2个'-',在目标字符串中,会匹配3个对象: "pro----gram-files"
可选参数 count 是要替换的最大次数;count 必须是非负整数。如果省略这个参数或设为 0,所有的匹配都会被替换。
样式的空匹配仅在与前一个空匹配不相邻时才会被替换。
>>> re.sub('x*', '-', 'abxd')
'-a-b--d-'
说明:
- 一共有5处匹配,分别是:'a'前面,'b'前面, 'x', 'd'前面, 行结尾
关于命名组和的引用,会单独列出来一个专题。
re.subn(pattern, repl, string, count=0, flags=0)
行为与 sub() 相同,但是返回一个元组(替换结果字符串,替换次数)。
>>> re.subn(r'(\w+)', r'\g<1> \g<1>', 'ab12cd34ef56')
('ab12cd34ef56 ab12cd34ef56', 1)
>>> re.subn(r'(\d+)', r'\g<1>\g<1>', 'ab12cd34ef56')
('ab1212cd3434ef5656', 3)
说明:
- 例子中的\g<1>就是一种命名组的使用方法,等价于\1,即所谓的反向引用。\g<1>这种用法可以用于pattern和repl参数
re.escape(pattern)
该函数会将特殊字符表达为转义的形式,即前面加一个'\'
>>> print(re.escape('https://www.python.org'))
https://www\.python\.org
>>> operators = ['+', '-', '*', '/', '**', '|']
>>> print(', '.join(map(re.escape, sorted(operators, reverse=True))))
\|, /, \-, \+, \*\*, \*
通过这个函数,我们可以测试字符串中有哪些特殊字符、在直接显示时是需要转义符的。