目录
第3章 括号
^[1-9]\d{14}(\d{2}[1-9x])?$
分组:如果用量词限定出现次数的元素不是字符或者字符组,而是连续的几个字符甚至子表达式,就应该用括号将它们“编为一组”
有了分组,就可以准确表示“长度只能是m或n”
例子:匹配一个长度为13或者16的数字字符串
\d{13,16},但长度为14或15的数字字符串同样会匹配。
真正准确的做法是:首先匹配长度为13的数字字符串,然后匹配可能出现的长度为3的数字字符串,正则表达式就成了\d{13}(\d{3})?。
例3-5 准确匹配open tag
例3-6 URL匹配的表达式
例3-8 完整匹配E-mail地址的正则表达式
re.search(r"^[-\w.]{1,64}@([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}$", "aww@qq.com")
3.2 多选结构
身份证号分情况处理
15位身份证号就是[1-9]开头,之后是14位数字;
18位身份证号就是[1-9]开头,之后是16位数字,最后是[0-9x]?
(…|…) 在括号内以竖线|分隔开多个子表达式,这些子表达式也叫多选分支(option);在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就能成功;如果所有子表达式都不能匹配,则整个多选结构匹配失败。
例3-9 用多选结构匹配身份证号码
^([1-9]\d{14}|[1-9]\d{16}[0-9x])$
例3-10 准确匹配0~255之间的字符串
(([0-9]|[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])
匹配时间
匹配号码
(0|\+86)?(13[0-9]|15[0-356]|18[025-9])\d{8}
例3-11 准确的HTML tag匹配
<('[^']*'|"[^"]*"|[^'">])+>
例3-12 没有括号的多选结构
因为竖线|的优先级很低(关于优先级,☞108),所以^ab|cd$其实是(^ab|cd$),而不是^(ab|cd)$,它的真正意思是“字符串开头的ab或者字符串结尾的cd”,而不是“只包含ab或cd的字符串”
多选分支和字符组的另一点重要区别(同时也是常犯的错误)是:排除型字符组可以表示“无法由某几个字符匹配的字符”,多选结构却没有对应的结构表示“无法由某几个表达式匹配的字符串”。从例3-14可以看到,[^abc]表示“匹配除a、b、c之外的任意字符”,(^a|b|c)却不能表示“匹配除a、b、c之外的任意字符串”。
3.3 引用分组
括号不仅能把有联系的元素归拢起来并分组,还有其他的作用—使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过group(num)之类的方法“引用”分组在匹配时捕获的内容(这个方法之前已经出现过)。其中,num表示对应括号的编号,括号分组的编号规则是从左向右计数,从1开始。因为“捕获”了文本,所以这种功能叫作捕获分组(capturing group)。对应的,这种括号叫作捕获型括号。
例3-16 引用捕获分组
例3-17 默认存在编号为0的分组
例3-18 嵌套的括号
无论括号如何嵌套,分组的编号都是根据开括号出现顺序来计数的;开括号是从左向右数起第多少个开括号,整个括号分组的编号就是多少
通常的超链接tag类似这样:<ahref="url">text</a>
其中url是超链接地址,text是文本,为了准确获取这两部分内容,可以把表达式改为
<a\s+href="([^"]+)">([^<]+)</a>
给匹配url和text的表达式分别加上括号,就是([^"]+)和([^<]+)(注意其中<a之后是\s+,因为这里需要的是空白字符,而不限定是空格字符,而且可能不止一个字符)。
在等号=两端可能还有空白字符,比如<a href = "url">text</a>,所以正则表达式中的=两端也应该添加\s*,于是得到<a\s+href\s*=\s*"([^"]+)">([^<]+)</a>。
属性既可以用双引号字符串表示,也可以用单引号字符串表示,比如<ahref='url'>text</a>;甚至可以不用引号,比如<a href=url>text</a>
首尾出现的单引号或者双引号字符用["']?即可匹配;真正的URL,既不能包含单引号,也不能包含双引号,还不能有空白字符,所以可以用[^" '\s]+匹配,而且这部分是需要提取出来的,别忘了它外面的括号。于是得到了最后的表达式<a\s+href\s*=\s*["']?([^"'\s]+)["']?>([^<]+)</a>。
例3-19 用分组提取出超链接的详细信息
# 用分组提取超链接信息
regexLang = r"<a\s+href\s*=\s*[\"']?([^\"'\s]+)[\"']?>([^<]+)</a>"
text = "<a href=\"www.baidu.com\">百度</a>"
# print(re.findall(regexLang, text))
for hyperLink in re.findall(regexLang, text):
print(hyperLink[1], hyperLink[0])
----------------------
百度 www.baidu.com
匹配<img>时,在<img和src之间可能还有其他内容,比如width=750之类,所以不能仅仅用\s+匹配,而应当添加[^>]*?。在src=…之后也同样如此
例3-20 新手容易弄错分组的结构
# 容易弄错分组的结构
print(re.search(r"(\d{4})-(\d{2})-(\d{2})", "2012-12-21").group(1))
2012
print(re.search(r"(\d){4}-(\d{2})-(\d{2})", "2012-12-21").group(1))
2
在第一个表达式中,编号为1的分组对应的括号是(\d{4}),其中的\d{4}是“匹配4个数字字符”的子表达式。在第二个表达式中,编号为1的分组对应的括号是(\d),其中的\d是“匹配1个数字字符”的子表达式,因为之后有量词{4},所以整个括号作为单个元素,要重复出现4次,而且编号都是1。于是每重复出现一次,就要更新一次匹配结果。所以在匹配过程中,编号为1的分组匹配的文本的值,依次是2、0、1、0,最后的结果是0。
例3-21 正则表达式替换
'''
re.sub(pattern,replacement,string)
pattern是用来匹配被替换文本的表达式
replacement是要替换成的文本
string是要进行替换操作的字符串
'''
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", "2012-12-21"))
# 12/21/2012
replacement中也可以引用分组,形式是\num,其中的num是对应分组的编号。不过,replacement并不是一个正则表达式,而是一个普通字符串。根据字符串中的转义规定,\t表制表符,\n表示换行符,\1、\2却不是字符串中的合法转义序列,所以也必须指定replacement为原生字符串(☞98)
例3-22 在替换中使用分组
如果想在replacement中引用整个表达式匹配的文本,不能使用\0,即便用原生字符串也不行。因为在字符串中,\0开头的转义序列通常表示用八进制形式表示的字符,\0本身表示ASCII字符编码为0的字符。如果一定要引用整个表达式匹配的文本,则可以稍加变通,给整个表达式加上一对括号,之后用\1来引用
例3-23 在替换中,使用\1替代\0
3.3.1 反向引用
引用分组,了解到能引用某个分组内的子表达式匹配的文本,但引用都是在匹配完成后进行的,能不能在正则表达式中引用呢?
答案是可以的,这种功能被称作反向引用(back-reference),它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是左侧),其形式也是\num,其中num表示所引用分组的编号,编号规则与之前介绍的相同。
例3-24 用反向引用匹配重复字母
有了反向引用功能,就可以先匹配open tag,再匹配其他内容,直到最近的close tag为止:在匹配open tag时,用一个括号分组匹配tag name的表达式([^>]+);在匹配close tag时,用\1引用之前匹配的tag name,就完成了配对(要注意的是,这里需要用到忽略优先量词*?,否则可能会出现错误匹配,理由在第2章匹配JavaScript代码时讲过)。最后得到的表达式就是<([^>]+)>[\s\S]*?</\1>
例3-25 用反向引用匹配成对的tag
print(re.search(r"<([^>]+)>[\s\S]*?</\1>", "<text>cc</text>") is not None) # True
例3-26 用反向引用匹配更复杂的成对tag
也有些tag更复杂一点,比如<span class="class1">text</span>,在tag名之后有一个空白字符,然后是其他属性,此时原有的表达式就无法匹配了。为应对这类情况,应当修改表达式,让分组1准确匹配tag name,它可以是数字、小写字母、大写字母,所以将它修改为<([a-zA-Z0-9]+)\s[^>]+>[\s\S]*?<\1>,但满足了\s[^>]+的匹配,就无法应对之前的那些open tag。为了兼容两种情况,必须用括号分组和量词?来限定,也就是改为(\s[^>]+)?,最后的表达式就是<([a-zA-Z0-9]+)(\s[^>]+)?>[\s\S]*?</\1>
print(re.search(r"<([a-zA-Z0-9]+)(\s[^>]+)?>[\s\S]*?</\1>", "<text fds='xx'>cc</text>") is not None) # True
'''
group(0) # <text fds='xx'>cc</text>
group(1) # text
group(2) # fds='xx'
'''
print(re.search(r"<([a-zA-Z0-9]+)(\s[^>]+)?>[\s\S]*?</\1>", "<text fds='xx'>cc</text>").group(2))
例3-27 匹配IP地址的正则表达式
例3-28 可能具有二义性的反向引用
原来\10会被解释成“第10个捕获分组匹配的文本”,而不是“第1个捕获分组匹配的文本之后加上字符0”。如果我们就是希望做到后面这步,Python提供了\g<num>表示法,将\10写成\g<1>0,这样同时也避免了替换时无法使用\0的问题
例3-29 使用g<n>消除二义性
例3-31 Java中的引用
Python和PHP的规定比较明确,所以避免了\num的二义性;其他一些语言却不是如此,根据它们的文档,引用捕获分组只有\num(或者$num)一种记法,这时候\10(其实\11、\21等都是如此)的二义性问题就无可避免了(实际上,本书中介绍的语言,除了Python和PHP之外都是如此)。比如Java对\num中的num是这样规定的:如果是一位数,则引用对应的捕获分组;如果是两位数且存在对应捕获分组时,引用对应的捕获分组,如果不存在对应的捕获分组,则引用一位数编号的捕获分组。也就是说,如果确实存在编号为10的捕获分组,则\10引用此捕获分组匹配的文本;否则,\10表示“第1个捕获分组匹配的文本”和“字符0”。
3.3.3 命名分组
捕获分组通常用数字编号来标识,但这样有几个问题:数字编号不够直观,虽然规则是“从左向右按照开括号出现的顺序计数”,但括号多了难免混淆;引用时也不够方便,上面已经讲过\10引起混淆的情况。为解决这类问题,一些语言和工具提供了命名分组(named grouping),可以将它看作另一种捕获分组,但是标识是容易记忆和辨别的名字,而不是数字编号。命名分组的记法也并不复杂。在Python中用(?P<name>regex)来分组的,其中的name是赋予这个分组的名字,regex则是分组内的正则表达式。这样,匹配年月日的正则表达式中,可以给年、月、日的分组分别命名,再用group(name)来获得对应分组匹配的文本。
例3-32 命名分组捕获
例3-33 命名分组捕获时仍然保留了数字编号
在Python中,如果使用了命名分组,在表达式中反向引用时,必须使用(?P=name)的记法;而要进行正则表达式替换,则需要写作\g<name>,其中的name是分组的名字。
例3-34 命名分组的引用方法
3.4 非捕获分组
无论是否需要引用分组,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果并不需要引用,保存这些信息无疑会影响正则表达式的性能;如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能。为解决这种问题,正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟一个问号和冒号(?:…),这样的括号叫作非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组
例3-35 非捕获分组的使用
非捕获分组不需要保存匹配的文本,整个表达式的效率也因此提高,但是看起来不如捕获分组美观,所以很多人不习惯这种记法。不过,如果只需要使用括号的分组或者多选结构的功能,而没有用到引用分组,则应当尽量使用非捕获型括号。如果不习惯这种记法,比较好的办法是,在写正则表达式时先统一使用捕获分组,确保正确之后,再把不需要引用的分组改为非捕获分组—当然,引用分组的编号可能也要调整(在上例中,只需要取月份信息,把第一个分组改为非捕获分组之后,取月份信息对应分组的编号从2变为1)
3.5 补充
3.5.1 转义
括号的转义与它们都不同,与括号有关的所有三个元字符(、)、|都必须转义。因为括号非常重要,所以无论是开括号还是闭括号,只要出现,正则表达式就会尝试寻找整个括号,如果只转义了开括号而没有转义闭括号,一般会报告“括号不匹配”的错误。另一方面,多选结构中的|也必须转义(多选结构可以不用括号只出现|),所以,也不要忘记对|的转义,否则就可能出现例3-36的问题。
例3-36 括号的转义
3.5.2 URL Rewrite
URL Rewrite是常见Web服务器中都具备(也必需)的功能,它用来进行网址的转发,下面是一个转发的例子。