为学习Python中正则表达式的用法,通读了["Regular Expression HOWTO"]: https://docs.python.org/3.7/howto/regex.html#regex-howto
在此,将原文的要点进行记录与分享。
引用Introduction
Python中的正则表达式(Regular expressions,RE)模块为re
,其底层匹配引擎由C编写。正则表达式语言相对较小而且严格受限制,并不能应对所有情况。有时应该直接使用Python编写合适的代码进行处理,尽管Python代码处理效率可能比精细的正则表达式处理效率更低,但Python代码更加直接明确。
基本模式Simple Patterns
元字符
所有正则表达式的元字符
. ^ $ * + ? { } [ ] \ | ( )
方括号[
与]
的组合用以指定一组字符集[asdf]
- 连字符
-
简化标识字符范围。[a-z]
匹配所有小写26个字母。 - 在方括号中的元字符不起元字符功能,直接表示字符。
[akm$]
匹配a
,k
,w
,$
4个字符。 - 方括号中的首个字符为
^
时,表示反义,用以匹配不在字符集的字符。[^5]
匹配所有不是'5'
的字符,但[5^]
匹配字符5
与^
。
反斜杠\
是正则表达式的转义字符,其后跟随不同字符将有不同含义。例如,\w
与[a-zA-Z0-9_]
表示相同含义,其它转义遇到时再说明。
-
\d
匹配所有数字[0-9]
-
\D
匹配所有非数字[^0-9]
-
\s
匹配所有空白字符[ \t\n\r\f\v]
-
\S
匹配所有非空白字符[ \t\n\r\f\v]
-
\w
匹配所用字符[a-zA-Z0-9_]
-
\W
匹配所用字符[a-zA-Z0-9_]
- 上述序列可以用在
[ ]
中,表示相应的字符集
句点.
匹配任意非换行的字符(在re.DOTALL
设置中有不同含义)
匹配重复的字符串
星号*
表示重复前面的字符0次或任意次。
加号+
表示重复前面的字符至少1次任意次。
问号?
表示重复前面的字符0次或1次。
花括号{m,n}
表示重复前面的字符至少m次,至多n次。省略参数m
表示至少0次,省略n
表示匹配尽可能多次。
上述所有重复模式元字符均为贪婪匹配,即匹配尽可能多的字符。
使用正则表达式Using Regular Expresssions
python正则表达式匹配模式对象
Python模块函数re.compile()
将正则表达式RE编译为相应的匹配对象,正则表达式RE以字符串形式输入。
>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')
此外re.compile()
包含一些可选标识,例如
>>> p = re.compile('ab*', re.IGNORECASE)
python正则表达式的反斜杠灾难
考虑正则表达式匹配字符串\section
的情况。在正则表达语法中,为匹配字符反斜杠\
,需要使用额外反斜杠进行转义表达,即正则表达式应写作'\section'。并且,在python的字符串语法中,反斜杠\
同样为转义字符,为表示上述正则表达式,相应的python字符串为'\\\\section'
。这就导致了反斜杠灾难问题。
不过,带有前缀r
的python裸字符串不进行转义处理,即上述正则表达式可写作r'\\section'
。因此在python的正则表达式中,通常使用r
前缀的裸字符串。
进行匹配
在准备后RE匹配模式对象后,可以进行如下匹配操作:
方法/属性 | 说明 |
---|---|
match() |
判断RE是否匹配字符串开始部分 |
search() |
查找字符串是否与RE匹配的部分 |
findall() |
将所有与RE匹配的子字符串以list 返回 |
finditer() |
将所有与RE匹配的子字符串以iterator 返回 |
方法match()
与search()
返回的对象为RE匹配对象(re.Match
),未成功匹配时返回None
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
>>> p.match("")
>>> print(p.match(""))
None
>>> m = p.match('tempo')
>>> m
RE匹配对象具有如下方法
方法/属性 | 说明 |
---|---|
group() | 返回RE匹配的字符串 |
start() | 返回RE匹配的字符串起始位置 |
end() | 返回RE匹配的字符串结束位置 |
span() | 返回一个元组包含上述字符串的起始、结束位置 |
>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
>>> m.group()
'message'
>>> m.span()
(4, 11)
模块层级的函数
上述macht(), serach(), findall()
等函数并非必须通过RE模式对象进行方法调用,模块re
提供的直接使用的方法,以RE与待匹配的字符串作为输入,例如
>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
RE编译模式标志位
标志Flag | 含义 |
---|---|
ASCII, A | 使多个转义字符集\w ,\b ,\s 与\d 仅匹配ASCII字符集(相对于UNICODE) |
DOTALL, S | 使元字符句点. 匹配包括换行符的所有字符 |
IGNORECAE, I | 忽略字符大小写区别 |
LOCALE, L | 以本地字符串进行匹配 |
MULTILINE, M | 对字符串以多行模式匹配,影响元字符^ 与$ 的含义 |
VERBOSE, X | 启用verbose REs,可优化RE表达式显示 |
其中VERBOSE模式的RE忽略空格与换行,且#
表示行注释,例如
# Use verbose setting
charref = re.compile(r"""
&[#] # Start of a numeric entity reference
(
0[0-7]+ # Octal form
| [0-9]+ # Decimal form
| x[0-9a-fA-F]+ # Hexadecimal form
)
; # Trailing semicolon
""", re.VERBOSE)
# Without verbose setting
charref = re.compile("(0[0-7]+"
"|[0-9]+"
"|x[0-9a-fA-F]+);")
高阶模式More Pattern Power
元字符
下面将讨论的元字符包括
| ^ $ \A \Z \b \B
这些元字符都是零宽断言,即指定特殊含义但并不消耗任何字符串字符
- 竖线
|
表示或者,例如A
与B
为RE,那么A|B
匹配A
或者B
。 -
^
匹配行的开始,但在MULTILINE
标志下仅匹配整个字符串的开始 -
$
匹配行的末位,但在MULTILINE
标志下仅匹配整个字符串的末尾 -
\A
匹配整个字符串的开始 -
\Z
匹配整个字符串的末尾 -
\b
表示单词的边界,匹配一个单词的开始或结束 -
\B
与\b
的含义相反,不能是单词的结束或开始
例如\b
的用法
>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
>>> print(p.search('one subclass is'))
None
RE中的分组
分组由RE中的(
与)
确定,有以下几方面作用
- 分组作为一个整体参与RE表达式匹配
- 分组指定的子字符串可以从匹配结果中获取相关信息
- RE表达式中可以引用前面使用的分组以简化表达式
与数学计算中括号有相似的含义,(
与)
将其中的表达式部分作为一个分组,例如当分组的后续为*
,+
等则整个分组进行重复。例如
>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)
由(
与)
指定的分组代表匹配结果的子字符串,可由group()
获得,序号0指代整个RE的匹配字符串,从序号1开始依次指代各个子字符串,顺序与(
在RE表达式中的顺序一致。
>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'
RE表达式可以引用前面由(
与)
指定的分组,例如\1
表达式中第1个分组的内容
>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'
在RE表达式中,括号(
紧跟?
时,?
没有任何字符用于重复,这本是一种语法错误,但RE由此扩展定义一批特殊含义的表达式
扩展表达式定义 | 含义 |
---|---|
(?:...) |
不需捕获分组信息的分组 |
(?P |
命名的分组,Python语言特性 |
(?=...) |
零宽的超前断言分组 |
(?!...) |
零宽的超前否定断言分组 |
不捕获的分组
在一个复杂的RE表达式中,有时仅需要分组作为整体参与运算,但却不需要在结果中获取该分组信息,这时使用不捕获的分组表示(?:...)
。不捕获的分组不在子字符串中占有序号,因此不影响其它分组。
>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()
python扩展命名分组
Python语言定义了命名的分组,与序号的分组作用一致。还可以使用(?P=name)
指代前面的命名分组,与\1
作用类似。
>>> p = re.compile(r'(?P\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'
>>> p = re.compile(r'\b(?P\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'
超前断言分组
(?=...)
是一个零宽断言,若后续字符串与...
部分匹配则匹配断言成功,否则匹配失败。与普通的匹配不同点在于(?=...)
不消耗字符串内容仅作为尝试查看
(?!...)
与(?=...)
反义,若后续字符串与...
部分不匹配时则匹配断言称,否则匹配失败。
举例,若希望对含有.
分隔的文件名(例如foo.py)匹配,RE表达式为
.*[.].*$
此外,若不想匹配bat
文件,那么不使用超前断言的RE表达式可以写为
.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
若同时不想匹配bat
与exe
文件,那么不使用超前断言的RE表达式将十分复杂,而使用超前断言可以写作
.*[.](?!bat$|exe$)[^.]*$
字符串编辑Modifying Strings
正则表达式可在多方面用于字符串编辑
方法/属性 | 功能 |
---|---|
split() |
根据RE匹配分割字符串 |
sub() |
将所有与RE匹配的子字符串进行替换 |
subn() |
与sub()相似,额外返回替换的子字符串数量 |
分割字符串
.split(string[, maxsplit=0])
参数maxsplit
可以指定最大的分割次数,另外在RE表达式中使用了分组括号,则分组的内容也会出现在结果中
>>> 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', '.', '']
查找与替换
.sub(replacement, string[, count=0])
从字符串最左侧开始依次替换所有不相互重叠的子字符串。若RE模式中有*
将匹配所有空子字符串,此时的每次替换仅进行一次。
>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'
在替换字符串replacement
中,可以引用RE表达式匹配的分组,例如下面的例子中\1
分别指代了原字符串中的'First'
与'second'
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
替换字符串的一个额外的语法是\g<1>
指代RE表达式中的分组1,作用与\1
相同。不过采用后者时可能会出现模糊歧义(例如当替换字符串中出现\10
时,可以表示分组10,也可以表示分组1与字符'0'
)对于Python,可以在替换字符串中使用\g
指代RE表达式中的命名分组。
>>> p = re.compile('section{ (?P [^}]* ) }', 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}','section{First}')
'subsection{First}'
参数replacement
还可以为函数,接受匹配结果的match object
对象,返回字符串
>>> 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.'
公共问题Common Problems
优先考虑字符串的内部方法
有时,仅需要将源字符串中的'foo'
替换为'bar'
,那么字符串的replace()
方法更合适,而不是re.sub()
。不过,如果需要替换的目标包括'foo'
,'Foo'
等大小写不定情况,或者位于其它长单词之间'ffooo'
(将被replace()
替换为'fbaro'
),此时应使用re.sub()
另一种常见的情况是将源字符串中的某些单个字符替换为其它字符,例如re.sub('\n', ' ', S)
,此时字符串的translate()
方法更合适。
match()与search()
有时在re.match()
的RE表达式中添加.*
前缀以实现re.search()
的功能,此时直接使用re.search()
的效率更高。
贪婪与非贪婪
在表重复匹配的元字符后跟?
将采取非贪婪匹配的重复模式,如*?
,+?
,??
及{m,n}?
这些将匹配尽量少次的重复模式。
>>> s = 'Title '
>>> print(re.match('<.*>', s).group())
Title
>>> print(re.match('<.*?>', s).group())
不过,对HTML或者XML使用RE处理是很头疼的,直接使用Python相应的处理模块更好。
本文内容主要翻译自["Regular Expression HOWTO"]: https://docs.python.org/3.7/howto/regex.html#regex-howto 若侵权请联系删除