本文假设你已知晓正则表达式的基本概念并能进行常规识别和使用,包括但不限于:单字符匹配 .
, 多字符匹配 [ ]
, 元字符使用 \s
, 重复匹配 +*?
等等
匹配空白字符
元字符 | 说明 |
---|---|
[\b] | 回退(并删除)一个字符(Backspace键) |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\v | 垂直制表符 |
\s | 等价于 [\f\n\r\t\v] |
防止过度匹配
对于以下文本
This offer is not available to customers living in <B>AK</B> and <B>HI</B>.
如果想将两个 <B>
标签里的文本匹配出来,使用 <[Bb]>.*</[Bb]>
,会发现结果是
This offer is not available to customers living in ==<B>AK</B> and <B>HI</B>==.
这个模式只找到了一个匹配而不是两个,这是因为 * 和 + 都是 “贪婪型” 元字符,它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾。正确方法是使用这些元字符的 “懒惰型” 版本。
贪婪型元字符 | 懒惰型元字符 |
---|---|
* | *? |
+ | +? |
{n, } | {n, }? |
针对上面的例子,使用 <[Bb]>.*?</[Bb]>
,能得到我们想要的结果:
This offer is not available to customers living in ==<B>AK</B>== and ==<B>HI</B>==.
回溯引用(backreference)
如果我们想匹配 HTML
中的标题,我们可能会想到使用 <[Hh][1-6]>.*?</[Hh][1-6]>
,但是有个问题,如果有以下非法标题:
<H1>This is not valid HTML</H3>
上面的模式也是能够成功匹配到的,这种情况如果不使用回溯匹配则无法解决。回溯引用指在模式的后半部分引用在前半部分中定义的字表达式,在这个例子中,我们使用模式 <Hh>([1-6])>.*?</[Hh]\1>
则可以排出上面的非法标题。
你可以把回溯引用想象成变量,\1
代表模式里的第一个表达式。在上面的例子正确的模式中,([1-6])
是一个只匹配 1~6 的子表达式,\1
只匹配与之相同的数字,这样问题就得到了解决。其实回溯引用大家可能已经见过,$1
就是在进行文本替换时的回溯引用。
大小写转换
回溯引用还有一个使用场景,那就是文本大小写转换。
元字符 | 说明 |
---|---|
\E | 结束 \L 或 \U 转换 |
\l | 把下个字符(或子表达式)转换成小写 |
\L | 把 \L 到 \E 之间的字符全部转换成小写 |
\u | 把下个字符(或子表达式)转换成大写 |
\U | 把 \U 到 \E 之间的字符全部转换成小写 |
举个例子说明下,如果想把一级标题的标题文字转换为大写:
模式:(<[Hh]1)(.*?)(</[Hh]1)
;
替换:$1\U$2\E$3
前后查找(lookaround)
前后查找用于我们需要用正则表达式来标记要匹配的文本的位置的情况。
向前查找(lookahead)
如果我们想在一堆 URL 中拿到它们的协议名
http://www.test.com
https://www.example.com
ftp://ftp.aaa.com
我们可能会使用 .+:
来完成要求,但是该模式匹配的是 http:
,https:
,ftp:
,要提取协议名我们还得对字符串做二次处理。所幸的是,使用向前查找 .+(?=:)
就能够省去后面的冒号,其中子表达式 (?=:)
表示找到 :
就可以了,不把它包括在最终的匹配结果里。
向后查找(lookbehind)
除了 ?=
表示向前查找,还有很多正则表达式(JS不在其中。。。)也支持向后查找,操作符为 ?<=
。同样地,来看一个例子:对于如下文本
ABC01: $23.45
HGG43: $5.31
如果我们想将其中的价格匹配出来(不含 $
),使用 [0-9.]+
是不行的,因为它也会匹配出 01
和 43
,这时候使用向后查找 (?<=\$)[0-9.]+
问题就迎刃而解了。
对前后查找取非(negative lookaround)
前后查找还有一种不常见的用法叫 负前后查找(negative lookaround),负向前查找将向前查找不与给定模式匹配的文本,负向后查找同理。
操作符 | 说明 |
---|---|
(?=) | 正向前查找 |
(?!) | 负向前查找 |
(?<=) | 正向后查找 |
(?<!) | 负向后查找 |
比如下面的文本中我们只想匹配数量而不匹配金额:
I paid $30 for 100 apples,
50 orange, and 60 pears,
I saved $5 on this order.
\b(?<!\$)\d+\b
最终的匹配的结果是只包含那些不以 $
开头的数值。
嵌入条件
北美的电话号码格式是 (123)456-7890
和 123-456-7890
,要匹配该模式,可能很容易就想到使用 \(?\d{3}\)?-?\d{3}-\d{4}
,但是该表达式也会匹配到非法的数据格式比如 (123-456-7890
,这种情况我们就需要使用条件:如果电话号码里有一个 (
,则第五个字符匹配 )
,否则匹配 -
。
嵌入条件的语法为:
(?(backreference)true-regex)
(?(backreference)true-regex|false-regex)
你可以理解为
if (backreference) { true-regex } else { false-regex }
回到电话号码的问题,这时可以解决了
(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}
分析这个模式,其中 (\()?
匹配一个可选的左括号,(?(1)\)|-)
是一个回溯引用条件,只有配对出现的括号才会被匹配。