正则表达式(简称RE)本质上可以看作一个小的、高度专业化的编程语言,在Python中可以通过re模块使用它。使用正则表达式,你需要为想要匹配的字符串集合指定一套规则,字符串集合可以包含英文句子、e-mail地址、TeX命令或者其它任何你希望的字符串。然后您能提这样的问题:“这个字符串匹配这个模式吗?”,或者“在这个字符串中存在这个模式的匹配吗?”。你也能使用正则表达式修改一个字符串或者分离它。
正则表达式被编译到一系列的字节码,然后被C语言实现的匹配引擎执行。在一些高级应用场景,必须关注引擎怎么执行一个RE,以根据引擎的特征编写RE提高字节码的处理效率。这篇文章不包含优化,优化需要对匹配引擎的内部实现有好的理解。
正则表达式相对小并且存在限制,所以不是所有的字符串处理任务都能用正则表达式解决。也存在有些任务可以用正则表达式做,但表达式非常复杂。在这些情况下,更好的选择是使用Python代码处理,但Python代码相对正则表达式会更慢,但却可能更好理解。
正则表达式简述
我们将从最简单的正则表达式开始,由于正则表达式被用于字符串的操作,我们将从最常用的任务开始:匹配字符。
匹配字符串
大部分字母和字符将匹配他们自身,例如,正则表达式test将匹配字符串test(你可以开始大小写不敏感模式,这样RE可以匹配Test或者TEST)。
这个规则存在例外,一些字符是特殊元字符,不匹配他们自身。他们暗示一些不寻常的事将被匹配,或者他们影响RE的其它部分,例如重复他们或者改变他们的含义。文章的其余部分都主要讨论各种元字符和他们的含义。
下面是元字符的列表,后面将介绍他们的含义:
. ^ $ * + ? { } [ ] \ | ( )
首先我们看[和],他们被用于指定一个字符类,表示你希望匹配的一套字符集。字符能被单独列出,或者使用‘-‘来指示字符的范围,例如:[abc]将匹配字符a、b或c的任意一个;[a-c]也是匹配a、b或c中的任意一个。如果你想匹配仅小写字母,那么RE应该为[a-z]。
在类中([ ]内)的元字符不是激活的,例如:[akm$]将匹配‘a‘、‘k‘、‘m‘或‘$‘中的任意一个,‘$‘是一个元字符,但是在字符类中它作为普通字符使用。
你也能排除类中列出的字符集,通过将‘^‘作为类的第一个字符,注意在类之外的‘^‘将仅仅匹配‘^‘字符,例如:[^5]将匹配除了5之外的任何字符。
或许最重要的元字符是反斜杠\。在Python中,反斜杠能被各种字符跟随作为各种特殊序列使用。它也能被用于取出元字符的特殊性将其作为本身匹配,例如:如果你需要匹配一个[或者\,你能在它们之前带上一个反斜杠移除它们的特殊含义,即\[或者\\。
以‘\‘开始的特殊序列中的一些表示了经常被使用的预定义字符集,例如数字集合、字符集合、或者非空白的任意字符集合。
让我们看一个例子:\w匹配任何字母数字。如果正则表达式模式以字节为单位,这等价于类[a-zA-Z0-9_]。如果正则表达式模式是字符串,则\w将匹配所有被unicodedata模块提供的在Unicode数据库总的字符。当编译正则表达式时,你能添加re.ASCII标志给\w更严格的限制。
下面提供了部分特殊序列供参考:
\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_]。
这些序列可以被包含到字符类中,例如:[\s,.]是一个字符类,将匹配任意的空白字符,或者‘,‘,或者‘.‘。
在这节中最后的元字符是‘.‘,它匹配除了新行字符之外的任何字符,使用交替模式(re.DOTALL)它将匹配包括新行的所有字符,‘.‘通常被用于需要匹配“任意字符”的场景。
处理重复
正则表达式的首要功能是匹配字符集,而正则表达式的另一个能力则是指定RE中特定部分必须被重复多少次。
处理重复的第一个元字符是‘*‘,‘*‘不会匹配字符‘*‘,它表示先前的字符能被匹配0次或者多次。
例如:ca*t将匹配ct(0个a)、cat(1个a)、caaat(3个a)、等等。RE引擎内部会限制a的匹配的数量,但通常足够了。
重复(例如*)算法是贪婪的,对于重复的RE,匹配引擎将尝试尽可能多的重复次数,如果模式的后面部分不匹配,则匹配引擎将回退并再次尝试更少的重复次数。
例如,考虑表达式a[bcd]*b,这匹配单词‘a‘,0个或者多个来自类[bcd]的字母,最后以‘b‘结束。下面是RE匹配abcbd的过程:
1、匹配a:RE匹配a成功;
2、匹配abcbd:引擎匹配[bcd]*,由于尽可能的匹配更多,所以匹配了整个字符串;
3、匹配失败:引擎试着匹配b,但是已经到达字符串结尾,因此失败;
4、匹配abcb:回退,[bcd]*匹配减少一个字符;
5、匹配失败:再次尝试b,但当前位置的字符为d;
6、匹配abc:继续回退,以至于[bcd]*仅匹配bc;
7、匹配abcb:再次尝试b,这次当前位置的字符为b,匹配成功,结束。
RE最终匹配abcb,整个过程演示了匹配引擎的匹配过程,首先匹配尽可能多的字符,如果不匹配,则不断回退再次尝试。它将回退直到[bcd]*匹配0个字符,如果任然失败,则引擎得出结论“字符串不匹配RE”。
另一个重复的元字符是+,匹配一次或者多次。小心*和+之间的不同,*匹配0次或者多次,即可以匹配空;+则需要至少出现一次。例如:ca+t将匹配cat(1个a),caaat(3个a),但不匹配ct。
另外还有两个重复限定符,其一是问号‘?‘,表示匹配一次或者0次,例如:home-?brew匹配homebrew或者home-brew。
最复杂的重复限定符是{m,n},其中m和n都是正整数,表示至少匹配m次,最多匹配n次。例如:a/{1,3}b将匹配a/b,a//b,和a///b,它将不匹配ab,或者a////b。
你能忽略m或者n,忽略m表示最小值为0,而忽略n表示无限制。
你可能已经注意到,使用最后一个限定符可以取代前面3个限定符:{0,}等价于*;{1,}等价于+;{0,1}等价于?。为什么使用*、+或者?呢?主要在于,更简短的表达式更利于阅读和理解。
使用正则表达式
现在我们已经了解了正则表达式的基本语法,下面看在Python中怎么使用正则表达式。re模块提供了使用正则表达式的接口,允许你编译RE到对象,然后使用它们。
编译正则表达式
正则表达式被编译到模式对象,提供了各种操作的方法,例如模式匹配或者替换。
>>> import re >>> p = re.compile('ab*') >>> p re.compile('ab*')
re.compile()也提供了一个可选的flags参数,用于激活各种特征,后面将详细介绍,下面是一个简单的例子:
>>> p = re.compile('ab*', re.IGNORECASE)
RE作为一个字符串传给re.compile()。RE被作为字符串处理是因为正则表达式不是Python语言核心的一部分,没有特定的语言用于创建它们。re模块仅仅是Python包含的一个C语言扩展模块,就像socket和zlib模块一样。
将RE作为字符串保持了Python语言的简单,但也存在不利,例如下一节将讲述的内容。
反斜杠问题
如前所述,正则表达式使用反斜杠来表示一些特殊组合或者允许特殊字符作为普通字符使用。这一点和Python对于发斜杠的使用冲突。
你如果想写一个RE匹配字符串\section,我们看看怎么构造一个正则表达式对象:首先,我们使用整个字符串作为正则表达式;其次,找出反斜杠和其它元字符,在它们前面添加反斜杠,变为\\section;最后,字符串被传入到re.compile(),由于传入的必须为\\section,结合Python语法,每个\的前面必须再次添加一个\,因此,最终在Python中传入的字符串为"\\\\section"。
简而言之,为了匹配一个反斜杠,在Python中你需要写‘\\\\‘作为RE字符串。这导致了很多重复的反斜杠,使语法很难于理解。
解决方案是为正则表达式使用Python的原生字符串注释。当字符串带有前缀‘r‘时,反斜杠将不以特殊字符处理,于是r"\n"是包含‘\‘和‘n‘的两个字符的字符串,而"\n"是包含换行符的一个字符的字符串。在Python中正则表达式将经常采用这种方式编写。
执行匹配
一旦你有一个已编译的正则表达式对象,你就可以使用该对象的方法和属性,下面做一个简单的介绍。
1)match()
确定RE是否匹配字符串的开头。
2)search()
扫描字符串,查找和RE匹配的任何位置。
3)findall()
找到所有RE匹配的子字符串,并作为一个列表返回。
4)finditer()
发现所有RE匹配的子字符串,并作为一个iterator返回。
如果找到匹配,match()和search()返回None;如果匹配成功,则返回一个匹配对象实例,包含匹配的信息:开始和结束点、匹配的子字符串、等等。
下面来看看Python中怎么使用正则表达式。
首先,运行Python解释器,导入re模块,并且编译一个RE:
>>> import re >>> p = re.compile('[a-z]+') >>> p re.compile('[a-z]+')
现在,你能尝试匹配各种字符串,一个空字符串将根本不匹配,由于+意味着‘一个或者更多’,match()将返回None,你能直接打印结果:
>>> p.match("") >>> print(p.match("")) None
接下来,我们尝试一个匹配的字符串,这时,match()将返回一个匹配对象,因此你应该存储结果在一个变量中以供后面使用:
>>> m = p.match('tempo') >>> m <_sre.SRE_Match object; span=(0, 5), match='tempo'>
现在你能询问匹配对象关于匹配字符串的信息。匹配对象也有几个方法和属性,最重要的几个是:
1)group()
返回被RE匹配的字符串
2)start()
返回匹配的开始位置
3)end()
返回匹配的结束位置
4)span()
返回包含匹配位置的元组(开始,结束)
下面是一些使用这些方法的例子:
>>> m.group() 'tempo' >>> m.start(), m.end() (0, 5) >>> m.span() (0, 5)
由于match()仅检查RE是否匹配字符串的开始,start()将总是返回0。然而,search()方法扫描整个字符串,因此开始位置不一定为0:
>>> print(p.match('::: message')) None >>> m = p.search('::: message'); print(m) <_sre.SRE_Match object; span=(4, 11), match='message'> >>> m.group() 'message' >>> m.span() (4, 11)
在实际编程汇总,通常将匹配对象存入一个变量中,然后检查它是否为None,例如:
p = re.compile( ... ) m = p.match( 'string goes here' ) if m: print('Match found: ', m.group()) else: print('No match')
findall()返回匹配字符串的列表:
>>> p = re.compile('\d+') >>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping') ['12', '11', '10']
findall()在返回结果前必须创建完整的列表,而finditer()则返回匹配对象实例作为一个iterator:
>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...') >>> iterator <callable_iterator object at 0x...> >>> for match in iterator: ... print(match.span()) ... (0, 2) (22, 24) (29, 31)
模块级函数
你不是一定需要创建一个模式对象然后调用它的方法,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') <_sre.SRE_Match object; span=(0, 5), match='From '>
这些函数创建一个模式对象,并调用它上面的方法,它们也存储编译后的对象到缓存中,以至于未来使用同样的RE将不需要重新编译。
你应该用这些模块及的函数,还是应该通过模块对象来调用呢?如果你正在做一个正则表达式的循环,则预编译将节省许多函数调用,否则,两个方式没有太大区别。
编译标志
编译标志让你修改正则表达式如何工作的一些方面。在re模块中标志可以使用两种名称,长名称,例如IGNORECASE,和短名称,例如I。通过位或运算,多个标志能被指定,例如re.I | re.M设置I和M标志。
下面是可用标志的列表和每个标志的解释:
1)ASCII, A
当使用\w、\b、\s和\d时仅匹配ASCII字符;
2)DOTALL, S
使‘.‘匹配任何字符,包括新行;
3)IGNORECASE, I
忽略大小写匹配;
4)LOCALE, L
做地域相关匹配;
5)MULTILINE, M
多行匹配,影响^和$;
6)VERBOSE, X (for ‘extended’)
启动详细的RE,能更清晰的组织和更好理解。
例如,下面使用了re.VERBOSE,使RE更容易阅读:
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)
如果没有使用re.VERBOSE,则RE将是这样:
charref = re.compile("&#(0[0-7]+" "|[0-9]+" "|x[0-9a-fA-F]+);")
在上面的例子中,Python的自动字符串串联被用于将RE分化到多个片段,但是它任然比使用re.VERBOSE更难理解。
正则表达式的更多特性
到目前为止我们仅覆盖了正则表达式的部分特性,在这里,我们将探索一些新的特性。
更多元字符
这里我们将介绍更多的元字符。
1)|
表示‘或’操作,如果A和B都是正则表达式,则A|B表示匹配A或者匹配B。为了能有效的工作,|有很低的优先级,例如:Crow|Servo将匹配Crow或者Servo,而不是Cro,一个‘w’或者一个‘S’,再接上ervo。
为了匹配字符‘|‘,你需要使用\|,或者封装它到一个字符类中,作为[|]。
2)^
匹配行的开始。除非设置了MULTILINE标志,这将仅匹配字符串的开始。在MULTILINE模式下,这也匹配每个新行的开始。
例如:如果你希望匹配在行的开始匹配单词From,RE中将使用^From。
>>> print(re.search('^From', 'From Here to Eternity')) <_sre.SRE_Match object; span=(0, 4), match='From'> >>> print(re.search('^From', 'Reciting From Memory')) None
3)$
匹配行的结尾。可以是字符串的结尾,或者是被新行符跟随的部分。
>>> print(re.search('}$', '{block}')) <_sre.SRE_Match object; span=(6, 7), match='}'> >>> print(re.search('}$', '{block} ')) None >>> print(re.search('}$', '{block}\n')) <_sre.SRE_Match object; span=(6, 7), match='}'>
为了匹配字符‘$‘,需要使用\$或者将它分装到字符类中,作为[$]。
4)\A
仅匹配字符串的开始。当不使用MULTILINE模式时,\A和^是相同的;在MULTILINE模式,他们是不同的:\A任然仅匹配字符串的开始,而^可以匹配每个新行的开始。
5)\Z
匹配仅在字符串末尾。
6)\b
单词边界。这是一个零宽度断言,仅匹配单词的开始和结束。一个单词被定义为字母的序列,以至于单词的结束被表示为空白或者一个非字母字符。
下面是一个例子,匹配单词class,但它位于一个单词内部时将不被匹配:
>>> p = re.compile(r'\bclass\b') >>> print(p.search('no class at all')) <_sre.SRE_Match object; span=(3, 8), match='class'> >>> print(p.search('the declassified algorithm')) None >>> print(p.search('one subclass is')) None
在使用时有两点需要注意:首先,在Python中,\b表示退格字符,ASCII值是8,如果你不用原始字符串,那么Python将转换\b到一个退格字符,你的RE将不按你的设想匹配。下面的例子和我们上面的例子相似,仅有的区别是RE字符串少了‘r‘前缀:
>>> p = re.compile('\bclass\b') >>> print(p.search('no class at all')) None >>> print(p.search('\b' + 'class' + '\b')) <_sre.SRE_Match object; span=(0, 7), match='\x08class\x08'>
第二,在字符类里,\b表示退格字符,和Python中的含义表示一致。
7)\B
另一个零宽度断言,和\b相反,仅匹配当前位置不是单词边界。
分组
组通过‘(‘和‘)‘元字符标识,‘(‘和‘)‘在这里和数学表达式中的含义相同,它们将内部的表达式归为一个分组,你能指定一个分组重复的次数,通过使用重复元字符*、+、?或者{m,n}。例如,(ab)*将匹配0个或者多个ab。
>>> p = re.compile('(ab)*') >>> print(p.match('ababababab').span()) (0, 10)
组也能获取它们匹配的字符串的开始和结束点,通过传递一个参数到group()、start()、end()和span()。组的编号从0开始,组0总是存在的,他就是整个RE,因此匹配对象方法将组0作为他们的默认参数。
>>> p = re.compile('(a)b') >>> m = p.match('ab') >>> m.group() 'ab' >>> m.group(0) 'ab'
子组从左到右编号,从1开始。组能是嵌套的。为了确定编号,从左向右只算开放括号字符。
>>> p = re.compile('(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中必须使用原始字符串表示。
例如,下面的RE探测同时出现两个相同单词的情况:
>>> p = re.compile(r'(\b\w+)\s+\1') >>> p.search('Paris in the the spring').group() 'the the'
这种匹配方式在搜索中很少使用,但在字符串替换时却非常有用。
非捕获和命名组
RE可以使用许多组,用于捕获感兴趣的子字符串或者使复杂的RE结构更清晰,这使通过组编号进行跟踪变得非常困难。有两个方法可以解决这个问题,我们首先看第一个。
有时你将想要使用一个组表示正则表达式的一部分,但是不想要获取该组的内容。这时,你能使用非捕获组:(?:...),将...替换为任何正则表达式。
>>> m = re.match("([abc])+", "abc") >>> m.groups() ('c',) >>> m = re.match("(?:[abc])+", "abc") >>> m.groups() ()
除了你不能获取组匹配的内容,一个非捕获组的行为和捕获组的行为完全一致,你能放任何内容在它里面,可以使用重复元字符(例如*)重复它,或者嵌套其它组(捕获或者非捕获)。当修改一个已经存在的模式时(?:...)是特别有用的,因为你可以增加新的组而不改变已有的组的编号。但需要注意,使用非捕获组和捕获组在匹配上没有任何效率上的不同。
另一个更有意义的特征是命名组:取代为组编号,改为使用为组指定一个名称。
命名组是Python特定扩展之一,语法为:(?P<name>...),name是组的名称。匹配对象方法可以接受组的编号或者组的名称,因此你能使用两种方法得到组的匹配信息:
>>> p = re.compile(r'(?P<word>\b\w+\b)') >>> m = p.search( '(((( Lots of punctuation )))' ) >>> m.group('word') 'Lots' >>> m.group(1) 'Lots'
命名组是便利的,因为名称比编号更容易记忆,下面是一个来自imaplib模块的RE的例子:
InternalDate = re.compile(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的内容应该和当前点的内容匹配。为发现2个连续重复单词的正则表达式,(\b\w+)\s+\1能被写为(?P<word>\b\w+)\s+(?P=word):
>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)') >>> p.search('Paris in the the spring').group() 'the the'
预测先行断言
另一个零宽度断言是预测先行断言。预测先行断言可以在正、负形式使用,像这样:
1)(?=...)
正预测先行断言。如果包含...表示的正则表达式在当前位置被成功匹配,则成功,否则失败。但是,虽然包含的正则表达式被尝试,但匹配引擎并不会前进,模式的其余部分还是从断言开始的地方开始匹配。
2)(?!...)
负预测先行断言。和正预测先行断言相反,如果它包含的正则表达式不匹配当前位置的字符串,则成功。
为了使描述更加具体,我们看一个例子说明预测先行的作用。考虑一个简单的模式,用于匹配一个文件名,并将它拆分为文件名和扩展名。例如,news.rc中,news表示文件名,rc表示扩展名。
匹配的模式很简单:
.*[.].*$
注意.需要放到字符类中,因为它是一个元字符;也注意$,用于确保所有字符串的其余部分被包含在扩展中。这个正则表达式可以匹配foo.bar、autoexec.bat、sendmail.cf和printers.conf。
现在,考虑一个稍复杂点的情况,如果你想匹配扩展名不是bat的文件名该怎么做?下面是一些不正确的尝试:
1).*[.][^b].*$
这个尝试要求扩展名的第一个字符不是b来排除bat。这时错误的,因为该模式也不匹配foo.bar。
2).*[.]([^b]..|.[^a].|..[^t])$
这个比上一个更复杂一点,要求:扩展的第一个字符不匹配b,或者第二个字符不匹配a,或者第三个字符不匹配t。这个模式匹配foo.bar,不匹配autoexec.bat,但是它要求扩展名必须为3个字符,将不匹配带有2个字符扩展名的文件,例如sendmail.cf。我们将继续完善它。
3).*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$
在这个尝试中,第二个和第三个字符都是可选的,为了匹配的扩展名小于三个字符的情况,例如sendmail.cf。
现在模式开始复杂起来了,开始难于阅读和理解。更糟的是,如果问题改变,你想同时排除扩展名bat和exe,模式将变得更为复杂和难于理解。
一个负预测先行断言可以解决这个问题。
.*[.](?!bat$).*$
含义为:如果当前点表达式bat不匹配,则尝试模式的其余部分;如果bat$匹配,整个模式将失败。结尾的$用于防止出现sample.batch的情况。
排除另一个文件扩展名现在也容易了,简单的增加它作为断言的二选一。下面的模式同时排除bat和exe:
.*[.](?!bat$|exe$).*$
修改字符串
目前为止,我们仅适用正则表达式查询字符串,正则表达式也可用于修改字符串,使用下面的方法:
1)split()
从RE匹配的地方将字符串分解为字符串列表;
2)sub()
找到RE匹配的所有子字符串,并使用不同的字符串取代它们;
3)subn()
和sub做的事相同,但是返回新字符串和替换的次数。
分解字符串
split()方法用于分解一个字符串,使用RE匹配的子字符串作为分隔符,返回分解后的子字符串列表。它和字符串的split()方法是类似的,但是提供了更为通用的分隔符;字符串的split()方法仅支持空格或者固定的字符串。re也提供了一个模块级的re.split()函数。
split(string[, maxsplit=0])
通过正则表达式的匹配分解字符串。如果在RE中使用了括号,则正则表达式的匹配也将出现在结果列表中。如果maxsplit值大于0,则最多做maxsplit次分解。
你能通过设置maxsplit的值限制分解的数量。当maxsplit大于0时,最多进行maxsplit次分解,字符串的剩余部分被作为列表的最后一个元素返回。在下面的例子中,分隔符时任何非字符或数字的字符组合:
>>> 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', ''] >>> p.split('This is a test, short and sweet, of split().', 3) ['This', 'is', 'a', 'test, short and sweet, of split().']
有时你不仅对分隔符之间是什么感兴趣,而且需要知道哦分隔符是什么。如果在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', '.', '']
模块级的函数re.split()增加了RE作为第一个参数,其余的相同:
>>> 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,可以是一个字符串,或者一个函数。
sub(replacement, string[, count=0])
返回替换后的字符串,替换采用从左到右并且非重叠的方式。如果模式未被发现,返回未改变的字符串。
可选参数count用于指定替换的最大次数;count必须非负。默认值0意味着替换所有。
下面是一个简单的例子。它使用colour替换所有匹配的颜色名:
>>> 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'
subn()方法做同样的事,但是返回一个长度为2的元组,包含新字符串和替换的次数:
>>> p = re.compile( '(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 = re.compile('x*') >>> p.sub('-', 'abxd') '-a-b-d-'
如果replacement是一个字符串,在它里面的任何反斜杠转义符都会被处理。即,\n会被转换为一个新行字符,\r被转换为回车符,等等。未知的转义符例如\j被遗留。反向引用,例如\6,被RE中的对应组匹配的子字符串取代。这让你在替换后的结果字符串中能合并原始字符串的部分。
下面的例子匹配单词section,被一个{}包含的字符串跟随,并且改变section到subsection:
>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE) >>> p.sub(r'subsection{\1}','section{First} section{second}') 'subsection{First} subsection{second}'
也可以使用(?P<name>...)命名的组。\g<name>将通过组名来匹配,\g<number>将通过组编号来匹配。因此\g<2>等价于\2,当可以避免歧义,例如\g<2>0表示匹配组2,而\20则会被解释为匹配组20。下面替换的例子都是等价的,但是使用了3种不同的方式:
>>> 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也可以是一个函数,可以给你更多的控制。如果replacement是一个函数,函数会处理每一个模式匹配的非重叠的子字符串。在每次调用,函数被传递一个匹配对象作为参数,函数可以使用这个信息计算替换字符串并返回它。
在下面的例子中,replacement函数转换10进制数到16进制:
>>> 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.'
当使用模块级别的re.sub()函数时,模式作为第一个参数传入。模式可以为一个对象或者字符串;如果你需要指定正则表达式标志,你必须使用一个模式对象作为第一个参数,或者在模式字符串中用嵌入的修饰语,例如:sub("(?i)b+", "x", "bbbb BBBB")返回‘x x‘。
常见问题
正则表达式在一些应用中是有用的工具,但他们的行为不是直观的,有时并不按照你所期望的方式工作。这节将描述一些常见的陷阱。
用String方法
有时使用re模块是错误的。如果你正在匹配一个固定的字符串,或者一个单个的字符类,并且你并没有用到任何re特征,例如IGNORECASE标志,那么正则表达式的威力并不被需要。String有几个为固定的字符串执行操作的方法,并且它们通常更快,因为他们的实现是单个的C循环,并且针对该场景做了优化。
一个常见的例子是替换一个固定的字符串为另一个,例如,你想替换word为deed,re.sub()似乎可以用于这种场景,但是你应该考虑replace()方法。注意replace()也将替换单词内的word,例如修改swordfish为sdeedfish,但是简单的RE word也将做那。(为了避免单词内的替换,模式将必须是\bword\b,为了要求word是一个独立的单词。这一点超出了replace()的能力。)
另一个常见任务是探测字符串中某个字符的位置,或者使用另一个字符替换它。你可以使用类似这样的操作来实现:re.sub(‘\n‘, ‘ ‘, S),但是translate()也可以完成这样的任务,并且比任何正则表达式的操作都更快。
总之,使用re模块之前,考虑你的问题是否能使用更快、更简单的字符串方法解决。
match() VS search()
match()函数仅检查RE是否在字符串的开始匹配,而search()将扫描整个字符串。记住这一点非常重要,match()将仅报告在起点为0进行的成功匹配;如果匹配的起点不为0,match()将不报告它。
>>> print(re.match('super', 'superstition').span()) (0, 5) >>> print(re.match('super', 'insuperable')) None
另一个方面,search()将扫描整个字符串,报告发现的第一个成功匹配。
>>> print(re.search('super', 'superstition').span()) (0, 5) >>> print(re.search('super', 'insuperable').span()) (2, 7)
有时你会被引诱使用re.match(),仅仅增加.*到你的RE之前。你应该拒绝这个诱惑,转而使用re.search()。正则表达式编译器会做一些RE的分析,为了加速查找匹配的处理。一个如此的分析是分析出匹配的首字符必定是什么;例如,一个以Crow开始的模式必须匹配首字符‘C‘。这个分析使引擎快速扫描字符串查询开始字符,当‘C‘被发现时才继续向下匹配。
增加.*将使这个优化无效,要求扫描到字符串的结尾,在回溯发现RE其余部分的一个匹配。因此,优先使用re.search()。
贪婪 VS 非贪婪
当重复一个正则表达式时,例如a*,正则表达式的行为是尽可能多的匹配。这一点经常会导致一些问题,当你尝试匹配一对对称的限定符时,例如包含HTML标签的尖括号,由于.*的贪婪特性,简单的匹配一个HTML标签的模式不工作:
>>> s = '<html><head><title>Title</title>' >>> len(s) 32 >>> print(re.match('<.*>', s).span()) (0, 32) >>> print(re.match('<.*>', s).group()) <html><head><title>Title</title>
RE在<html>中匹配‘<‘,然后.*消费字符串其余的所有部分,由于RE最后的>不能匹配,于是正则表达式引擎不得不回溯字符直到它为>找到一个匹配。最后的匹配就是从<html>的‘<‘到</title>的‘>‘,并不是你想要的。
在这种场景,应该使用非贪婪限制符*?、+?、??、或者{m,n}?,他们将匹配尽可能少的字符。在上面的例子中,在第一个‘<‘匹配之后,‘>‘将被立即尝试,如果失败,引擎每次前进一个字符,再次尝试,最后得到正确的结果:
>>> print(re.match('<.*?>', s).group()) <html>
(注意使用正则表达式解析HTML或者XML是痛苦的。因为写一个能处理所有场景的正则表达式是非常复杂的,使用HTML或者XML解析器来完成这样的任务。)
使用re.VERBOSE
到现在你可能注意到正则表达式是一个非常紧凑的形式,但是他们不是非常易读的。中等复杂程度的RE能成为反斜杠、括号和元字符的冗长的集合,使他们呢难于阅读和理解。
为如此的RE,当编译正则表达式时指定re.VERBOSE标志是有帮助的,因为它允许你格式化正则表达式使其更清晰。
re.VERBOSE标志有几个影响。在正则表达式中但不在字符类中的空格将被忽略,这意味着一个表达式例如dog | cat将等价于dog|cat,但是[a b]任然匹配字符‘a‘、‘b‘和空格。此外,你也能放注释在一个RE中;注释从一个#字符到下一行。当用三引号字符串时,RE被格式化的更加清晰:
pat = re.compile(r""" \s* # Skip leading whitespace (?P<header>[^:]+) # Header name \s* : # Whitespace, and a colon (?P<value>.*?) # The header's value -- *? used to # lose the following trailing whitespace \s*$ # Trailing whitespace to end-of-line """, re.VERBOSE)
和下面的表达式比起来,这是更可读的:
pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")