简介
正则表达式(称为RE,或正则,或正则表达式模式)本质上是嵌入在Python中的一种微小的、高度专业化的编程语言,可通过 re 模块获得。 使用正则,可以为要匹配的可能字符串集指定规则,然后在任何字符串进行匹配。还可以使用正则修改字符串,或以各种方式将字符串拆分。
正则表达式模式被编译成一系列字节码,然后由用 C 编写的匹配引擎执行。正则表达式语言相对较小且受限制,因此并非所有可能的字符串处理任务都可以使用正则表达式完成。
简单模式
先来看看最简单的正则表达式,正则表达式最常用的任务就是匹配字符。
匹配字符
大多数字母和字符只会匹配自己。 例如,正则表达式test
将完全匹配字符串test
。
一些字符是特殊的 元字符(metacharacters ),并且不匹配自己。 相反,它们表示应该匹配一些与众不同的东西,或者通过重复它们或改变它们的含义来影响正则的其他部分。
元字符只有14个,完整列表如下:
. ^ $ * + ? { } [ ] \ | ( )
先来看看[
和]
。 它们用于指定字符类,它是你希望匹配的一组字符。 可以单独列出字符,也可以通过给出两个字符并用 ‘-’ 标记将它们分开来表示一系列字符。 例如,[abc]
将匹配任何字符 a、 b 或 c ;这与[a-c]
相同,它使用一个范围来表示同一组字符。 如果你只想匹配小写字母,可以使用[a-z]
。
字符类中的元字符不生效。 例如,[akm$]
将匹配 ‘a’ , ‘k’ 、 ‘m’ 或 ‘$’ 中的任意字符; ‘$’ 通常是一个元字符,但在一个字符类中它被剥夺了特殊性。
当'^'
作为该类的第一个字符时表示字符类中未出现的字符(类似取补集)。 例如,[^5]
将匹配除 ‘5’ 之外的任何字符。但是!如果'^'
出现在字符类的其他位置,则它没有特殊含义。 例如:[5^]
将匹配 ‘5’ 或 ‘^’。
.
可以匹配除换行符之外的任何内容,并且有一个可选模式(re.DOTALL
)甚至可以匹配换行符。
|
或者“or”运算符。 如果 A 和 B 是正则表达式,A|B 将匹配任何与 A 或 B 匹配的字符串。 | 具有非常低的优先级,以便在交替使用多字符字符串时使其合理地工作。Crow|Servo
将匹配'Crow'
或'Servo'
,而不是 ‘Cro’、‘w’ 或 ‘S’ 和 ‘ervo’。
$
匹配行的末尾,定义为字符串的结尾,或者后跟换行符的任何位置。
\
可以转义所有元字符,比如使用\[
来匹配[
,使用\\
来匹配\
。有一些以\
开头的特殊序列表示预定义的字符集:
\number
匹配数字代表的组合。每个括号是一个组合,组合从1开始编号。比如 (.+) \1 匹配 ‘the the’ 或者 ‘55 55’, 但不会匹配 ‘thethe’ (注意组合后面的空格)。这个特殊序列只能用于匹配前面99个组合。如果 number 的第一个数位是0, 或者 number 是三个八进制数,它将不会被看作是一个组合,而是八进制的数字值。在 ‘[’ 和 ‘]’ 字符集合内,任何数字转义都被看作是字符。
\A
只匹配字符串开始。
\b
匹配空字符串,但只在单词开始或结尾的位置。一个单词被定义为一个单词字符的序列。注意,通常\b
定义为\w
和\W
字符之间,或者\w
和字符串开始/结尾的边界, 意思就是r'\bfoo\b'
匹配'foo', 'foo.', '(foo)', 'bar foo baz'
但不匹配'foobar'
或者'foo3'
。
默认情况下,Unicode字母和数字是在Unicode样式中使用的,但是可以用 ASCII 标记来更改。如果 LOCALE 标记被设置的话,词的边界是由当前语言区域设置决定的,\b
表示退格字符,以便与Python字符串文本兼容。
\B
匹配空字符串,但不能在词的开头或者结尾。意思就是r'py\B'
匹配'python', 'py3', 'py2'
, 但不匹配'py', 'py.'
, 或者'py!'
.\B
是\b
的取非。
\d
匹配任何十进制数,等价于[0-9]
。
\D
匹配任何非十进制数字的字符。就是\d
取非,等价于[^0-9]
。
\s
匹配任何空白字符,等价于[ \t\n\r\f\v]
。
\S
匹配任何非空白字符。就是\s
取非,等价于[^ \t\n\r\f\v]
。
\w
匹配任何数字和字母和下划线,等价于[a-zA-Z0-9_]
。
\W
匹配非单词字符的字符。这与\w
正相反,等价于[^a-zA-Z0-9_]
。
\Z
只匹配字符串尾。
重复
*
可以指定它前一个字符可以匹配任意次,比如wo*
可以匹配w
(0个o
),wo
(1个o
),woo
(2个o
),wooo
(3个o
)等。*
的重复是贪婪的,匹配引擎会尽可能多的重复它。
+
则是指定它前一个字符可以匹配一次或多次,与*
的区别是无法匹配零次,比如wo+
可以匹配wo
(1个o
),woo
(2个o
),wooo
(3个o
)等,但不能匹配w
(0个o
),。
?
则是匹配零次或一次,比如wo?
匹配w
(0个o
),wo
(1个o
)。
{m,n}
表示至少重复m次,最多重复n次,比如wo{1,3}
可以匹配wo
(1个o
),woo
(2个o
),wooo
(3个o
)。m和n可以省略,省略m表示下限为0,省略n表示上限为无穷大。
使用正则表达式
python标准库中的re
模块提供了正则表达式引擎的接口,允许你将正则编译为对象,然后用它们进行匹配。
编译正则表达式
正则表达式被编译成模式对象,模式对象具有各种操作的方法,例如搜索模式匹配或执行字符串替换。
>>> import re
>>> p = pile(‘ab*’)
>>> p
pile(‘ab*’)
反斜杠灾难
正则表达式使用反斜杠字符\
来表示特殊形式或转义特殊字符。 这与 Python 中\
的使用相冲突。
假设要编写一个与字符串\section
相匹配的正则,必须传递给pile()
的结果字符串必须是\\section
,但是,要将其表示为 Python 字符串文字,必须再次转义两个反斜杠,最终你写出来的字符串变成"\\\\section"
。
简而言之,要匹配文字反斜杠\
,正则字符串为'\\\\'
。 在反复使用反斜杠的正则中,这会导致大量重复的反斜杠,并使得生成的字符串难以理解。
解决方案是使用 Python 的原始字符串表示法来表示正则表达式;反斜杠不以任何特殊的方式处理前缀为'r'
的字符串字面,因此r"\n"
是一个包含'\'
和'n'
的双字符字符串,而"\n"
是一个包含换行符的单字符字符串。"\\\\section"
可以写为r"\\section"
。
进行匹配
现在我们得到了一个编译正则表达式的对象,该对象有几个方法和属性,最常用的有以下几个:
方法 / 属性
match()
确定正则是否从字符串的开头匹配。
search()
扫描字符串,查找此正则匹配的任何位置。
findall()
找到正则匹配的所有子字符串,并将它们作为列表返回。
finditer()
找到正则匹配的所有子字符串,并将它们返回为一个迭代器(iterator)。
如果没有找到匹配,match()
和search()
返回None
。如果它们成功, 一个匹配对象实例将被返回,包含匹配相关的信息:起始和终结位置、匹配的子串以及其它。
python附带了一个小工具用来测试正则表达式,位于Tools/demo/redemo.py,源代码如下:
#!/usr/bin/env python3"""Basic regular expression demonstration facility (Perl style syntax)."""from tkinter import *import reclass ReDemo:def __init__(self, master):self.master = masterself.promptdisplay = Label(self.master, anchor=W,text="Enter a Perl-style regular expression:")self.promptdisplay.pack(side=TOP, fill=X)self.regexdisplay = Entry(self.master)self.regexdisplay.pack(fill=X)self.regexdisplay.focus_set()self.addoptions()self.statusdisplay = Label(self.master, text="", anchor=W)self.statusdisplay.pack(side=TOP, fill=X)self.labeldisplay = Label(self.master, anchor=W,text="Enter a string to search:")self.labeldisplay.pack(fill=X)self.labeldisplay.pack(fill=X)self.showframe = Frame(master)self.showframe.pack(fill=X, anchor=W)self.showvar = StringVar(master)self.showvar.set("first")self.showfirstradio = Radiobutton(self.showframe,text="Highlight first match",variable=self.showvar,value="first",command=self.recompile)self.showfirstradio.pack(side=LEFT)self.showallradio = Radiobutton(self.showframe,text="Highlight all matches",variable=self.showvar,value="all",command=self.recompile)self.showallradio.pack(side=LEFT)self.stringdisplay = Text(self.master, width=60, height=4)self.stringdisplay.pack(fill=BOTH, expand=1)self.stringdisplay.tag_configure("hit", background="yellow")self.grouplabel = Label(self.master, text="Groups:", anchor=W)self.grouplabel.pack(fill=X)self.grouplist = Listbox(self.master)self.grouplist.pack(expand=1, fill=BOTH)self.regexdisplay.bind('<Key>', self.recompile)self.stringdisplay.bind('<Key>', self.reevaluate)piled = Noneself.recompile()btags = self.regexdisplay.bindtags()self.regexdisplay.bindtags(btags[1:] + btags[:1])btags = self.stringdisplay.bindtags()self.stringdisplay.bindtags(btags[1:] + btags[:1])def addoptions(self):self.frames = []self.boxes = []self.vars = []for name in ('IGNORECASE','MULTILINE','DOTALL','VERBOSE'):if len(self.boxes) % 3 == 0:frame = Frame(self.master)frame.pack(fill=X)self.frames.append(frame)val = getattr(re, name).valuevar = IntVar()box = Checkbutton(frame,variable=var, text=name,offvalue=0, onvalue=val,command=self.recompile)box.pack(side=LEFT)self.boxes.append(box)self.vars.append(var)def getflags(self):flags = 0for var in self.vars:flags = flags | var.get()flags = flagsreturn flagsdef recompile(self, event=None):try:piled = pile(self.regexdisplay.get(),self.getflags())bg = self.promptdisplay['background']self.statusdisplay.config(text="", background=bg)except re.error as msg:piled = Noneself.statusdisplay.config(text="re.error: %s" % str(msg),background="red")self.reevaluate()def reevaluate(self, event=None):try:self.stringdisplay.tag_remove("hit", "1.0", END)except TclError:passtry:self.stringdisplay.tag_remove("hit0", "1.0", END)except TclError:passself.grouplist.delete(0, END)if not piled:returnself.stringdisplay.tag_configure("hit", background="yellow")self.stringdisplay.tag_configure("hit0", background="orange")text = self.stringdisplay.get("1.0", END)last = 0nmatches = 0while last <= len(text):m = piled.search(text, last)if m is None:breakfirst, last = m.span()if last == first:last = first+1tag = "hit0"else:tag = "hit"pfirst = "1.0 + %d chars" % firstplast = "1.0 + %d chars" % lastself.stringdisplay.tag_add(tag, pfirst, plast)if nmatches == 0:self.stringdisplay.yview_pickplace(pfirst)groups = list(m.groups())groups.insert(0, m.group())for i in range(len(groups)):g = "%2d: %r" % (i, groups[i])self.grouplist.insert(END, g)nmatches = nmatches + 1if self.showvar.get() == "first":breakif nmatches == 0:self.statusdisplay.config(text="(no match)",background="yellow")else:self.statusdisplay.config(text="")# Main function, run when invoked as a stand-alone Python program.def main():root = Tk()demo = ReDemo(root)root.protocol('WM_DELETE_WINDOW', root.quit)root.mainloop()if __name__ == '__main__':main()
运行代码后会出现一个界面,在其中输入正则表达式和需要匹配的字符串,可以实时看到匹配结果。
使用这个小工具可以很容易确定你写的正则表达式是否正确的匹配。另一种方式是使用标准python解释器来测试:
>>> import re
>>> p = pile(’[a-z]+’)
>>> p
pile(’[a-z]+’)
我们得到了一个编译的正则表达式对象,接下来尝试一下它应该匹配的字符串,例如 tempo。在这个例子中 match() 将返回一个 匹配对象,因此你应该将结果储存到一个变量中以供稍后使用。
>>> m = p.match(‘tempo’)
>>> m
<re.Match object; span=(0, 5), match=‘tempo’>
匹配对象实例也有几个方法和属性,最重要的是以下几个:
group()
返回正则匹配的字符串
start()
返回匹配的开始位置
end()
返回匹配的结束位置
span()
返回包含匹配 (start, end) 位置的元组
模块级别函数
一般人可能只是偶尔使用正则表达式,大可不必创建模式对象并调用其方法,re模块中提供了顶级函数match()
,search()
,findall()
,sub()
等等。 这些函数采用与相应模式方法相同的参数,并将正则字符串作为第一个参数添加,并仍然返回None或匹配对象实例。
>>> 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 模块中有两个名称,长名称如IGNORECASE
和一个简短的单字母形式,例如 I。 (如果你熟悉 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
启用详细的正则,可以更清晰,更容易理解。此标志允许你编写更易读的正则表达式,方法是为您提供更灵活的格式化方式。 指定此标志后,将忽略正则字符串中的空格,除非空格位于字符类中或前面带有未转义的反斜杠;这使你可以更清楚地组织和缩进正则。 此标志还允许你将注释放在正则中,引擎将忽略该注释;注释标记为 ‘#’ 既不是在字符类中,也不是在未转义的反斜杠之前。
charref = pile(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)
分组
通常,你需要获取更多信息,而不仅仅是正则是否匹配。 正则表达式通常用于通过将正则分成几个子组来解析字符串,这些子组匹配不同的感兴趣组件。
组由'('
,')'
元字符标记。'('
和')'
与数学表达式的含义大致相同;它们将包含在其中的表达式组合在一起,你可以使用重复限定符重复组的内容,例如*
,+
,?
或{m,n}
。
用'('
,')'
表示的组也捕获它们匹配的文本的起始和结束索引;这可以通过将参数传递给group()
、start()
、end()
以及span()
。 组从 0 开始编号。组 0 始终存在;它表示整个正则,所以 匹配对象 方法都将组 0 作为默认参数。
>>> p = pile(’(a)b’)
>>> m = p.match(‘ab’)
>>> m.group()
‘ab’
>>> m.group(0)
‘ab’
子组从左到右编号,从 1 向上编号。 组可以嵌套;要确定编号,只需计算从左到右的左括号字符。:
>>> p = pile(’(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() 方法返回一个元组,其中包含所有子组的字符串,从1到最后一个子组。:
>>> m.groups()
(‘abc’, ‘b’)
模式中的后向引用允许你指定还必须在字符串中的当前位置找到先前捕获组的内容。 例如,如果可以在当前位置找到组 1 的确切内容,则\1
将成功,否则将失败。 请记住,Python 的字符串文字也使用反斜杠后跟数字以允许在字符串中包含任意字符,因此正则中引入反向引用时务必使用原始字符串。
例如,以下正则检测字符串中的双字。
>>> p = pile(r’\b(\w+)\s+\1\b’)
>>> p.search(‘Paris in the the spring’).group()
‘the the’
非捕获和命名组
非捕获组
有时你会想要使用组来表示正则表达式的一部分,但是对检索组的内容不感兴趣。 你可以通过使用非捕获组来显式表达这个事实:(?:...)
,你可以用任何其他正则表达式替换...
。
>>> m = re.match("([abc])+", “abc”)
>>> m.groups()
(‘c’,)
>>> m = re.match("(?:[abc])+", “abc”)
>>> m.groups()
()
除了你无法检索组匹配内容的事实外,非捕获组的行为与捕获组完全相同;你可以在里面放任何东西,用重复元字符重复它,比如 *,然后把它嵌入其他组(捕获或不捕获)。(?:...)
在修改现有模式时特别有用,因为你可以添加新组而不更改所有其他组的编号方式。 值得一提的是,捕获和非捕获组之间的搜索没有性能差异;两种形式没有一种更快。
命名组
命名组的语法是Python特定的扩展之一:(?P<name>...)
。 name 显然是该组的名称。 命名组的行为与捕获组完全相同,并且还将名称与组关联。 处理捕获组的 匹配对象 方法都接受按编号引用组的整数或包含所需组名的字符串。 命名组仍然是给定的数字,因此你可以通过两种方式检索有关组的信息:
>>> p = pile(r’(?P<word>\b\w+\b)’)
>>> m = p.search( ‘(((( Lots of punctuation )))’ )
>>> m.group(‘word’)
‘Lots’
>>> m.group(1)
‘Lots’
此外,你可以通过 groupdict() 将命名分组提取为一个字典:
>>> m = re.match(r’(?P<first>\w+) (?P<last>\w+)’, ‘Jane Doe’)
>>> m.groupdict()
{‘first’: ‘Jane’, ‘last’: ‘Doe’}
命名组很有用,因为它们允许你使用容易记住的名称,而不必记住数字。 这是来自 imaplib 模块的示例正则
InternalDate = pile(r'INTERNALDATE "'r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'r'(?P<year>[0-9][0-9][0-9][0-9])'r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'r'"')
检索m.group('zonem')
显然要容易得多,而不必记住检索第 9 组。
表达式中的后向引用语法,例如(...)\1
,指的是组的编号。 当然有一种变体使用组名而不是数字。 这是另一个 Python 扩展:(?P=name)
表示在当前点再次匹配名为 name 的组的内容。 用于查找双字的正则表达式,\b(\w+)\s+\1\b
也可以写为\b(?P<word>\w+)\s+(?P=word)\b
:
>>> p = pile(r’\b(?P<word>\w+)\s+(?P=word)\b’)
>>> p.search(‘Paris in the the spring’).group()
‘the the’
修改字符串
正则表达式通常也用于以各种方式修改字符串,使用以下模式方法:
split()
将字符串拆分为一个列表,在正则匹配的任何地方将其拆分
>>> p = pile(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 = pile(r’\W+’)
>>> p2 = pile(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.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.’]
sub()
找到正则匹配的所有子字符串,并用不同的字符串替换它们
>>> p = pile(’(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 = pile(’(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<name>...)
语法定义的命名组。\g<name>
将使用名为 name 的组匹配的子字符串,\g<number>
使用相应的组号。 因此\g<2>
等同于\2
,但在诸如\g<2>0
之类的替换字符串中并不模糊。以下替换都是等效的,但使用所有三种变体替换字符串。
>>> p = pile(‘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}’
对于复杂的匹配规则,还可以写成函数:
>>> def hexrepl(match):
… “将小数转换为16进制”
… value = int(match.group())
… return hex(value)
…
>>> p = pile(r’\d+’)
>>> p.sub(hexrepl, ‘Call 65490 for printing, 49152 for user code.’)
‘Call 0xffd2 for printing, 0xc000 for user code.’
注意正则表达式的贪婪匹配
假设需要匹配'<html><head><title>Title</title>'
中的<html>
。
s = '<html><head><title>Title</title>'print(re.match('<.*>', s).group())
你会得到这样的结果:<html><head><title>Title</title>
。正则匹配'<'
中的'<html>'
和.*
消耗字符串的其余部分。 正则中还有更多的剩余东西,并且>
在字符串的末尾不能匹配,所以正则表达式引擎必须逐个字符地回溯,直到它找到匹配>
。最终匹配从'<html>'
中的'<'
扩展到'</title>'
中的'>'
,而这并不是你想要的结果。
在这种情况下,解决方案是使用非贪婪的限定符*?
、+?
、??
或{m,n}?
,匹配为尽可能少的文字。 在上面的例子中,在第一次'<'
匹配后立即尝试'>'
,当它失败时,引擎一次前进一个字符,每一步都重试'>'
。 正确的结果是:
s = '<html><head><title>Title</title>'print(re.match('<.*?>', s).group())
现在可以得到正确的结果:<html>