子表达式
使用括号(
和)
把一个表达式包围起来,变成一组,括号的这种功能叫做分组(grouping),括号内的表达式通常被称为子表达式。子表达式是一个独立元素,它的作用是把同一个表达式的各个相关部分组合在一起。
子表达式可以把一组字符编组为一个字符集合。这样的字符集合主要用于精确设定需要重复匹配的文本及其重复次数,对|
操作符的OR条件作出准确的定义等。
子表达式的分组
如果用量词限定出现次数的元素不是字符或者字符组,而是连续的几个字符甚至子表达式,就应该用括号将它们“编为一组”。比如,希望字符串ab重复出现一次以上,就应该写作(ab)+
,此时(ab)
成为一个整体,由量词+
限定;如果不用括号直接写作ab+
,受限定的就只有b
,表示匹配a出现一次,b出现一次以上的字符串。
例子1:匹配身份证号码。身份证号码是一个长度为15或18个字符的字符串,如果是15位,则全部由数字组成,首位不能为0;如果是18位,前17位全部是数字,首位同样不能是0,末位可能是数字,也可能是x。
身份证号码的长度可以是15位,也可以是18位。两种情况下的首位都不能是0,接下来的14位必须是数字。15位的情况到这里就结束了。18位的还会有2个数字以及末位的数字或x,为了兼顾15位和18位的情况,这个作为一个整体或许不出现或许出现一次。所以正则表达式:[1-9]\d{14}(\d{2}[0-9x])?
或([1-9]\d{14}|[1-9]\d{14}\d{2}[0-9x])
例子2:在Web服务中,经常并不希望暴露真正的程序细节,所以用某种模式的URL来掩盖。比如URL:/foo/bar_qux.php,看起来是访问一个PHP页面,其实背后有复杂的路由规则。foo是模块的名称,bar是控制器的名字,qux则是方法名,三个名称中都只能出现小写字母。希望能处理的情况有三种:
只有模块名,比如
/foo
只有模块名和控制器名,比如
/foo/bar.php
模块名,控制器名和方法名同时出现,比如
/foo/bar_qux.php
首先/foo
是必须出现的,之后有两种可能。第一种可能中,/bar.php
(也就是反斜杠、控制器名、结尾的.php)是必须出现的;后一种可能中,/bar_qux.php
(也就是反斜杠、控制器名、下划线、模块名、结尾的.php)是必须出现的。第一种可能的正则表达式是/[a-z]+\.php
,第二种可能的正则表达式是/[a-z]+_[a-z]+\.php
。两种可能可以合并成/[a-z]+(_[a-z]+)?\.php
。最终的正则表达式是/[a-z]+(/[a-z]+(_[a-z]+)?\.php)?
例子3 Email地址的匹配。E-mail地址以@分隔为两段,@之前的是用户名(username),之后的是主机名(hostname)。用户名一般只允许出现数字、字母、下划线、点号,且最大长度是64个字符,而主机名则是类似mail.google.com, mail.163.com之类的字符串。
用户名的匹配比较简单,就是[A-Za-z0-9_.]
,可以简化为[\w.]
,再加上长度限制,就是[\w.]{1,64}
主机名则比较麻烦,简单的情况如somehost.com,复杂的情况则还包括子域名,如mail.somehost.net,而且子域名可能不只一级,比如mail.sub.somehost.net。主机名被点号分隔为若干段,叫做域名字段(label),每个域名字段中能出现的字符是字母、数字和横线,长度必须在1-63之间。主机名还可以是localhost这种。正则表达式是([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}
,其中*
表示不出现,或者出现多次。
总的表达式就是[\w.]{1,64}@([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}
子表达式在多选结构中的作用
比如(19|20)\d{2}
匹配19或20开头的4位数字(这种形式称为多选结构),多选结构可以没有括号,比如19|20
,但还是推荐写括号。注意这里必须写括号,否则会达不到预期效果。19|20\d{2}
匹配的是数字19或者以20开头的4位数字,为什么呢?因为|
是把位于它左边和右边的两个部分都作为一个整体来看待的,它把模式19|20\d{2}
解释为19
或20\d{2}
。
一些常用匹配
匹配月:
(0?[1-9]|1[012])
匹配日:
(0?[1-9]|[12]\d|3[01])
匹配小时:
(0?[1-9]|[01]\d|2[0-4])
匹配分钟:
(0?[1-9]|[0-5]\d|60)
匹配手机号码:
(0|\+86)?(13[0-9]|15[0-356]|18[025-9])\d{8}
手机号码的匹配:手机号码通常是11位,前面3位是号段,目前有130-139,150-153,155-156,180,182,185-189号段。另外,开头可能有0或者+86。
子表达式的嵌套
子表达式允许多重嵌套,理论上没有限制,但是还是应该遵循适可而止的原则。
例子1:匹配合法的IP地址,IP地址的表示形式是xx.xx.xx.xx
,其中xx表示一个数字,合法的IP地址中的数字必须在0-255之间。
但是这个规则用正则不好直接表示,需要分情况讨论。可以将数字从小到大进行分析,得到地址的各位数字必须符合的规则是
任何一个1位或2位数字
任何一个以1开头的3位数字
任何一个以2开头、第2位数字在0~4之间的3位数字
任何一个以25开头、第3位数字在0~5之间的3位数字
我们依次来分析这几个规则怎么用正则来表示。
任何一个1位或2位数字:
\d{1,2}
任何一个以1开头的3位数字:
1\d{2}
任何一个以2开头、第2位数字在0~4之间的3位数字:
2[0-4]\d
任何一个以25开头、第3位数字在0~5之间的3位数字:
25[0-5]
上述规则之间是或的关系,只要满足其中一个即可,可以用|
来合并所有规则得到总的正则表达式:(\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])
,注意这里每个规则都用小括号()
括起来了,表示一个子表达式。
数字后面是.
,用\.
表示。得到((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.
。注意前面的数字模式要用小括号括起来,不然.
也会被视为其中的一个(和一开始的例子类似)。
观察IP地址的形式,这个数字加.
的模式需要重复3次(最后一个数字后面没有.
),我们可以用大括号来表示重复次数,得到(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}
。数字加.
外层加上小括号,构成了一个更大的子表达式,{3}
作用在这个大的子表达式上,表示数字加.
的这个模式需要重复3次。
最后,再把数字模式重复一次就可以了。最终的正则表达式就是:(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))
注意:把必须匹配的情况考虑周全并写出一个匹配的结果符合预期的正则表达式很容易,但把不需要匹配的情况也考虑周全并确保它们都将被排除在匹配结果以外往往要困难得多。
回溯引用:前后一致匹配
子表达式的另一个重要用途就是定义回溯引用(backreference)。
例子:查找所有HTML标题文字
HTML Web页面里的标题文字是用标题标签(<H1>
到<H6>
)来定义和排版的。我们的任务是找出某个Web页面里所有的标题文字。
例如Web页面的标题相关文本是:<H1>Welcome to my Homepage</H1>
那么正则表达式可以是<[hH]1>.*</[hH]1>
。但是它只能匹配一级标题,而我们需要匹配任意级别的标题,怎么办呢?
可以用字符集合来代替1,得到<[hH][1-6]>.*?</[hH][1-6]>
。这里之所以要使用.*?
(懒惰型)而不是.*
(贪婪型),是因为贪婪型元字符会尽可能匹配所有的符合要求的文本。假设文本是<H1>Welcome to my Homepage</H1><H2>Welcome to my Homepage</H2>
,那么贪婪型元字符会从开头的<H1>
一直匹配到末尾的</H2>
,这并不是我们想要的结果。懒惰型元字符则匹配到</H1>
就停止了。
看到这里你是不是发现了其他问题?上述匹配中,即使<H1>
和</H2>
不是配对的,贪婪型元字符的模式也会匹配。这是因为模式中用来匹配结束标签的部分(第2部分)和用来匹配开始标签的部分毫无关联(第1部分)。第2部分并不知道第1部分匹配的是<H1>
还是<H2>
。
要解决这个问题,必须用到回溯引用。使用括号之后,正则表达式会保存每个子表达式的匹配结果,这可以理解为“捕获”了文本,所以这种功能也叫做捕获分组,对应的,这种括号叫做捕获型括号。
回溯引用匹配(反向引用)
回溯引用允许正则表达式模式引用前面的匹配结果(只能引用子表达式),这也称为反向引用,允许在正则表达式内部引用之前的匹配结果。
比如要在一段文本里找出所有连续2次重复出现的单词,示例文本是This is a block of of text
。正则表达式可以是[ ]+(\w+)[ ]+\1
,其中[ ]+
匹配一个或多个空格,\w+
匹配一个或多个字母数字字符,它被括号括起来,是一个子表达式。但是这里的子表达式不是用来进行重复匹配的,而是为了划分出来以便在后面引用。模式末尾的\1
就是一个回溯引用,引用的是前面划分出来的子表达式(w+)
:当(\w+)
匹配到单词of
时,\1
也匹配单词of
。
\1
代表模式里的第1个子表达式,\2
代表第2个子表达式,以此类推。
注意:无论括号如何嵌套,编号都是根据开括号出现顺序来计数的。
注意:回溯引用匹配通常从1开始计数,在许多实现里,第0个匹配可以用来代表整个正则表达式。
那么HTML标题匹配的问题就可以解决了。匹配HTML的任何级别标题的开始标签和与之配对的结束标签(忽略任何不配对的标签组合)的正则表达式就是<[hH]([1-6])>.*?</[hH]\1>
,这里的子表达式是([1-6])
。
回溯引用的跨模式使用
回溯引用可以跨模式使用,在第一个模式里被匹配的子表达式可以用在第二个模式里。同一个子表达式可以被引用任意多次。这个功能可以用在数据提取和替换中。
正则表达式会保存每个子表达式匹配的文本,匹配完成后,通过group(num)之类的方法引用之前匹配的文本,num表示子表达式的编号。后续在介绍Python的re模块时会有使用方法的介绍。
回溯引用在替换操作中的应用
替换操作需要用到两个正则表达式:一个用来给出搜索模式,另一个用来给出匹配文本的替换模式。
例子:把电话号码例如313-555-1234重新排版为(313) 555-1234的形式。
先用正则表达式匹配电话号码:(\d{3})(-)(\d{3})(-)(\d{4})
,这个模式被划分为5个子表达式,第1个子表达式匹配前3位数字,第2个子表达式匹配-
字符等。这5个子表达式都可以拿出来单独使用。重新排版替换的表达式是($1) $3-$5
。
注意:不同的正则表达式实现里的回溯引用语法有差异,有的是用$
,有的是用\
。
大小写转换(只有部分正则表达式的实现支持)
\E
结束\L或\U转换
\l
把下一个字符转换为小写
\L
把\L到\E之间的字符全部转换为小写
\u
把下一个字符转换为大写
\U
把\U到\E之间的字符全部转换为大写
比如把一级标题的标题文字转换为大写
正则表达式:(<[hH]1>)(.*?)(</[hH]1>)
,这里面有3个子表达式:开始标签,标题文字,结束标签。
替换操作:$1\U$2\E$3
非捕获分组
在捕获分组中,无论我们是否需要引用分组,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果不需要引用,保存这些信息无疑会影响正则表达式的性能;如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能。
为解决这种问题,正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟着一个问号和冒号(?:...)
,这样的括号叫做非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组。
例如(?:\d{4})-(\d{2})-(\d{2})
就只有2个捕获分组,\1
获取到的是第一个(\d{2})
捕获到的文本。
Last updated
Was this helpful?