如何在复杂环境匹配字符串。(正则表达式之零宽断言)

334 阅读6分钟

问题的由来

同事问我一个问题:如何在一句文本信息里面分别高亮身份证、电话号码?

他遇到的问题是匹配电话号码的正则会把特定的身份证号码中间的一部分识别为电话号码。

比如 450205197901031042 这个号码,显示为 450205197901031042450205\underline{\text{19790103104}}2

在实际需求中 假设有一段文字 身份证:430626199503068563 联系方式:13521867463,13521867333,职业xxxx

我们需要高亮手机号码,理想效果是这样的:

身份证:430626199503068563联系方式:1352186746313521867333,职业xxxx身份证:430626199503068563 联系方式:\underline{\text{13521867463}},\underline{\text{13521867333}},职业xxxx

但因为使用的不合适的正则表达式/1[3-9]\d{9}/g导致效果是这样的:

身份证:430626199503068563联系方式:1352186746313521867333,职业xxxx身份证:430626\underline{\text{19950306856}}3 联系方式:\underline{\text{13521867463}},\underline{\text{13521867333}},职业xxxx

也就是这个正则会把不是电话号码的身份证中间的一部分识别为电话号码。

这么一来就无法精确匹配手机号码了。

有没有办法只匹配长度为11位数字的手机号呢?答案是是可以。

解决方法

在正则表达式里面有一个叫先行断言后行断言的东西。它们统一称作零宽断言,是一种特殊结构,它们在匹配的时候不会消耗字符,只是对匹配 位置 进行条件判断。这对以一些复杂的模式匹配非常有用,因为它允许你在匹配位置前面或后面添加条件,从而更精准地控制匹配。

正则表达式的先行断言和后行断言一共有 4 种形式:

  • (?=pattern) 零宽正向先行断言(zero-width positive lookahead assertion)
  • (?!pattern) 零宽负向先行断言(zero-width negative lookahead assertion)
  • (?<=pattern) 零宽正向后行断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽负向后行断言(zero-width negative lookbehind assertion)

我们分别来学习下它们:

零宽正向先行断言

假如有字符串 123 123a 123

比如我想匹配 a 前面的这个 123 怎么办呢?

通过观察我们可以知道只要找到a所在的位置然后向前匹配123即可那么我们分两步可以做到

1.找到a这个位置的前面那个位置

2.这个位置左边是不是123 如果是则匹配成功

第一步

零宽正向先行断言?=就可以轻松做到第一步。

?=匹配的是一个位置

?=a匹配的是一个在字符串a前面的位置 因为这个位置在前面所以它叫先行断言。

所以我们可以写的通用一些 ?=[a-zA-Z] 匹配字母前面的位置

然后我们需要将它用()包裹起来因为它是模式匹配(?=[a-zA-Z])

/(?=[a-zA-Z])/它就可以匹配字母前面的位置了第一步就做到了

第二步

匹配这个位置左边是不是123 如果是则匹配成功。

很简单 只需要在这个模式的左边加上123即可 123(?=[a-zA-Z]) 最终结果就是 /123(?=[a-zA-Z])/ 加上全局匹配就是 /123(?=[a-zA-Z])/g

试一下:

image.png

成功。

其实到这里你就已经掌握了这 4 个正则表达式了。

不用记忆的负向和后行断言

(负向就是取反,后行就是换位置,前边换后边)

只需要记住 = 为匹配等于 = 符号后面的元素称之为正向断言。 ! 为 匹配不等于 !符号后面的元素称之为负向断言, 加< 为匹配的位置在匹配元素的后面称之为后行断言, 不加< 匹配的位置在匹配元素的前面称之为先行断言。

举个例子: 假如有字符串 123 a123 123

我们把上面的字符串中a位置放在123的前面,这个时候 找到a后面的123 需要两步

1.找到a这个位置的后面那个位置

2.这个位置右边是不是123 如果是则匹配成功

因为 /(?=[a-zA-Z])/ 匹配的位置在字母的前边 我们容易得到 /(?<=[a-zA-Z])/ 匹配的位置在字母的后边

容易得到表达式 /(?<=[a-zA-Z])123/ 字母后边位置的右边匹配123

试一下:

image.png

成功。

接下来试一下负向匹配:

image.png

image.png

其实就是匹配上面两个剩余的不和 a 在一起的 123 其中两个 a 的位置一个在前边一个后边。

也成功了。

答案

掌握了零宽断言这个技能上面的问题就好解决了,我们只需要匹配出11位数字它的左边和右边都不挨着数字即可。

分为四步:

1、匹配1个电话号码

2、匹配一个位置它的左侧没有数字

3、匹配一个位置它的右侧没有数字

4、这个电话号码在这两个位置的中间

第一步、匹配1个电话号码

我搞快点写简单点 \d{11}

第二步、匹配一个位置它的左侧没有数字

表达式是:(?<!\d) ,接下来让我来看一下它是怎么来的。

匹配数字 \d

匹配不是数字 零宽负向断言 用 ! 正向用 = 所以是 !\d

位置的左侧没有数字其实这个位置就在这个非数字的后面 在后面就加 < 不在后面就不加 < 所以是 <!\d

再加上我们要匹配的未知位置 ? 所以是 ?<!\d

它是一个模式所以最终是 (?<!\d)

第三步、匹配一个位置它的右侧没有数字

表达式是:(?!\d) ,接下来让我来看一下它是怎么来的。

匹配数字 \d

匹配不是数字 零宽负向断言 用 ! 正向用 = 所以是 !\d

位置的右侧没有数字其实这个位置就在这个非数字的前面 所以不加 < 所以是 !\d

再加上我们要匹配的未知位置 ? 所以是 ?!\d

最后得到 (?!\d)

第四步、按顺序拼起来

左边的位置(?<!\d)+电话号码\d{11}+右边的位置 (?!\d)

得到 /(?<!\d)\d{11}(?!\d)/g 我加上了全局匹配符号 g

试一下:

image.png

成功。

至于身份证的高亮接下来就可以交给专门处理身份证高亮的正则拉,这里就不写了。

(完)