Python正则表达式

2020-03-05  本文已影响0人  东旭曦影

为学习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]

反斜杠\是正则表达式的转义字符,其后跟随不同字符将有不同含义。例如,\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.Match object; span=(0, 5), match='tempo'>

RE匹配对象具有如下方法

方法/属性 说明
group() 返回RE匹配的字符串
start() 返回RE匹配的字符串起始位置
end() 返回RE匹配的字符串结束位置
span() 返回一个元组包含上述字符串的起始、结束位置
>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> 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.Match object; span=(0, 5), match='From '>

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

这些元字符都是零宽断言,即指定特殊含义但并不消耗任何字符串字符

例如\b的用法

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('one subclass is'))
None

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<name>...) 命名的分组,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<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

超前断言分组

(?=...)是一个零宽断言,若后续字符串与...部分匹配则匹配断言成功,否则匹配失败。与普通的匹配不同点在于(?=...)不消耗字符串内容仅作为尝试查看

(?!...)(?=...)反义,若后续字符串与...部分不匹配时则匹配断言称,否则匹配失败。

举例,若希望对含有.分隔的文件名(例如foo.py)匹配,RE表达式为

.*[.].*$

此外,若不想匹配bat文件,那么不使用超前断言的RE表达式可以写为

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

若同时不想匹配batexe文件,那么不使用超前断言的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<name>指代RE表达式中的命名分组。

>>> 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还可以为函数,接受匹配结果的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 = '<html><head><title>Title</title>'
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>
>>> print(re.match('<.*?>', s).group())
<html>

不过,对HTML或者XML使用RE处理是很头疼的,直接使用Python相应的处理模块更好。

本文内容主要翻译自["Regular Expression HOWTO"]: https://docs.python.org/3.7/howto/regex.html#regex-howto 若侵权请联系删除

上一篇下一篇

猜你喜欢

热点阅读