用字符组和量词可以匹配引号字符串,也可以匹配html tag,如果需要用正则表达式匹配身份证号码,依靠字符组和量词如何实现?
身份证号码是一个长度为15或18的字符串,如果是15位,则全部由数字组成,首位不能是0;如果是18位,则前17位全部是数字,末尾可能是数字,也可能是x。
虽然字符串的长度是可变的,但除去第一位和最后一位,中间部分的长度必须是13或16;另外末尾一位到底是[0-9]
还是[0-9x]
,取决于整体长度。区分两种情况分别考虑,要更加清楚一些:
位数 | 表达式 |
---|---|
15位 | [1-9]\d{14} |
18位 | [1-9]\d{14}\d{2}[0-9x] |
看来,只要以15位号码的匹配位基础,末尾加上可能出现的\d{2}[0-9x]
即可。此时的\d{2}[0-9x]
必须作为一个整体,同时出现或不出现。
使用括号()
,把正则表达式改写为[1-9]\d{14}(\d{2}[0-9x])?
。上一章提到过,量词限定之前元素
的出现次数,这个元素可能是一个字符,也可能是一个字符组,还可能是一个表达式——如果把一个表达式用括号包围起来,这个元素就是括号里的表达式,通常被称为“自表达式”。所以(\d{2}[0-9x])
就表示该子表达式作为一个整体,或许不出现,或许出现最多一次。
括号的这种功能叫做分组(grouping)。如果量词限定出现次数的元素不是字符或字符组,而是几个字符甚至表达式,就应该用括号将他们“分为一组”。比如,希望字符串ab重复出现一次以上,就应该写作(ab)+
,此时(ab)
成为一个整体,由量词+
来限定;如果不用括号而直接写ab+
,受+
限定的就只有b
。
有了分组,就可以准确表示“长度只能是m或n”。比如在上面匹配身份证的例子中,要匹配一个长度为13或16的数字字符串,正则表达式可以写为\d{13}(\d{3})?
。
例3-1 匹配身份证
idCardRegex = re.compile(r'^[1-9]\d{14}(\d{2}[\dx])?')
分组是非常有用的功能,因为使用正则表达式时经常会遇到并没有直接相连,但却是存在联系的部分,分组可以把这些概念上相关的部分“归拢”到一起,以免割裂。下面来看几个例子。
上一章中使用表达式<[^/][^>]*>
来匹配html中的open tag,比如<table>
,但是这个表达式会匹配self-closing tag,比如<br/>
。如果把表达式改写为<[^/][^>]*[^/]>
,却是可以避免self-closing tag,但是因为两个排除型字符组必须匹配两个字符,这个表达式又会放过<u>
这样的单字符open tag,仅仅依赖字符组和量词无法解决这个问题,必须使用到括号的分组功能。
<[^/][^>]*[^/]>
错过的只有一种情况,就是tag name为单个字母的情况。如果tag name不是单个字母,则第一个字母之后必须出现这样一个字符串:其不包含>
,结尾的字符不是/
([^/>]*[^/>]
)。最后才是tag结尾的>
。所以,需要用一个括号将可选出现的部分分组,再用量词?
限定,就可以兼顾这两种情况,准确的匹配open tag了。
例3-5 准确匹配open tag
openTagRegex = re.compile(r'^<[^/>]([^>]*[^/])?>$')
openTagRegex.search("<a>") # True
openTagRegex.search("<table>") # True
openTagRegex.search("<br/>") # False
再来看个更复杂的例子。在web服务中,经常并不希望暴露真正的程序细节,所以用某种模式的url来掩盖。比如/foo/bar_qux.php
,看起来是访问一个php页面,其实完全不是这样。真正的结构是:foo是模块名,bar是控制器名,qux是方法名,三个名字都只能出现小写字母。
希望能处理的情况有三种,其他情况都不予考虑:
只有模块名 | /foo |
---|---|
只有模块名和控制器名 | /foo/bar.php |
模块名,控制器名和方法名同时出现 | /foo/bar_qux.php |
/foo
是必须出现的:之后存在两种可能:/bar.php
或bar_qux.php
。
规则 | 表达式 |
---|---|
/bar.php对应的表达式 | /[a-z]+\.php |
/bar_qux.php对应的表达式 | /[a-z]+_[a-z]+\.php |
再配合量词和分组,最终的表达式为:/[a-z]+(/[a-z]+(_[a-z]+)?\.php)?
。
例3-7 匹配url
regex = re.compile(r'^/[a-z]+(/[a-z]+(_[a-z]+)?\.php)?$')
print(regex.search('/foo'))
print(regex.search('/foo/bar.php'))
print(regex.search('/foo/bar_qux.php'))
print(regex.search('/foo.php')) # None
关扩括号的分组,最好来看email的匹配:email地址以@分为两段,之前的是用户名(username),之后的是主机名(hostname),用户名一般只允许出现数字和字母,主机名则是类似mail.google.com
、163.com
之类的字符串。
用户名由数字字母下划线和点号组成,最大长度为64:[-\w.]{0,64}
。
主机名被点号分割为若干段,每一段叫做域名字段(label)。每个label中可能出现的字符是数字、字母、横线,长度必须在1~63之间。例如:host.net
、sub.host.net
、mail.sub.host.net
。
看来规律是这样的:最后的域名是顶级域名,之前的部分可以看作是某种模式的重复。该模式为由域名字段和点号组成。所以主机名的表达式可以写为([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}
,再加上用户名部分,完整的email正则表达式为:[-\w.]{0,64}@([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}
。
例3-8 email正则表达式
regex = re.compile(r'^[-\w.]{0,64}@([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}$')
print(regex.search('abc@host'))
print(regex.search('abc@host.com'))
print(regex.search('abc123@sub.host.com'))
print(regex.search('abc123@sub.mail.host.com'))
网友评论