就其本质而言,正则表达式(或 RE)是一种小型的、高度专业化的编程语言,(在Python中)它内嵌在 Python 中,并通过 re 模块实现。
使用这个小型语言,你可以为想要匹配的相应字符串集指定规则;该字符串集可能包含英文语句、e-mail地址、TeX命令或任何你想搞定的东西。
然后你可以问诸如“这个字符串匹配该模式吗?”或“在这个字符串中是否有部分匹配该模式呢?”。
你也可以使用 RE 以各种方式来修改或分割字符串。
. ^ $ * + ? { [ ] \ | ( )
[akm$]
将匹配字符"a", "k", "m", 或 "$
" 中的任意一个;"$
"通常用作元字符,但在字符类别里,其特性被除去,恢复成普通字符。[^5]
将匹配除 "5" 之外的任意字符。\[
或 \\
。\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_]
。.
。它匹配除了换行字符外的任何字符,在 alternate 模式(re.DOTALL
)下它甚至可以匹配换行。".
" 通常被用于你想匹配“任何字符”的地方。*
匹配零或更多次,所以可以根本就不出现+
则要求至少出现一次?
匹配一次或零次,可以认为它用于标识某事物是可选的{m,n}
(注意m,n之间不能有空格),其中 m 和 n 是十进制整数。该限定符的意思是至少有 m 个重复,至多到 n 个重复考虑表达式 a[bcd]*b
。它匹配字母 "a",零个或更多个来自类 [bcd]
中的字母,最后以 "b" 结尾。现在想一想该 RE 对字符串 "abcbd" 的匹配。
匹配引擎一开始会尽其所能进行匹配(贪婪匹配),如果没有匹配然后就逐步退回并反复尝试 RE 剩下来的部分。直到它退回尝试匹配 [bcd] 到零次为止,如果随后还是失败,那么引擎就会认为该字符串根本无法匹配 RE 。
Step | Matched | Explanation | |
---|---|---|---|
1 | a | a 匹配模式 | |
2 | abcbd | 引擎匹配 [bcd]* ,并尽其所能匹配到字符串的结尾 |
|
3 | Failure | 引擎尝试匹配 b,但当前位置已经是字符的最后了,所以失败 | |
4 | abcb | 退回,[bcd]* 尝试少匹配一个字符。 |
|
5 | Failure | 再次尝次 b,但在当前最后一位字符是 "d"。 | |
6 | abc | 再次退回,[bcd]* 只匹配 "bc"。 |
|
7 | abcb | 再次尝试 b ,这次当前位上的字符正好是 "b" |
import re
p = re.compile('ab*')
p = re.compile('ab*', re.IGNORECASE)
print (p)
re.compile('ab*', re.IGNORECASE)
为了匹配一个反斜杠,不得不在 RE 字符串中写 '\\\\
',因为正则表达式中必须是 "\\
",而每个反斜杠在常规的 Python 字符串实值中必须表示成 "\\
"。在 REs 中反斜杠的这个重复特性会导致大量重复的反斜杠,而且所生成的字符串也很难懂。
解决的办法就是为正则表达式使用 Python 的 raw 字符串表示;在字符串前加个 "r" 反斜杠就不会被任何特殊方式处理,所以 r"\n"
就是包含 "\
" 和 "n" 的两个字符,而 "\n
" 则是一个字符,表示一个换行。正则表达式通常在 Python 代码中都是用这种 raw 字符串表示。
常规字符串 | Raw 字符串 |
---|---|
"ab* " |
r"ab* " |
"\\\\section " |
r"\\section " |
"\\w+\\s+\\1 " |
r"\w+\s+\1 " |
方法/属性 | 作用 |
---|---|
match() | 决定 RE 是否在字符串刚开始的位置匹配 |
search() | 扫描字符串,找到这个 RE 匹配的位置 |
findall() | 找到 RE 匹配的所有子串,并把它们作为一个列表返回 |
finditer() | 找到 RE 匹配的所有子串,并把它们作为一个迭代器返回 |
如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 MatchObject
实例,其中有这次匹配的信息:它是从哪里开始和结束,它所匹配的子串等等。
findall() 在它返回结果时不得不创建一个列表。在 Python 2.2中,也可以用 finditer() 方法。
MatchObject 实例也有几个方法和属性;最重要的那些如下所示:
方法/属性 | 作用 |
---|---|
group() | 返回被 RE 匹配的字符串 |
start() | 返回匹配开始的位置 |
end() | 返回匹配结束的位置 |
span() | 返回一个元组包含匹配 (开始,结束) 的位置 |
在实际程序中,最常见的作法是将 MatchObject
保存在一个变量里,然後检查它是否为 None,通常如下所示:
p = re.compile('[a-z]+')
m = p.match( 'tempo')
print(m)
if m:
print ('Match found: ', m.group())
else:
print ('No match')
<_sre.SRE_Match object; span=(0, 5), match='tempo'> Match found: tempo
m.group()
'tempo'
m.start(), m.end()
(0, 5)
m.span()
(0, 5)
iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
for match in iterator:
print(match.group())
print(match.span())
drummers (3, 11) drumming (12, 20)
编译标志让你可以修改正则表达式的一些运行方式。在 re 模块中标志可以使用两个名字,一个是全名如 IGNORECASE,一个是缩写,一字母形式如 I。(如果你熟悉 Perl 的模式修改,一字母形式使用同样的字母;例如 re.VERBOSE的缩写形式是 re.X。)
多个标志可以通过按位 OR-ing 它们来指定。如 re.I | re.M
被设置成 I 和 M 标志:
标志 | 含义 |
---|---|
DOTALL, S | 使 . 匹配包括换行在内的所有字符 |
IGNORECASE, I | 使匹配对大小写不敏感 |
LOCALE, L | 做本地化识别(locale-aware)匹配 |
MULTILINE, M | 多行匹配,影响 ^ 和 $ ,^ 和 $ 不会被解释 |
VERBOSE, X | 能够使用 REs 的 verbose 状态,使之被组织得更清晰易懂 |
剩下来要讨论的一部分元字符是零宽界定符(zero-width assertions)。它们并不会使引擎在处理字符串时更快;相反,它们根本就没有对应任何字符,只是简单的成功或失败。举个例子,\b 是一个在单词边界定位当前位置的界定符(assertions),这个位置根本就不会被 \b 改变。这意味着零宽界定符(zero-width assertions)将永远不会被重复,因为如果它们在给定位置匹配一次,那么它们很明显可以被匹配无数次。
|
\|
,或将其包含在字符类中,如 [|]
。^
$
$
",使用 \$
或将其包含在字符类中,如 [$]
。\A
\A
和 ^
实际上是一样的。\A
只是匹配字符串首,而 ^
还可以匹配在换行符之后字符串的任何位置。\Z
\b
\B
p = re.compile(r'\bclass\b')
print (p.search('no class at all'))
print (p.search('the declassified algorithm'))
print (p.search('one subclass is'))
<_sre.SRE_Match object; span=(3, 8), match='class'> None None
当用这个特殊序列时你应该记住这里有两个微妙之处。第一个是 Python 字符串和正则表达式之间最糟的冲突。在 Python 字符串里,"\b" 是反斜杠字符,ASCII值是8。如果你没有使用 raw 字符串时,那么 Python 将会把 "\b" 转换成一个回退符,你的 RE 将无法象你希望的那样匹配它了。下面的例子看起来和我们前面的 RE 一样,但在 RE 字符串前少了一个 "r" 。
p = re.compile('\bclass\b')
print (p.search('no class at all'))
print (p.search('\b' + 'class' + '\b'))
None <_sre.SRE_Match object; span=(0, 7), match='\x08class\x08'>
第二个在字符类中,这个限定符(assertion)不起作用,\b 表示回退符,以便与 Python 字符串兼容。
*, +, ?, 和 {m,n}
,来重复组里的内容,比如说 (ab)*
将匹配零或更多个重复的 "ab"。The groups()
方法返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。p = re.compile('(ab)*')
print (p.match('ababababab').span())
(0, 10)
p = re.compile('(a)b')
m = p.match('ab')
print(m.group())
print(m.group(0))
ab ab
p = re.compile('(a(b)c)d')
m = p.match('abcd')
print(m.group(0))
print(m.group(1))
print(m.group(2))
print(m.group(2,1))
abcd abc b ('b', 'abc')
m.groups()
('abc', 'b')
p = re.compile(r'(\b\w+)\s+\1')
p.findall('Paris in the the the spring')
#p.search('Paris in the the the spring').group()
['the']
(?P<name>...)
定义一个命名组(?P=name)
则是对命名组的逆向引用(?:...)
来实现这项功能,这样你可以在括号中发送任何其他正则表达式。*
" 来重复它,可以在其他组(无捕获组与捕获组)中嵌套它。(?:...)
对于修改已有组尤其有用,因为你可以不用改变所有其他组号的情况下添加一个新组。(?P<name>...)
。名字很明显是组的名字。MatchObject
的方法处理捕获组时接受的要么是表示组号的整数,要么是包含组名的字符串。命名组也可以是数字,所以你可以通过两种方式来得到一个组的信息。(...)\1
这样的表达式所表示的是组号,这时用组名代替组号自然会有差别。(?P=name)
,它可以使叫 name 的组内容再次在当前位置发现。(\b\w+)\s+\1
也可以被写成 (?P<word>\b\w+)\s+(?P=word)
m = re.match("([abc])+", "abc")
print(m.groups())
('c',)
m = re.match("(?:[abc])+", "abc")
print(m.groups())
('c',) ()
p = re.compile(r'(?P<word>\b\w+\b)')
m = p.search( '(((( Lots of punctuation )))' )
print(m.groups('word'))
print(m.group(1))
('Lots',) Lots
p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
p.search('Paris in the the spring').group()
'the the'
另一个零宽界定符(zero-width assertion)是前向界定符。前向界定符包括前向肯定界定符和前项否定界定符,如下所示:
(?=...)
(?!...)
前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功考虑一个简单的模式用于匹配一个文件名,并将其通过 "." 分成基本名和扩展名两部分。如在 "news.rc" 中,"news" 是基本名,"rc" 是文件的扩展名。
.*[.].*$
.
" 需要特殊对待,因为它是一个元字符;我把它放在一个字符类中。另外注意后面的 $
;现在,考虑把问题变得复杂点;如果你想匹配的扩展名不是 "bat" 的文件名?
.*[.](?!bat$).*$
bat$
匹配,整个模式将失败。$
被要求是为了确保象 "sample.batch" 这样扩展名以 "bat" 开头的会被允许。.*[.](?!bat$|exe$).*$
import re
twitter = re.compile(
'''(?<=@)([\w\d_]+)''',re.UNICODE | re.VERBOSE)
text = '''This text includes two Twitter handles.
One for @ThePSF, and one for the author, @doughellmann.
'''
print (text)
for match in twitter.findall(text):
print ('Handle:', match)
This text includes two Twitter handles. One for @ThePSF, and one for the author, @doughellmann. Handle: ThePSF Handle: doughellmann
# 前向
# Isaac 前面有 Asimov 才能匹配
test = re.compile(r'Isaac(?=Asimov)')
test.findall("IsaacAsimov")
#test.findall("AsimovIsaac")
['Isaac']
# 前向
# Isaac 前面有 Asimov 不能匹配
test = re.compile(r'Isaac(?!Asimov)')
test.findall("IsaacAsimov")
# test.findall("AsimovIsaac")
[]
# 后向
# Isaac 后面有 Asimov 才能匹配
test = re.compile(r'(?<=Asimov)Isaac')
test.findall("AsimovIsaac")
#test.findall("IsaacAsimov")
['Isaac']
# 后向
# Isaac 后面有 Asimov 不能匹配
test = re.compile(r'(?<!Asimov)Isaac')
test.findall("AsimovIsaac")
#test.findall("IsaacAsimov")
[]
# 后向
m = re.search('(?<=abc)def', 'abcdef')
m.group(0)
'def'
# 后向
m = re.search('(?<=-)\w+', 'spam-egg')
m.group(0)
'egg'
方法/属性 | 作用 |
---|---|
split() |
将字符串在 RE 匹配的地方分片并生成一个列表, |
sub() |
找到 RE 匹配的所有子串,并将其用一个不同的字符串替换 |
subn() |
与 sub() 相同,但返回新的字符串和替换次数 |
re.compile().split(text, num)
: num 非 0 时,最多分成 num 段p2 = re.compile(r'(\W+)')
: 打印出定界符,如果不需要则为:p = re.compile(r'\W+')
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', '']
# 最多分出 3 段(0 开始数)
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('[\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(replacement, string[, count = 0])
subn()
方法作用一样,但返回的是包含新字符串和替换执行次数的两元组。(?P<name>...)
语法定义的命名组。"\g<name>
" 将通过组名 "name" 用子串来匹配,并且 "\g<number>
" 使用相应的组号。所以 "\g<2>
" 等于 "\2
",但能在替换字符串里含义不清,如 "\g<2>0
"。("\20
" 被解释成对组 20 的引用,而不是对后面跟着一个字母 "0" 的组 2 的引用。)MatchObject
的对象作为参数,因此可以用这个对象去计算出替换字符串并返回它。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'
p = re.compile('x*')
p.sub('-', 'abxd')
'-a-b-d-'
p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
p = re.compile('section{(\w+)}')
p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'
p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
p = re.compile('section{ (?P<name> \w+) }', 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 ):
... "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.'
用一个固定字符串替换另一个 的例子,如:你可以把 "deed" 替换成 "word"。re.sub() 似乎正是胜任这个工作的函数,但还是考虑考虑 replace() 方法吧。注意 replace() 也可以在单词里面进行替换,可以把 "swordfish" 变成 "sdeedfish"。
另一个常见任务是从一个字符串中删除单个字符或用另一个字符来替代它。你也许可以用 re.sub('\n',' ', s) 这样来实现,但 translate() 能够实现这两个任务,而且比任何正则表达式操作起来更快。 (translate 需要配合 string.maketrans 使用。例如:import string 后 'a1b3'.translate(string.maketrans('ab', 'cd')) )
match() vs search()
¶match() 函数只检查 RE 是否在字符串开始处匹配,而 search() 则是扫描整个字符串。
不贪婪的限定符 *?、+?、?? 或 {m,n}?
,尽可能匹配小的文本。
re.VERBOSE
¶大多数字母和字符一般都会和自身匹配
元字符在类别里([]
里)并不起作用
可以用补集来匹配不在区间范围内的字符。其做法是把"^"作为类别的首个字符;其它地方的"^"只会简单匹配 "^"字符本身
在字符串前加个 "r" 反斜杠就不会被任何特殊方式处理
如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 MatchObject
实例,其中有这次匹配的信息:它是从哪里开始和结束,它所匹配的子串等等。findall() 在它返回结果时不得不创建一个列表。在 Python 2.2中,也可以用 finditer() 方法。
多个标志可以通过按位 OR-ing 它们来指定。如 re.I | re.M
被设置成 I 和 M 标志
组可以被嵌套。计数的数值可以通过从左到右计算打开的括号数来确定。The groups()
方法返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
模式中的逆向引用允许你指定先前捕获组的内容,该组也必须在字符串当前位置被找到。举个例子,如果组 1 的内容能够在当前位置找到的话,\1 就成功否则失败。
无捕获组和命名组
(?P<name>...)
定义一个命名组,(?P=name)
则是对命名组的逆向引用;除了用数字指定组,它可以用名字来指定,如:(\b\w+)\s+\1
也可以被写成 (?P<word>\b\w+)\s+(?P=word)
(?:...)
,对于修改已有组尤其有用,因为你可以不用改变所有其他组号的情况下添加一个新组。前向界定符
(?=...)
前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。(?!...)
前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功,匹配不是 bat 或 exe 后缀的:.*[.](?!bat$|exe$).*$
split(string [, maxsplit = 0])
re.compile().split(text, num)
: num 非 0 时,最多分成 num 段p2 = re.compile(r'(\W+)')
: 打印出定界符,如果不需要则为:p = re.compile(r'\W+')
sub(replacement, string[, count = 0])
(?P<name>...)
语法定义的命名组。"\g<name>
" 将通过组名 "name" 用子串来匹配,并且 "\g<number>
" 使用相应的组号。更快的替换:replace
从一个字符串中删除单个字符或用另一个字符来替代它
re.sub('\n',' ', s)
这样来实现,但 translate()
能够实现这两个任务,而且比任何正则表达式操作起来更快。translate
需要配合string.maketrans
使用。例如:import string
后 'a1b3'.translate(string.maketrans('ab', 'cd'))