本文为原创文章,转载注明出处,欢迎关注网站https://hkvision.cn

历史

正则表达式萌芽于1940年代的神经生理学研究,由著名数学家Stephen Kleene第一个正式描述。具体地说,Kleene归纳了前述的神经生理学研究,在一篇题为《正则集代数》的论文中定义了“正则集”,并在其上定义了一个代数系统,并且引入了一种记号系统来描述正则集,这种记号系统被他称为“正则表达式”。在理论数学的圈子里被研究了几十年之后,1968年,后来发明了UNIX系统的Ken Thompson第一个把正则表达式用于计算机领域,开发了qed和grep两个实用文本处理工具,取得了巨大成功。在此后十几年里,一大批一流计算机科学家和黑客对正则表达式进行了密集的研究和实践。在1980年代早期,UNIX运动的两个中心贝尔实验室和加州大学伯克利分校分别围绕grep工具对正则表达式引擎进行了研究和实现。与之同时,编译器“龙书”的作者Alfred Aho开发了Egrep工具,大大扩展和增强了正则表达式的功能。此后,他又与《C程序设计语言》的作者Brian Kernighan等三人一起发明了流行的awk文本编辑语言。到了1986年,正则表达式迎来了一次飞跃。先是C语言顶级黑客Henry Spencer以源代码形式发布了一个用C语言写成的正则表达式程序库(当时还不叫open source),从而把正则表达式的奥妙带入寻常百姓家,然后是技术怪杰Larry Wall横空出世,发布了Perl语言的第一个版本。自那以后,Perl一直是正则表达式的旗手,可以说,今天正则表达式的标准和地位是由Perl塑造的。Perl 5.x发布以后,正则表达式进入了稳定成熟期,其强大能力已经征服了几乎所有主流语言平台,成为每个专业开发者都必须掌握的基本工具。

以上历史是我抄的一个博客,原文链接在这里

简单的正则表达式

简单来说,正则表达式就是在字符串中按照一定的模式来匹配特定的子串。

简单的正则表达式匹配部分包含两部分内容:匹配项量词, 匹配项是指匹配什么字符,例如a匹配abc,量词是指匹配的次数,例如*代表匹配匹配0次至无穷次。

abc*代表匹配以ab开头的,后面接着0个或以上的c字符的字符串,例如ab,abc,abcc

匹配项

匹配项可以由多种方式组成,总结来说包括三种

  • 基本字符:字符本身
  • 字符集:匹配多个字符,用[]表示,例如[abc]代表匹配a,b,c中的某一个字符
  • 简写字符集:一些常用字符集的简写形式,例如\d代表数字,\w代表所有字母数字和下划线 这三种匹配项可以组合使用
符号含义字符集表示
.除换行符以外的所有字符
\w字母(大小写)数字和下划线[a-zA-Z0-9_]
\d数字[0-9]
\s空白符号[\t\n\f\r\p{Z}]
\b单词边界

所有符号改为大写后是对应的否定,例如\W代表所有非字母数字和下划线

量词

量词代表对应的匹配项可以出现的次数

量词含义
*匹配0次至无穷次
+匹配1次至无穷次
?匹配0次或一次
{m,n}匹配m至n次
{m,}匹配m至无穷次
{,n}匹配0至n次

贪婪与非贪婪

在正则表达式中另外一个常听到的概念就是贪婪与非贪婪模式,这个意思是说在匹配到的内容中是选择尽可能多匹配还是尽可能少匹配,例如abc*代表匹配以ab开头的,后面有c的内容,那么可以匹配到的内容包括ab,abc,abccc,但如果是abc*?,那么代表使用非贪婪匹配,对于abccccabc来说,匹配到的就是ab而不是整个字符串。

abc* -> It will match ab, abc, abccc

abc*? -> It will only match ab, abc, abccc

文首和文尾

^$分别代表文首和文尾,与abc*不同的是^abc*只会匹配出现在最开始的abc,而不会匹配位于中间部分的abc

abc* -> abc is abc

^abc* -> abc is abc

文尾符类似

分组

分组是一组写在圆括号内的子模式 (…)。如果我们把一个量词放在一个字符之后,它会重复前一个字符。 但是,如果我们把量词放在一个分组之后,它会重复整个分组。在分组中,匹配结果除了完整结果外,还包含分组结果。例如(ab)c除了完整匹配到abc外,还会得到ab这个分组。

分支结构

所谓分支结构,就是“或”语句,用|符号表示,例如(a|b)c代表匹配ac或者bc

复杂一点的正则表达式

捕获分组

捕获分组是指引用前面分组的结果,假设我们想匹配下面的结构

1
<p id="123">123</p>

我们可能会这样写正则表达式:<p id="\d*">\d*?</p>

然而这会将这样的内容也提取出来

1
<p id="123">1234</p>

很明显我们需要的是id和html里面的内容一样的结构,上面的正则不能帮助我们完成任务,利用捕获分组则可以。

<p id="(\d*)">\1</p>

这里\1代表引用第一个分组的结果,在正则表达式中,每个分组会从前至后分配一个分组编号,从1开始,你也可以自己命名分组名称,只需要这样写(?P<name>\d*)即可

如果我们的分组不需要被捕获,则可以用以下方式声明此分组为非捕获分组(?:\d*)

零宽度断言

学习这部分的时候我们先把为什么取这个名字的问题抛弃掉,不然你会非常纠结。

这里比较复杂,首先我们来明确匹配项,匹配项与结果有关,例如当正则表达式这样写的时候abc\d*d,匹配的是abc123d这种,而如果我们只需要中间的数字(很多情况是这样的),那么我们没有办法用常规方法来实现,这个时候我们就需要零宽度断言了。

PS:实际上用分组就能实现,是的,笔者已知的零宽度断言就可以用分组来代替,但是相对而言不够方便,并且分组只是替代方案,不够通用

符号表示含义
exp2(?=exp1)匹配后面是exp1的exp2
exp2(?!exp1)匹配后面不是exp1的exp2
(?<=exp1)exp2匹配前面是exp1的exp2
(?<!exp1)exp2匹配前面不是exp1的exp2

这里前面后面,是与不是肯定把你弄糊了,这里再解释一下,重点看字后面的内容,exp2才是被匹配的内容而exp1只是用于限定(或者叫修饰)exp2的表达式。

举个例子:我们想匹配四位数字,对于五位数字则不匹配,三位数也不匹配,你可能会说我用量词就能解决\d{4}?,乍一眼看上去没问题,但是其实是有问题的,这个表达式会匹配12345中的前面四位数1234,而这不符合我的想法,那怎么办呢?那这个时候就可以请出我们的零宽度断言了,正则表达式这样写(?<!\d)\d{4}(?!\d)这里分三部分(?<!\d)\d{4},(?!\d)来看,第一部分代表第一部分的后面部分的前面不能是数字,第二部分代表匹配4个数字,第三部分代表第三部分的前面的后面不能是数字。通俗的来说就是第一部分和第三部分不能是数字,第二部分是4个数字,匹配项是第二部分,第一部分和第三部分都是限定。那么这个表达式就可以很精确的匹配四位数,不会匹配五位及以上的数。

PS: golang官方正则包不支持零宽度断言,而且还不支持捕获分组,只能在替换里面对分组进行引用,简直醉了,不然用捕获分组的方案是可以替代零宽度断言的,目前唯一想到的方案就是用替换把匹配到的子串直接替换成分组,但是这个实现太不优雅了。其他的语言(Perl,Python,JavaScript)都支持这个。

结束

暂时写到这里吧,这里我写一个匹配ipv4地址的正则表达式,比较复杂,但是我相信聪明的你肯定能看懂。

(?<!\d)(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?!\d)