个人根据《正则指引》内容总结记录,侵删!!
最近看了编译原理方面的书,觉得正则表达式非常重要,在各个语言当中都有支持,所以总结了这篇文章,作为学习总结以及记录~
正则表达式
Regular Expression
即描述某种规则的表达式。
字符组
普通字符组
字符组(Character Class
)是一组字符,表示 “在同一个位置可能出现的各种字符”
其写法是在一对方括号[
和]
之间列出所有可能出现的字符。
1 | #只要字符串中包含数字、字符就可以匹配 |
默认情况下re.search(pattern,string)
只判断string
的某个子串能否由pattern
匹配,为了测试整个string
能否被pattern
匹配,在pattern
两端加上^
和 $
。它们并不匹配任何字符,只表示“定位到字符串的起始位置”和“定位到字符串的结束位置”。
1 | #使用^和$测试string被pattern完整匹配 |
字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不影响,但是并不推荐在字符组中出现重复字符。
例如上例中匹配数字就要把所有数字都列出来还是有些繁琐,为此正则表达式提供了范围表示法(range
),它更直观,能进一步简化字符组。
在字符组中-
表示范围,一般是根据字符对应的码值(Code Point
)也就是字符在对应码表中的编码数值来确定的。小的在前,大的在后,所以[0-9]
正确,而[9-0]
会报错。
在字符组中可以同时并列多个-范围表示法
。
1 | #[0-9a-fA-F]准确判断十六进制字符 |
还可以用转义序列\xhex
来表示一个字符,其中\x
是固定前缀。字符组中有时会出现这种表示法,它可以表现一些难以输入或者难以显示的字符。依靠这种表示法可以很方便的匹配所有的中文字符。
1 | #[\x00-\x7F]准确判断ASCII字符 |
元字符与转义
字符组中的-
并不能匹配横线字符,这类字符叫做元字符。[
、]
、^
、$
都算元字符。
如果-
紧邻字符组中的[
那么它就是普通字符,其他情况都是元字符。
取消特殊含义的做法是在元字符前加上反斜杠\
。
1 | #作为普通字符 |
这段例子中,正则表达式是以字符串的方式传入的,而字符串本身也有关于转义的规定,所以要加两个反斜杠\\
。
针对这种问题Python
提供了原生字符串(Raw String),不需要考虑正则表达式之外的转义(只有双引号是例外,必须转义成\"
)。
1 | #原生字符串的使用 |
请注意,只有开方括号[
需要转义,闭方括号]
不用。
1 | #取消其他元字符的特殊含义 |
排除型字符组
排除型字符组(Negated Character Class)只是在方括号[
之后紧跟一个脱字符`^,所以
[^0-9]表示
0-9`之外的字符,也就是“非数字字符”。
1 | #使用排除型字符组 |
在排除型字符组中,^
是一个元字符,但只有它紧跟在[
之后时才是元字符,如果想表示这个字符组中可以出现^
字符,不要让它紧挨着[
,否则要转义。
1 | #匹配4个字符之一:"0","^","1","2" |
字符组的简记法
字符组间记法(shorthands):对于常用的表示数字字符、小写字母这类字符组提供的简单记法。
常见的有\d
、\w
、\s
,其中\d
等价于[0-9]
,d
代表“数字(digit)”;\w
等价于[0-9a-zA-Z_]
,w
代表“单词(word)”;\s
等价于[ \t\r\n\v\f]
(第一个字符是空格),s
代表“空白字符(space)”。(这些等价前提是采用ASCII匹配规则,采用Unicode匹配规则就不对了)。
1 | #如果没有原声字符串\d就必须写作\\d |
\w
能匹配下划线_
。
1 | #字符组简记法与普通字符组混用 |
相对于\d
、\w
和\s
这三个普通字符组简记法,正则表达式也提供了对应的排除型字符组的简记法:\D
、\W
和\S
——字母完全一样,只是改为大写。
这些简记法匹配字符互补:\s
能匹配的字符,\S
一定不能匹配,其他同理。
1 | #\d和\D |
量词
一般形式
字符组只能匹配单个字符,为此正则表达式提供了量词(quantifier),来支持匹配多个字符的功能。
1 | #重复确定次数的量词 |
量词还可以表示不确定的长度,其通用形式是{m,n}
,其中m
和n
是两个数字(逗号之后绝不能有空格),它限定之前的元素能够出现的次数,m
是下限,n
是上限(均为闭区间)。如果不确定长度的上限,也可以省略,写成\d{m,}
。量词限定一般都有明确的下限,如果没有,则默认为0。有些语言支持{,n}
的记法,省略下限为0的情况,但这种用法并不是所有语言都通用的,最好使用{0,n}
的记法。
量词 | 说明 |
---|---|
{n} | 之前的元素必须出现n次 |
{m,n} | 之前的元素最少出现m次,最多出现n次 |
{m,} | 之前的元素最少出现m次,出现次数无上限 |
{0,n} | 之前的元素可以不出现,也可以出现,最多出现n次(在某些语言中可以写为{,n}) |
1 | #表示不确定长度的量词 |
常用量词
{m,n}
是通用形式的量词,正则表达式还有三个常用量词,分别是+
、?
、*
。它们形态虽然不同于{m,n}
,功能却相同。(可以理解为“量词简记法”)
常用量词 | {m,n}等价形式 | 说明 |
---|---|---|
* | {0,} | 可能出现,也可能不出现,出现次数没有上限 |
+ | {1,} | 至少出现1次,出现次数没有上限 |
? | {0,1} | 至多出现1次,也可能不出现 |
1 | #量词?的应用 |
点号
一般文档都说点号可以匹配“任意字符”,但是换行符\n
不能匹配,如果非要匹配”任意字符”,有两种办法:可以使用单行匹配;或者使用[\s\S]
(也可以使用[\w\W]
、[\d\D]
)。
1 | #点号.的匹配 |
贪婪与懒惰
当使用量词匹配字符串有时会出现意料之外的错误情况。
1 | #字符串的值是"quoted string" |
我们只想匹配"quoted string"
但是下面的语句匹配到了错误的"quoted string" and another"
,这是因为默认的量词匹配采用贪婪规则。就是在拿不准是否要匹配时,先尝试匹配,并且记下这个状态,以备将来”反悔”。这个“反悔”的过程叫做回溯(backtracking)。
1 | #准确匹配双引号字符串,采用懒惰规则 |
贪婪匹配量词 | 懒惰匹配量词 | 限定次数 |
---|---|---|
* | *? | 可能不出现,也可能出现,出现次数没有上限 |
+ | +? | 至少出现1次,出现次数没有上限 |
? | ?? | 至多出现1次,也可能不出现 |
{m,n} | {m,n}? | 出现次数最少为m次,最多为n次 |
{m,} | {m,}? | 出现次数最少为m次,没有上限 |
{,n} | {,n}? | 可能不出现,也可能出现,最多出现n次 |
1 | jsStr = ''' |
转义
各种量词的转义形式
量词 | 转义形式 |
---|---|
{n} | \{n} |
{m,n} | \{m,n} |
{m,} | \{m,} |
{,n} | \{,n} |
* | \* |
+ | \+ |
? | \? |
*? | \*\? |
+? | \+\? |
?? | \?\? |
. | \. |
1 | #忽略转义点号可能导致错误 |
括号
分组
使用括号()
可以将一个字符、字符组或表达式包围起来作为一个整体,再用量词限定它们出现的次数,这种功能叫做分组。
1 | #用括号改变量词的作用元素 |
多选结构
多选结构的形式是(...|…)
,在括号内以竖线|
分隔开多个子表达式,这些表达式也叫多选分支(option);在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就成功了;如果所有子表达式都不能匹配,则整个多选结构匹配失败。
1 | #用多选结构匹配身份证号码 |
多选结构的补充:
第一、多选结构一般会同时使用括号()
和竖线|
;但是没有括号()
,只出现竖线|
,仍然是多选结构。
第二、多选结构并不等于字符组。字符组匹配要比多选结构效率高很多,字符组只能匹配单个字符,多选结构的每个分支长度没有限制。
第三、多选结构应当避免某段文字可以被多个分支同时匹配的情况,这将大大增加回溯的计算量,影响效率。如果遇到多个分支都能匹配的字符串,大多数语言优先匹配左侧分支。
1 | #多选结构的匹配顺序 |
引用分组
使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过group(num)之类的方法”引用”分组在匹配时捕获的内容。其中num表示对应括号的编号,无论括号如何嵌套,分组编号都是根据开括号出现的顺序来计数的;开括号是从左到右数起第多少个开括号,整个括号分组的编号就是多少。编号从1开始计数,不过也有0号分组,它是默认存在的,对应整个表达式匹配的文本。
1 | #引用捕获分组 |
容易错误的情况:
1 | #容易弄错的分组的结构 |
这个表达式中编号为1的括号是(\d)
,其中\d
是“匹配一个数字字符“的子表达式,因为之后有量词{4}
,所以整个括号作为单个元素,要重复4次,而且编号都是1;于是每重复一次,就要更新一次匹配结果。所以在匹配过程中,编号为1的分组匹配文本的值,依次是2
、0
、1
、0
,最后的结果是0。
1 | #正则表达式的替换 |
反向引用
反向引用(back-reference)它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是左侧),其形式也是\num,其中num表示所引用分组的编号,编号规则与之前介绍的相同。
1 | #用反向引用匹配重复字符 |
反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用的是由之前表达式决定的具体文本,而不是符合某种规则的位置文本。
1 | #匹配IP地址的正则表达式 |
各种引用的记法
语言 | 表达式中的反向引用 | 替换中的反向引用 |
---|---|---|
.NET | \num | $num |
Java | \num | $num |
JavaScript | $num | $num |
PHP | \num | \num或$num(PHP4.0.4以上版本) |
Python | \num | \num |
Ruby | \num | \num |
一般来说,$num要好于\num。原因在于,\$0可以准确表示“第0个分组”,而\0则不行,因为不少语言的字符串中,\num本身是一个有意义的转义序列,它表示值为num的ASCII字符,所以\0会被解释为“ASCII编码为0的字符”。但是反向引用不存在这个问题,因为不能在正则表达式还没匹配结束时,就用\0引用整个表达式匹配的文本。
但是无论是\num还是$num,都有可能遇到二义性的问题:如果出现了\10(或者$10),它到底是表示第10个捕获分组,还是第1个捕获分组之后跟着一个字符0?
Python将\10解释成“第10个捕获分组匹配的文本”,如果想表示第1个分组之后跟一个0,需要消除二义性。
1 | #使用g<n>消除二义性 |
Python和PHP的规定明确,所以避免了\num的二义性;Java、Ruby、Javascript这样规定\num,如果一位数,则引用对应的捕获分组;如果是两位数且存在对应的捕获分组时,引用对应的捕获分组,如果不存在则引用一位数编号的捕获分组。这样如果存在编号为10的捕获分组,无法用\10表示“编号为1的捕获分组和字符0”,如果在开发中遇到这个问题,现有规则下无解,但可以使用明明分组解决此问题。
命名分组
为了解决捕获分组数字编号不够直观和会引起冲突的问题,一些语言提供了命名分组(named grouping)。
在Python中用(?P<name>regex)
来分组,其中的name是赋予这个分组的名字,regex则是分组内的正则表达式。
1 | #命名分组捕获 |
不同语言中命名分组的记法
语言 | 分组记法 | 表达式中的引用记法 | 替换时的引用记法 |
---|---|---|---|
.NET | (? |
\k |
${name} |
Java7开始支持 | (? |
\k |
${name} |
PHP | (?P |
(?P=name) | 不支持,只能使用\${num},其中num 为对应分组的数字编号 |
Python | (?P |
(?P=name) | \g |
Ruby | (? |
\k |
\k |
非捕获分组
在使用分组时,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果不需要引用,保存这些信息无疑会影响正则表达式的性能;如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能。
为解决这种问题,提供了非捕获分组(non-capturing group),非捕获分组类似普通分组,只是在开括号后紧跟一个问号和冒号(?:...)
,这样的括号叫做非捕获型括号。在引用分组时,分组的编号同样会按开括号的顺序从左到右递增,只不过必须以捕获分组为准,非捕获分组会掠过。
1 | re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(1) |
转义
括号的转义必须转义与括号有关的所有元字符包括(
、)
和|
。因为括号非常重要,所以无论时开括号还是闭括号,只要出现,正则表达式就会尝试寻找整个括号,如果只转义了开括号而没有转义闭括号,一般会报告”括号不匹配”的错误。另外,多选结构中的|
也必须转义。
1 | #括号的转义 |
断言
正则表达式中的大多数的结构匹配的文本会出现在最终的匹配结果中,但是有些结构并不真正匹配文本,而只负责判断某个位置左/右侧的文本是否符合要求,这种结构被称为断言(assertion)。常见的断言有三类:单词边界、行起始/结束位置、环视。
待补充
个人根据《正则指引》内容总结记录,侵删!!