title: 正则表达式断言
tags: [正则表达式]
date: 2017-11-15 23:55:55
正则表达式大多数结构匹配的文本会出现在最终的匹配结果中,但也有些结构并不真正匹配文本,而只是负责判断某个位置左/右侧是否符合要求,这种结构被称为断言(assertion)。常见的断言有三类: 单词边界、行起始/结束位置、环视。本文主要简单阐述对三类断言的理解。
单词边界
单词边界顾名思义,是指单词字符(\w)能匹配的字符串的左右位置。在javascript、php、python 2、ruby中,单词字符(\w)等同于[0-9a-zA-Z],所以在这些语言中,给定一段文本可以用\b\w+\b
把所有单词提取出来。
// 例如
('Love is composed of a single soul inhabiting two bodies.').match(/\b\w+\b/g)
return ["Love", "is", "composed", "of", "a", "single", "soul", "inhabiting", "two", "bodies"]
这里值得注意的是,有些单词例如e-mail
和组合词I'm
这样的,\b\w+\b
是无法匹配的。如要匹配,可根据需求修改为\b['-\w]\b
单词边界记为\b
,它能匹配的位置:一边是单词字符\w
,一边是非单词字符\W
。
与单词边界对应的是非单词边界\B
,两者关系类似\w
与\W
、\d
与\D
。
这里注意,非单词边界(\B)和单词字符(\w)是不一样的,因为前者是断言,而后者是普通匹配。例如:
// 式一 String(1234567890).replace(/(?=(\B)(\d{3})+$)/g, ',') => 1,234,567,890 // 式二 String(1234567890).replace(/(?=(\w)(\d{3})+$)/g, ',') => ,123,456,7890 // 附加常用例子,20180911格式化为2018-09-11 '20180911'.replace(/(?=\B(\d{2})+$)/g, '-').replace(/-/, '') =>2018-09-11
造成差异的原因就是:
式一中的\B匹配边界(是断言)。第一次匹配时,在
1234567890
中数字1的前方时,会环视后方进行肯定断言(?=
):后方必须是满足两个pattern才通过。第一个pattern(\B)
在数字1的前方匹配成功;故继续在此位置匹配第二个pattern(\d{3})+$
,发现123456789
之后并不是结束符(结束符和开始符也是断言,下文讲述),故匹配失败。开始第二次匹配,从数字1和数字2的中间开始...最后会匹配成功三个位置:1和2之间、4和5之间、7和8之间,再被,
替换,故得到结果。同理,式二在第一次匹配时,在数字1的前方环视后方进行肯定断言:后方必须是满足两个pattern才通过。第一个pattern
(\w)
在数字1的前方匹配成功,并将匹配位置移动到1和2之间;然后继续匹配第二个pattern(\d{3})+$
...第一次匹配成功,故数字1前方的断言是成功的,标记该位置...最后得到三个位置:1前方、3和4之间、6和7之间,再被,
替换,故得到结果。所以
\B
只是去判断该位置左右是否只有一边有单词字符,另一边不是单词字符,且在匹配成功时,不会导致匹配位置发生改变。说起来算是一种判断吧~这种只是匹配某个位置而不是文本的元字符,在正则中也被称为锚点。下文继续介绍常见锚点之二:行起始/结束位置
行起始/结束位置
^
与$
分别表示(行)起始位置和(行)结束位置,比如正则表达式/^lu.*r$/
只能匹配的lu
开始并以r
结束的字符串,例如:luwuer
、lu fd --r
,不能匹配nb luwuer
、lu fd --rb
等。
其实行起始/结束位置断言,常用在正则表达式开启多行模式(Multiline Mode)的情况下。例如:
注:js开启多行模式的方式,在正则表达式后添加附加参数
m
,同全局匹配g
('first line\nsecond line\nlast line').match(/^\w+/gm)
return ["first", "second", "last"]
既然是多行匹配,这里说说如何划分行。
在编辑文本时,敲回车键就向文本输入了行终止符(line terminal),表示结束当前行。这里只需注意,敲入回车时向文本中输入的行终止符在主流平台上是有差别的:
- Windows的行终止符是
\r\n
- UNIX/Linux/Mac OS的行终止符是
\n
不过正则的行起始/结束位置断言都是可以识别的哈~
环视
环视是指在某个位置向左/向右看,保证其左/右位置必须出现某类字符(包括单词字符\w
和非单词字符\W
),且环视也同上两个断言,只是做一个判断(匹配一个位置,本身不匹配任何字符,但又比上两个断言灵活)。也有人称环视为零宽断言。
环视分为四种:
- 肯定顺序环视(正向肯定断言)positive-lookahead:
?=pattern
- 否定顺序环视(正向否定断言)positive-lookahead:
?!pattern
- 肯定逆序环视(反向肯定断言)positive-lookahead:
?<=pattern
,ES2018支持 - 否定逆序环视(反向否定断言)positive-lookahead:
?<!pattern
,ES2018支持
逆序环视兼容性:https://caniuse.com/?search=%20regular%20expressions%20lookbehind
比如我们要匹配一串文字中包含在书名号《》
中的书名,如不考虑环视可能需要如下实现:
('三体是刘慈欣创作的系列长篇科幻小说,由《三体》、《三体Ⅱ·黑暗森林》、《三体Ⅲ·死神永生》组成。').match(/《.*?》/g).join(',').replace(/[《》]/g, '').split(',')
return ["三体", "三体Ⅱ·黑暗森林", "三体Ⅲ·死神永生"]
正则默认是婪模模式(在整个表达式匹配成功的前提下,尽可能多的匹配),开启非贪婪模式(在整个表达式匹配成功的前提下,尽可能少的匹配)的方法:在贪婪量词
{m,n}
、{m,}
、?
、*
、+
后加上一个?
号,例如+?
而在使用环视时会更简单:
('三体是刘慈欣创作的系列长篇科幻小说,由《三体》、《三体Ⅱ·黑暗森林》、《三体Ⅲ·死神永生》组成。').replace(/《/g,'\n').match(/^.*?(?=》)/gm)
return ["三体", "三体Ⅱ·黑暗森林", "三体Ⅲ·死神永生"]
hah,例子没举好,似乎也没简单多少...当然最主要的原因是js不支持逆序环视啦啦啦
再举例,匹配6位数字构成的字符串:
// 无环视
'http://luwuer.com/629212/1234567890'.match(/[^\d]\d{6}[^\d]/g).join('').match(/\d{6}/g)
return ["629212"]
// 环视
'http://luwuer.com/629212/1234567890'.match(/(?!\d).\d{6}(?!\d)/g).join('').match(/\d{6}/g)
return ["629212"]
其实环视在js中更多的是与replace函数组合,就像在单词边界一节中最后的例子。
- 原文 不要误会,就是我写的 /keai
- 参考《正则指引》 - 于晟