正则表达式用法

2021/11/20·3 views

引子

上一章分享了正式表达式的入门知识,以及单字符、多字符常用的匹配方法,对于工作维护过程中已经够用,但是有时候只使用基础知识来实现就会比较麻烦,如果使用高级用法就会比较方便很多。
例如:匹配一个HTML文件中两个<B>标签中的文件。
文本内容:

xml 复制代码
This offer is not available to customers living in <B>AK</B> and <B>HI</B>

从上一章内容的知识可以想到的表达式可能如下:

xml 复制代码
<[Bb]>.*</[Bb]>

但是这个表达式配置的结果是AK</B> and <B>HI,而不是我们想要的AKHI

懒惰型匹配

引子中的例子中的匹配方式是属于贪婪行为,就是尽可能多的匹配内容,像例子中第一个和最后一个中间都被匹配到了,而不管匹配内容中是否存在。
上一章中讲到的*和+、{m,}都是所谓的“贪婪型”的。在这一节中讲一下与“贪婪型”相反的“懒惰型”,就是匹配尽可能少的内容。

实现很简单,就是在原有“贪婪型”元字符后面加上一个? 号,如下表格

贪婪型元字符 懒惰型元字符
* *?
+ +?
{m,} {m,}?

位置匹配

在现实的系统中一般表达位置的地方就是一个单词的开头以及结尾或者一个字符串的开头或者结尾。
注意这个边界只是一个位置,例如单词边界匹配的是\w\W之间的一个位置

元字符 说明 注意
\b 单词边界,单词的开头或者单词的结尾 回退键的元字符是[\b]
\B 表示非单词边界
^ 字符串的开头位置 放在[]中表示取非操作
$ 字符串结尾位置

扩展:

  • 像egrep中也支持使用\<匹配单词开头位置,使用\>匹配单词结尾位置,但是支持这种元字符的编辑器比较少。

  • (?m)是一个分行匹配模式的记号,放在一个表达式的最前面,会改变字符串位置匹配的行为。^不仅匹配正常的字符串开头还匹配行分隔符(换行符)后面的开始位置;同样$不仅匹配正常的字符串结尾还匹配行分隔符(换行符)后面的结束位置;此用法只有部分正则表达式会支持

选项 描述 支持平台
(?d) Unix中的行 java
(?i) 不区分大小写 PCRE Perl java
(?J) 允许重复的名字 PCRE*
(?m) 多行 PCRE Perl java
(?s) 单行 PCRE Perl java
(?u) Unicode java
(?U) 默认最短匹配,与懒惰型匹配类似 PCRE
(?x) 忽略空格和注释 PCRE Perl Java
(?-...) 复原或关闭选项 PCRE

捕获分组与后向引用

前面的元字符都是对紧挨着前面的一个字符有效,例如表达式the{3}匹配theee字符串,假如我们想匹配连续三个the字符串怎么办呢,这就涉及到子表达式的概念。

子表达式

把一个表达式匹配的内容做为一个单独的元素嵌入到另外一个表达式中,那这个做为独立元素的表达式就是子表达式,需要使用()括起来。这个跟数学的表达式概念很类似。
并且子表达与数学表达式还有一个类似的地方就是,正则表达式的子表达式也可以嵌套使用

本节开头说的那个问题就可以使用子表达式来实现,(the){3}就会匹配thethethe这个字符串。

假如我们再加个条件:我们想匹配连续三个the或者连续三个you,怎么实现?这就是正则表达式的选择操作符,也叫或操作符了


元字符 说明
` ` 或操作符,两边的表达式都是一个独立的元素,一般放在()中使用

上面的问题就可以使用正则表达式(the|you){3}来表示

捕获分组与后向引用

当一个模式的全部或者部分内容由一对括号括起来时,就对表达式进行了分组(其实就是放在()中的子表达式),并且把分组匹配到内容捕获并且临时存放在内存中。这就是捕获分组,可以在后面表达式中使用就叫后向引用,或者叫回溯引用。
默认情况下,分组是从左到右依次排序从1编号,第一个分组就是1,第二个分组就是2等等。

最开始的时候支持的编号范围是1到9,现在应该已经没有这种限制了。

后向引用很简单就是一个\或者$后面跟相应编号即可。例如\1或者$1就表示引用第一个捕获分组。

命名分组

前面讲捕获分组都是通过位置编号来访问,在perl和python、.NET等语言中还支持对捕获分组命名。这样就比较容易理解

命名语法 描述
(?<name>分组) 命名分组
(?P<name>分组) python中的命名分组
\k<name> Perl中引用命名分组
\k'name' Perl中引用命名分组
\g{name} Perl中引用命名分组
\k{name} .NET中引用命名分组
(?P=name) Python中引用命名分组

例:

复制代码
preg_match('/(?P<name>\d+)/', $string, $matched)

如果匹配到,那么name的值就是\d+匹配到的值

非捕获分组

顾名思义,与捕获分组相反,就是不会将分组匹配的内容放在内存中。主要是为了提高性能。
使用方法:在分组的开头加上?:,例如(?:the)

当把非捕获分组语法中的:换成>时,就变成了原子分组(另一种非捕获分组),可以进一步提升性能。因为原子分组会将分组内部的回溯操作关闭。

环视

环视是一种非捕获分组,它根据某个模式之前或者之后的内容要求匹配其他模式。环视也称为零宽度断言。

环视分类 说明 举例
(?=分组) 正前瞻,匹配且要求紧随其后内容为分组匹配的内容 a(?=b),匹配a并且后面坚接着是b的字符串,可以匹配abc但是不匹配acb
(?!分组) 反前瞻,即对正前瞻含义取反,匹配且要求紧随其后内容不为分组匹配的内容 a(?!b),匹配a并且后面坚接着不是b的字符串,可以匹配acb但是不匹配abc
(?<=分组) 正后顾,即对正前瞻方向取反,匹配且要求紧挨着之前的内容为分组匹配的内容 (?<=a)b),匹配b并且前面紧挨着是a的字符串,可以匹配abc但是不匹配cbc
(?<!分组) 反后顾,即对正后顾含义取反,匹配且要求紧挨着之前的内容不为分组匹配的内容 (?<!a)b),匹配b并且前面紧挨着不是a的字符串,可以匹配cbc但是不匹配abc

PHP官方文档:https://www.php.net/manual/zh/book.pcre.php

写个可以匹配一下各种特殊字符的正则表达式

复制代码
((?=[\x21-\x7e]+)[^A-Za-z0-9])
x21-\x7e]+)[^A-Za-z0-9])

这个匹配所有键盘上可见的非字母和数字的符号

复制代码
var patrn = /[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;&'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘&',。、]/im; 
复制代码
if (!patrn.test(str)) 
{
	// 如果包含特殊字符返回false
    return false;
}
return true;

废话少说,代码如下

复制代码
[^\\S\\r\\n]

\ s用于查找空白字符。

空白字符可以是:空格字符,制表符,回车符,换行符,垂直制表符,换页字符,\S是\s的取反。

解读:判断这个字符,首先 不是 非空白字符(也就是空白字符),然后不是回车符和换行符。

[]结构^开头,那么三个转义字符都是 非 判断。

PHP正则中的后向引用

php 复制代码
echo preg_replace(&'/(test)(date)/&', &'${1}&', &'testdate&');

输出testtest

其中的${1} 也可以写作\1或者\\1,表示第一个元组。

正则表达式匹配"非",以及"非"字符串的匹配

首先,正则表达式的"非",代表不想匹配某个字符。
比如字符串 helloword123,/[^0-9]+/g 可以匹配非数字,即匹配结果为 helloword;

同样的,/[^he]+/g 可以匹配非h非e的字符,匹配结果为lloword123;

那么 /[^hello]/g 呢?乍一看可能会以为能匹配word123,其实不然,[^] 内的多个字符是"或"的关系存在的,即它们并不是一个整体,/[^hello]/g 表示 非h非e非l非o,并不能理解为 非(hello),所以匹配结果是 w 和 rd123。

道理我们都懂,可我们就是想匹配非某个字符串呢?比如某一字符串若是含有hello则无匹配,若是不含hello则匹配,写成[hello]是显然不行的,[(hello)] 呢?其实不起作用。

这时我们需要用到正则表达式的断言——(?!pattern) 零宽负向先行断言 或者 (?<!pattern) 零宽负向后行断言 均可。

这里只介绍一种写法,大家可以都去尝试一下。

复制代码
/^((?!hello).)+$/

由于断言 (?!hello)是不占位的,后跟的 . 在原位置匹配任意字符,再用括号将其括起来,用+重复一次或多次,前后加上^和$,若是字符串中存在hello,则匹配到h字符之前的时候,断言(?!hello)匹配失败,正则匹配结果为false, 若是字符串中不存在hello,则匹配结果是整个字符串。

#用法实战

##匹配&和;之间不含有test的字符

复制代码
str = "hello&nbsp;&test1;test"";

正则表达式:/&((?!test).)+;/g

匹配结果:&nbsp;"

匹配不含有<img>标签的<div></div>标签

复制代码
str = "<div id='1'><img class='xx'></div><div id='1'><input type=''text"></div>";

正则表达式: /<div[^>]*>((?!<img[^>]*>).)+</div>/g

匹配结果:<div id='1'><input type=''text"></div>

正则表达式匹配次数

正则表达式里匹配次数的元字符有:
(一) {0,1}{0,}{1,}{3} 逗号前面次数到逗号后面次数
(二) ??? 0次或1次
(三) * 0次以上,即0n
(四)
+ 1次以上,即1n

下面深入讲解下这几个元字符的意义:

{0,1}{0,}{1,}{3} 逗号前面次数到逗号后面次数

上面列了几种都是{}用于表示次数时常见的写法。

  • {0,1} 匹配0次或1次

  • {0,} 匹配0次以上,逗号后面为空表示无限次的意思

  • {1,} 匹配1次以上

  • {3} 匹配3次,它是{3,3}的简写形式

需要注意的是,前面数字大于后面数字是会报错的

有时候会看到[0-9]{1}这样的写法,其实这样写也算是多余的,它等价[0-9],正则里没有添加量词修饰的匹配都是按1次算。

??? 0次或1次

? 匹配0次或1次,可以理解为{0,1}的简写,常见用法有:

  • https? 用于匹配httphttps

  • (https?://)? 用于匹配域名前缀https?://可有可无的情况。

另外,需要了解的是,?是匹配优先的,这是什么意思呢?
所谓匹配优先,就是字面意思,能匹配我就先匹配。
下面举例说明一下:
源码:www.zjmainstay.cn
正则:^(www\.)?(.+)$
匹配结果是:分组1得到了www.,分组2得到了zjmainstay.cn
从这里可以看出,对于(www\.)?部分的正则,它先进行了匹配,然后.+部分再匹配。
那么,如果我们想要把www.留给后面的.+去匹配怎么办?
我们需要找到一个非匹配优先的方法,对于非匹配优先,接触过非贪婪模式的读者可能马上想到非贪婪模式,不错,利用非贪婪模式可以解决这个问题:
正则:^(www\.)*?(.+)$
当然,使用正则:^(www\.){0,1}?(.+)$也是可以的,而且这个更接近原本的意思。
另外,很多人可能没见过,其实我们还能用:^(www\.)??(.+)$实现。这里的???的非匹配优先版本。

* 0次以上,即0~n

* 0次或任意次,可以理解为{0,}的简写,常见用法有:

  • .* 贪婪模式,匹配优先,匹配除换行外的任意字符0次以上

  • .*? 非贪婪模式,非匹配优先,匹配除换行外的任意字符0次以上

  • <a href="/[^"]*"href="/"的双引号中间,匹配非"的字符0次以上

大家可能发现,.*? 这里也有?,但是它已经脱离了前面对?介绍的0次或1次的范围,没错,只是为了让大家不至于混淆,我没有把它归入上面的?当中。
我们可以理解为,?只要跟本文所说的几个计次量词结合,就形成非贪婪模式。甚至,连??你都可以认为是结合后的0次或1次的非贪婪模式,反正也能解释通不是?

+ 1次以上,即1~n

+* 除了匹配次数意义上有所差别,常见用法都类似,它强调的是至少匹配1次以上,可以理解为{1,}的简写。

  • .+ 贪婪模式,匹配优先,匹配除换行外的任意字符1次以上

  • .+? 非贪婪模式,非匹配优先,匹配除换行外的任意字符1次以上

  • <a href="/[^"]+"href="/"的双引号中间,匹配非"的字符1次以上

总结

量词本身使用时,都是贪婪模式匹配,而量词和?能结合成非贪婪模式匹配。
贪婪模式匹配就是尽可能多地匹配,非贪婪模式匹配就是尽可能少地匹配。

原文地址: https://www.cnblogs.com/tsql/p/6386210.html

正则表达式断言 - (?!), (?:), (?=)

一个断言就是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符。简单的断言代码有\b、\B、 \A、 \Z、\z、 ^、$ 等,在 转义序列(反斜线) 中有描述。 更加复杂的断言以子组的方式编码。 它有两种类型: 前瞻断言(从当前位置向前测试)和后瞻断言(从当前位置向后测试)。

一个断言子组的匹配还是通过普通方式进行的, 不同在于它不会导致当前的匹配点发生改变。 前瞻断言中的正面断言(断言此匹配为真)以 ”(?=” 开始,消极断言以 ”(?!” 开头。比如, \w+(?=;) 匹配一个单词紧跟着一个分号但是匹配结果不会包含分号, foo(?!bar) 匹配所有后面没有紧跟 ”bar” 的 ”foo” 字符串。 注意一个类似的模式 (?!foo)bar 它不能用于查找之前出现所有不是 ”foo” 的 ”bar” 匹配, 它会查找到任意的 ”bar” 出现的情况, 因为 (?!foo) 这个断言在接下来三个字符时 ”bar” 的时候是永远都 true 的。 前瞻断言需要达到的就是这样的效果。

后瞻断言中的正面断言以”(?<=”开始, 消极断言以”(?<!”开始。比如, (?<!foo)bar 用于查找任何前面不是 ”foo” 的 ”bar”。 后瞻断言的内容被严格限制为只能用于匹配定长字符串。但是,如果有多个可选分支, 它们不需要拥有相同的长度。比如 (?<=bullock|donkey) 是允许的, 但是 (?<!dogs?|cats?) 将会引发一个编译期的错误。在最上级分支可以匹配不同长度的字符串是允许的。 相比较于 perl 5.005 而言,它会要求多个分支使用相同长度的字符串匹配。 (?<=ab(c|de)) 这样的断言是不允许的, 因为它单个的顶级分支可以匹配两个不同的长度, 但是它可以接受使用两个顶级分支的写法 (?<=abc|abde) 这样的断言实现, 对于每个可选分支,暂时将当前位置移动到尝试匹配的当前位置之前的固定宽度处。 如果在当前没有足够的字符就视为匹配失败。后瞻断言与一次性子组结合使用可以用来匹配字符串结尾; 一个例子就是在一次性子组上给出字符串结尾。

多个断言(任意顺序)可以同时出现。 比如 (?<=\d{3})(?<!999)foo 匹配前面有三个数字但不是 ”999” 的字符串 ”foo”。注意, 每个断言独立应用到对目标字符串该点的匹配。 首先它会检查前面的三位都是数字, 然后检查这三位不是 ”999”。 这个模式不能匹配 ”foo” 前面有三位数字然后紧跟 3 位非 999 共 6 个字符的字符串,比如, 它不匹配 ”123abcfoo”。 匹配 ”123abcfoo” 这个字符串的模式可以是 (?<=\d{3}…)(?<!999)foo。

这种情况下,第一个断言查看(当前匹配点)前面的 6 个字符,检查前三个是数字, 然后第二个断言检查(当前匹配点)前三个字符不是 ”999”。

断言可以以任意复杂度嵌套。 比如 (?<=(?<!foo)bar)baz 匹配前面有 ”bar” 但是 ”bar” 前面没有 ”foo” 的 ”baz”。 另外一个模式 (?<=\d{3}...(?<!999))foo 则匹配前面有三个数字字符紧跟 3 个不是 999 的任意字符的 ”foo”。

断言子组时非捕获子组,并且不能用量词修饰, 因为对同一件事做多次断言是没有意义的.如果所有的断言都包含一个捕获子组, 那么为了在整个模式中捕获子组计数的目的,它们都会被计算在内。然而, 子字符串的捕获仅可以用于正面断言,因为对于消极的断言是没有意义的。

将断言计算在内,可以拥有的最大子组数量是 200 个。

以上来自: https://www.php.net/manual/zh/regexp.reference.assertions.php

复制代码
(?:pattern) 

非获取匹配,匹配pattern但不获取匹配结果,不进行存储供以后使用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。

复制代码
(?=pattern)

非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

复制代码
(?!pattern)

非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。
(?<=pattern)
非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。

复制代码
(?<!pattern)

非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。这个地方不正确,有问题

复制代码
(?<=1)test

匹配前面不是1的test

复制代码
// 前瞻:
exp1(?=exp2) 查找exp2前面的exp1
// 后顾:
(?<=exp2)exp1 查找exp2后面的exp1
// 负前瞻:
exp1(?!exp2) 查找后面不是exp2的exp1
// 负后顾:
(?<!exp2)exp1 查找前面不是exp2的exp1

举例:

复制代码
"中国人".replace(/(?<=中国)人/, "rr") // 匹配中国人中的人,将其替换为rr,结果为 中国rr
"法国人".replace(/(?<=中国)人/, "rr") // 结果为 法国人,因为人前面不是中国,所以无法匹配到

要理解?:则需要理解捕获分组和非捕获分组的概念:

复制代码
()表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容)
(?:)表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来

举例:

复制代码
// 数字格式化 1,123,000
"1234567890".replace(/\B(?=(?:\d{3})+(?!\d))/g,",") // 结果:1,234,567,890,匹配的是后面是3*n个数字的非单词边界(\B)

参考文档

正则高级用法

递归引用

(?-n): 这是一个递归引用的语法,用于在正则表达式中引用之前的捕获组。n 表示要引用的捕获组的编号。
(?-n) 表示引用之前的第 n 个捕获组,并在当前位置再次应用它。这通常用于处理嵌套结构。

反向引用

\n: 这是反向引用的语法,用于在正则表达式中引用之前的捕获组。\n 表示引用第 n 个捕获组的内容,其中 n 是捕获组的编号。这通常用于在正则表达式中重用先前匹配的文本。反向引用支持数字计算, 例如:\1\2, ...: 可以在正则表达式中进行反向引用,但有些正则引擎支持在引用中使用计算,例如\1+1

零宽断言(Zero-Width Assertions):

  • (?=...): 正向先行断言,表示在某个位置之前的文本必须匹配括号内的模式。

  • (?<=...): 正向后行断言,表示在某个位置之后的文本必须匹配括号内的模式。

  • (?!...): 负向先行断言,表示在某个位置之前的文本必须不匹配括号内的模式。

  • (?<!...): 负向后行断言,表示在某个位置之后的文本必须不匹配括号内的模式。

非捕获组(Non-Capturing Groups):

  • (?:...): 创建一个非捕获组,用于分组但不捕获匹配的文本。

零宽度断言的重叠匹配:

  • (?=(...)): 允许零宽度断言在重叠的地方匹配。
    递归匹配:

  • (?R): 在正则表达式中进行递归匹配。

命名捕获组:

  • (?<name>...) 或 (?'name'...): 允许给捕获组命名,而不仅仅使用编号。