一篇文章教你学会【正则表达式】

作者语:不管你学习什么编程语言,都会发现基本上绕不开正则表达式,包括数据库也会有正则表达式,因为正则表达式给我们提供了一个强大的文本查找规则,从而节省大量的编写代码,所以使用优秀的正则表达式往往可以给项目带来质的飞跃,提高我们的开发效率。在这篇博客里,我逐一介绍了大部分你能遇到正则表达式语法,希望可以帮助到大家

什么是正则表达式?

简单来说,正则表达式是一些用来匹配和处理文本的字符串,说白了就是定义查找特定字符串的规则,而正则表达式引擎根据这些规则在给定的文本或者字符串中找出符合规则的文本或者字符串。


普通字符和元字符

在正则表达式里面,字符分为两种类型:没有特殊含义的普通字符和有特殊含义的特殊字符,有特殊含义的字符被称为元字符(metacharacter)

普通字符:
比如:“abc”,在正则表达式里面匹配的结果就是"abc"字符串本身,不会多也不会少!(正则表达式是区分字母大小写的

元字符:
比如". ? + ^ \ [ ]"等等,后面会逐一介绍所有元字符。字符类型的记忆规则为:除了元字符,其它所有字符都是普通字符


第二类元字符

在正则表达式里面分两种元字符:

第一种:本身就是元字符,比如". ^ [ ] \ -"等等,这些字符在正则表达式里面代表着特殊含义,如果想要取消该特殊含义就需要对其进行转义(后面会详细介绍转义的含义

第二种:本身是普通字符,但通过转义变成有特殊含义的元字符,下面列出了各种普通字符通过转义变成的元字符

空白元字符
[\b]回退并删除一个字符(Backspace键)
\f换页符
\n换行符
\r回车符
\t制表符(Tab键)
\v垂直制表符

Tip:在Windows系统中,"\r"+"\n"组合表示文本行结束,在Unix和Linux中只使用一个换行符"\n",所以在Windows中匹配空白行使用"\r\n\r\n"进行搜索,在Unix和Linux中使用"\n\n"来匹配空白行


数字元字符
\d任意一个数字字符(等价于[0-9])
\D任意一个非数字字符(等价于[^0-9])

d 表示 digit(数字)


字母数字元字符
\w任意一个数字字符或者字母字符(包括大小写)或者下划线(等价于[a-zA-Z0-9_])
\W任意一个非数字、非字母(包括大小写)、非下划线的字符(等价于[^a-zA-Z0-9_])

w 表示 word(单词),至于为什么包含下划线,我的理解是编程语言变量名包含了下划线"_"


空白字符元字符
\s任意一个空白字符(等价于[\f\n\r\t\v])
\S任意一个非空白字符(等价于[^\f\n\r\t\v])

s 表示 space(空白)

注意:退格元字符"[\b]" 是一个特例,它既不在"\s"所覆盖的范围内,也没有排除在"\S"的覆盖范围外(也就是在"\S"范围内)


进制元字符
\x表示紧跟在后面的两个数字为十六进制数字
\0表示紧跟在后面的一个数字为八进制数字

\x 只取后面的两个数字作为十六进制数字,比如:"\x041"等价于"\x04"和字符"1"


POSIX字符类
[:alnum:]任意一个字母或数字,不包含下划线(等价于[a-zA-Z0-9])
[:alpha:]任意一个字母(等价于[a-zA-Z])
[:digit:]任意一个数字(等价于[0-9])
[:blank:]空格或者制表符(等价于[ \t],\t前面有个空格)
[:space:]任意一个空白字符,包括空格(等价于[\f\n\r\t\v ],\v后面有个空格)
[:cntrl:]ASCII控制字符(ASCII 0到31,加上 ASCII 127)
[:print:]任意一个可打印字符
[:graph:]和[:print:]一样,但不包括空格
[:lower:]任意一个小写字母(等价于[a-z])
[:upper:]任意一个大写字母(等价于[A-Z])
[:punct:]标点符号(既不属于[:alnum:]也不属于[:cntrl:]的任意一个字符)
[:xdigit:]任意一个十六进制数字(等价于[a-fA-F0-9])

注意:POSIX字符两边的[]是连在一起的,比如:想要表示字母区间则正则表达式需要写成"[[:alpha:]]",外面那层[]表示字符区间,里面那层[]才表示POSIX字符。并不是所有的编程语言都支持这类字符,具体视编程语言而定


字符集合

在正则表达式里,使用元字符"[" 和 "]"来定义一个字符集合,正则表达式引擎将匹配集合里面任意一个字符成员,比如:[0123456789]将匹配"0123456789"中的任意一个字符

注意:"[" 和"]" 不匹配任何字符,它们只负责定义一个字符集合,如果想要匹配"[" 和"]" 字符本身则需要对其进行转义:"\ [" 和 “\ ]”

由于在使用正则表达式的时候,会频繁使用到一些字符区间(0~9、A~Z等),所以,为了简化字符区间的定义,正则表达式提供了一个特殊的元字符"-"(连字符)来表示字符区间,比如:

[0-9]、[a-z]、[A-Z]

分别表示:0到9的所有数字(等价于[0123456789]),小写字母a到z,大写字母A到Z。各种区间可以同时写在一起:[0-9a-zA-Z]

Tip:连字符"-" 是一个特殊的元字符,它只有在集合"[ ]" 中才表示元字符,在[ ]之外表示的是一个普通的字符,因此在集合区间之外"-"不需要转义

如果想要表示不匹配字符集合里面的字符,可以使用取非元字符"^",比如:[^0-9]表示匹配的字符不能是0到9中的任意数字,[^abc]表示匹配的字符不能是"abc"中的任何一个

Tip:元字符"\^" 只有在区间"[ ]" 中才表示取非的含义,如果在区间外面则表示字符串的开头的意思(这个后面会讲到),同时"\^"的效果作用范围为字符集合里的所有字符和字符区间,并不仅限于 "^"字符后面的那一个字符或者字符区间

字符匹配

匹配单个任意字符

元字符"." 在正则表达式里面表示的含义是匹配单个任意字符(除了换行符),如果想要匹配"." 字符本身的话,需要使用另一个元字符"\"(反斜杠)来进行转义

转义的意思是:告诉正则表达式引擎,把后面这个字符当成普通字符看待,所以表达式"\." 表示匹配字符"." 本身,当然"\" 本身也是元字符,所以匹配自身也需要转义"\\"

Tip:在正则表达式里,"\\" 字符永远出现在一个有着特殊含义的字符系列的开头,这个序列可以是一个或者多个字符组成。同时值得注意的是,斜杠"/" 虽然不是元字符,但为了避免不必要的麻烦,在需要匹配 / 字符本身的时候,最好总是使用它的转义序列:"[ \\ / ]"

匹配一个或多个字符

在正则表达式里,使用元字符"+" 表示匹配前面的字符或者字符区间匹配至少一次(不匹配0个字符的情况),比如"a+" 表示匹配一个或多个连续出现的a,"[0-9]+"表示匹配一个或多个连续的数字

注意:字符"+" 只有放在区间外面才有特殊含义,如果放在区间内部"[+]" 则表示匹配"+" 字符本身,相当于区间外部的"\+",不仅如此,像"." 以及后面会讲到的"*" 和"?"这类元字符,放在集合里使用时会被正则表达式引擎解析为普通字符,不需要转义,但转义也不会有什么影响,相反会提高阅读性!

匹配0个或多个字符

在正则表达式里,使用元字符"*“表示匹配前面的字符或者字符区间匹配0次或多次,用法和”+"一样,只是次数没有限制

匹配0个或1个字符

在正则表达式里,元字符"?" 表示匹配"?"前面的字符或者字符区间0次或者1次,比如:“https?//”,? 在这里的含义是:我前面的字符"s"要么不出现,要么最多出现一次,换句话说,https?// 既可以匹配 http// 也可以匹配 https//,并且仅此而已。

回到之前匹配空白行的例子里,windows匹配空白行的正则表达式为:"\r\n\r\n",而unix和linux里的是:"\n\n",所以我们可以通过"?" 来编写解决这个跨系统问题:"[\r]?\n[\r]?\n",这样可以适应两大类系统了,

建议:刚才的正则表达式"[\r]?\n[\r]?\n"里,"\r"我们用了"[\r]",这两个没有区别,只是使用集合更容易理解和阅读,所以这里建议大家在写复杂的正则表达式时,可以使用区间来增强可读性和避免产生误解,可以让人一眼就可以看出哪个字符与哪个字符相关联

匹配的重复次数

为了给匹配的次数给定准确的数字,正则表达式提供了重复次数(interval)的语法,重复的次数用元字符"{" 和"}"来给出,数值写在它们之间。写法有4种类型:

  • {n} 比如:"{3}"表示{3}前面的一个字符或者字符集合必须连续重复出现3次才算是一个匹配,一次都不能多不能少。

  • {n, m} 比如:"{2,5}"表示{2,5}前面的一个字符或者字符集合必须连续重复出现至少2次,最多不超过5次。

  • {n, } 比如:"{2,}"表示前面匹配的字符或者字符集合必须连续重复出现2次,最高次数不限制

  • {, m} 比如:"{,3}"表示前面匹配的字符或者字符集合连续重复出现最多3次,可以是0次、1次、2次或3次


匹配的"贪婪"和"懒惰"模式

什么是匹配的贪婪模式呢?我们举个例子:从下面的文本中匹配出<b>和</b>之间的内容

This is a test of regular expression
<b>a bold text</b> and <b>another bold text</b>

正则表达式为:<[Bb]>.*</[Bb]>

匹配结果为:

This is a test of regular expression
<b>a bold text</b> and <b>another bold text</b>

从中我们可以看到,两个<b></b>之间的所有东西都被一网打尽了,包括不属于之间的都被匹配了,为什么会这样呢?原因是:* 和 + 都是所谓的"贪婪型"元字符,它们在进行匹配的是多多益善而不是适可而止,它们会尽可能从文本的开头一直匹配到文本的结尾而不是碰到第一个匹配时为止。

为了避免这种情况,正则表达式提供了一种"懒惰型"的版本:

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

从中可以看出,只需要在贪婪型元字符加上一个 ? 后缀即可变成懒惰性元字符,所以把例子中的正则表达式改为:"<[Bb]>.*?</[Bb]>",匹配的结果为:

This is a test of regular expression
<b>a bold text</b> and <b>another bold text</b>

明显,改成懒惰型元字符之后得到的结果才是我们想要的结果,所以,为了防止过度匹配,我们需要适当的时候使用懒惰型元字符,具体视情况而定


位置匹配

单词边界

在正则表达式中,由元字符"\b"来匹配单词的开始和结尾,b是单词 boundary 的首字母(注意这里"\b"和空白元字符"[\b]“的区别,在集合”[ ]" 里面的"\b"表示回退并删除一个字符),如果想要匹配一个单词,直接在单词前后加上"\b",比如:

文本:The cat scattered his food all over the room.

匹配 food 正则表达式为:\bfood\b

匹配结果为:The cat scattered his food all over the room.

"\b"的工作原理是这样的:\b匹配一个位置,这个位置位于一个能够用来构成单词的字符(字母、数字和下划线,也就是\w所匹配的字符)和一个不能用来构成单词的字符(与\W匹配的字符)之间。

所以本例查找单词food的正则表达式"\bfood\b",告诉正则表达式引擎:查找"food"字符串,并且只有字符串"food"前后的字符都是不能构成单词的字符(这里是空格),才能算匹配结果。

对应的"\B"表示匹配不是单词边界的位置,比如:

文本:nine-digit and color - coded

正则表达式:\B-\B

匹配结果:nine-digit and color - coded

这里第一个 - 前后都是可以构成单词的字母,所以不匹配,而第二个"-",由于其前后都是不构成单词的空格,所以\B锁定位置在它前后然后返回匹配结果。

字符串边界

正则表达式中,用来定义字符串边界的元字符有两个:"^" 表示字符串的开头,"$"表示字符串的结尾

这里需要注意的是:元字符"^" 只有放在字符集合外面才表示字符串开头,如果放在集合里面"[^]"表示对字符集合取非,即:不匹配字符集合里面的字符或者字符区间。

分行匹配模式

在正则表达式中存在一些可以改变其它元字符行为的元字符序列,比如,用来启用分行匹配模式(multiline mode)的(?m)记号(m表示multiline的意思)。分行匹配模式告诉正则表达式引擎把行分隔符当做一个字符串分隔符来对待

所以在分行匹配模式下,^ 不仅匹配正常的字符串开头,还匹配换行符后面的开始位置(这个位置是不可见的),而 $ 不仅匹配征程字符串的结尾,还匹配换行符后面的结束位置

注意:(?m)必须放在正则表达式的最前面,比如下面的例子将匹配查找出 JavaScript 代码里的所有的注释内容:

文本:

function test(filed) {
    // check value of filed
    if (filed.value == " ") {
        return false;
    }
    // define a variable
    var name = filed.name;
}

正则表达式:(?m)^\s.*//.*$

匹配结果为:

function test(filed) {
    // check value of filed
    if (filed.value == " ") {
        return false;
    }
    // define a variable
    var name = filed.name;
}

表达式"^\s.*//.*$" 从一个字符串的开始匹配,然后是任意个空白字符,然后是//,再往后是任意多个字符,最后是字符串结束,不过这个模式只能找到第一条注释,并认为这条注释将一直延续到文件的末尾,因为 * 是一个"贪婪型"元字符。而加上(?m)之后,"(?m)^\s.*//.*$"将把换行符视为一个字符串分隔符,这样就可以把每一行注释都匹配出来了。


子表达式

子表达式的目的是为了把一些表达式当做一个独立元素来使用。子表达式使用元字符"(" 和")"括起来,当然,作为元字符,如果要匹配本身,就需要使用转义 \ ( 和 \ )

比如,我们想要匹配HTML语言中的非换行空格字符 &nbsp; 并把它连续两次或更多次重复出现的找出来,如果写成 &nbsp;{2, } 是不会得到预期结果的,因为{2, }只作用于紧挨着它的前一个字符,那是一个分号";",所以这个表达式只能匹配像 &nbsp;;; 这样的文本而无法匹配 &nbsp;&nbsp;

Tip:子表达式之间可以自由嵌套,说白了子表达式的作用就是把部分表达式作为整体条件来匹配,所以我们需要把它封装成一个字表达式:(\ ){2, },这样{2, }就会作用于整个 \ 

或操作符

正则表达式中存在一个特殊的操作符:或操作符("|"),它的作用是把表达式分成两个表达式,比如:19|20表示匹配数字19或者数字20,换句话说就是把19和20的数字都找出来,虽然读作或操作符,但其作用却是并列两个条件

注意:19|20{2}表示的不是匹配连续两个19或者连续两个20的文本,而是匹配19或者连续两个20的文本,因为或操作符分割出来的结果是"19"和"20{2}",所以为了表达连续两个19或者连续两个20,可以使用子表达式来界定:(19|20){2},这就是子表达式的好处,使表达式更清晰易读。


回溯引用

回溯引用指的是:表达式后面的部分引用前面部分中子表达式所匹配的结果。比如我们查找HTML闭合标签对:<h2></h2>

首先我们知道标签对除了闭标签多了一个斜杠之外,<>里面的内容要和开标签一致,这个时候可以利用回溯引用了,表达式为:<([Hh]2)>.*?</\1>,\1的意思是引用前面第一个子表达式所匹配的结果,只有当第一个子表达式匹配成功时,\1才会生效

注意:

  • 回溯引用只能引用表达式里面的子表达式,即:使用( )括起来的表达式
  • 回溯引用通常从1开始计数(\1, \2, \3等),而\0一本代表整个正则表达式
  • 不同的编程语言使用的回溯引用语法不同,比如PHP返回一个名为&matchs的数组,&matchs[1]表示第一个匹配,而Java和python返回一个名为group的数组匹配对象

前后查找

在查找文本的时候,存在这种情况:希望从某个标志字符串开始查找,但查找结果又不希望包含这个标志字符串,比如:我们希望查找<h1>和</h1>标签之间的文字,但查找的结果希望不包含这两个标签,只需要它们之间的内容就可以了。

有一种办法就是可以利用子表达式把表达式划分为3部分:开始标签、标签内容,结束标签,然后从匹配的文本中提取我们需要的东西,但是,我们明知道是自己不需要的东西还把它检索出来,岂不是浪费时间和精力。所以这时就需要向前查找和向后查找了。

Tip:在正则表达式里有一个术语叫做:"消费"(consume),用来表示"匹配和返回文本"的含义,被匹配的文本不包含在最终的匹配结果里被称为"不消费"

向前查找(lookahead)

向前查找的意思是:查找指定字符之前的文本。这是一种新的查找模式,这种模式指定了一个必须匹配但不在匹配结果中返回的模式

向前查找模式是一个以"?=" 开头的子表达式,需要匹配的文本跟在"=" 的后面,比如查找网址的协议名:

文本:

http://www.baidu.com
https://www.baidu.com
ftp://ftp.forta.com

正则表达式: .+(?=:)

匹配结果:

http: //www.baidu.com
https: //www.baidu.com
ftp: //ftp.forta.com

这里".+" 表示匹配任意文本,子表达式(?=:)表示匹配":" 但不包含在最终匹配结果中,?= 的作用是告诉正则表达式引擎:只要找到":" 就可以了,但不要把它包括在最终的匹配结果中,意思就是"不消费"它。如果把 ?= 去掉的话,表达式:.+( : )匹配的结果如下:

匹配结果:

http://www.baidu.com
https://www.baidu.com
ftp://ftp.forta.com

显然,子表达式( : )正确地匹配到了":" 并消费了这个字符(出现在了最终的匹配结果中)

Tip:任何一个子表达式都可以转换为一个向前查找表达式,只需给它加上一个 ?= 即可,并且在同一个表达式中可以在任意位置出现多个向前查找表达式

向后查找(lookbehind)

和向前查找相对应,向后查找的意思是:查找指定字符之后的文本,也就是找到出现在被匹配文本之后的字符,其中向后查找操作符为 ?<=

Tip:有些人可能记不住 ?= 和 ?<=,这里给个简单的办法:小于号指向的是左边,表示不要返回左边的字符而是要返回右边的字符,返回右边的字符就是向后查找的意思了。

下面举个例子:查找所有$后面的价格数字

文本:

Apple: $23.45
Banana: $6.77
Peach: $356.78
Pear: $69.96

如果使用正则表达式:$[0-9.]+ 得到的结果将是:

Apple: $23.45
Banana: $6.77
Peach: $356.78
Pear: $69.96

显然这不是我们想要的,所以需要使用向后查找来查找 后 面 的 字 符 , 但 不 要 返 回 后面的字符,但不要返回 ,表达式为:(?<=$)[0-9.]+ 得到的结果为:

Apple: $23.45
Banana: $6.77
Peach: $356.78
Pear: $69.96

好了,现在我们可以结合向前查找和向后查找来解决查找<h1>和</h1>标签之间文字的问题了:

文本:

<body>
<h1>This is a head</h1>
</body>

正则表达式:(?<=<[Hh]1>).*(?=</[Hh]1>)

结果:

<body>
<h1>This is a head</h1>
</body>

Tip:为了减少歧义,我们应该对第一个字表达式的<进行转义:(?<=\<[Hh]1>)

"正负"前后查找

上面介绍的向前查找和向后查找都有一个特点:指定匹配结果的前后必须是哪些文本。这种用法称为正向查找

相应的还存在一种用法:负向查找,表示指定匹配的结果的前后不要包含指定的文本字符。负向查找就是把正向查找中的"=" 换成"!",表示取非:

操作符功能
(?=)正向前查找
(? !)负向前查找
(?<=)正向后查找
(?<!)负向后查找

下面举个例子来说明正负查找的区别:

查找以$开头的数字

文本:

I paid $30 for 100 apples

正向后查找:(?<=$)\d+

结果:

I paid $30 for 100 apples

查找不以$开头的数字

负向后查找:\b(?=!$)\d+\b

结果:

I paid $30 for 100 apples

从例子中可以看出,负向后查找告诉正则表达式引擎:匹配的结果不要那些以$开头的数字,这里如果不给负向后查找两边添加边界元字符的话会导致$30中的0被匹配出来,因为$30的0也是满足负向后查找的结果


条件查找

在实际使用正则表达式查找时,有时我们往往需要根据前面的查找条件来做出相应的不同操作,相当于编程语言里面的条件处理,换句话说就是:如果前面的某个子表达式匹配成功,则执行A表达式,否则执行B表达式。

正则表达式中,条件查找使用的是回溯引用条件,语法为 (?(backreference)true-regex | false-regex),比如:(?(1)[0-9] | [a-z]),这里 ?(1) 表示检查第一个回溯引用是否存在,如果存在则匹配[0-9],如果不存则匹配[a-z]

Tip:我们需要注意回溯引用条件和回溯引用的区别,回溯引用的表达式为:\1,是一个带有转义的数字,表示引用前面的第1个字表达内容,而回溯引用条件的表达式为:?(1),是一个?加上一个不带转义的数字子表达式。

举个例子:北美的电话号码格式支持两种:(123)456-7890 和 123-456-7890,显然,按照正则表达式的思想来理解的话就是,如果匹配的第一个字符是"(" 则连续3数字字符之后的字符应该是")", 否则如果第一个字符是数字的话,那么连续3个数字字符之后应该是"-"字符

查找的正则表达式为:( \ ( )?\d{3}(?(1) \ ) | -)\d{3}-\d{4}

分析表达式:( \ ( )?表示匹配一个可选的左括号,使用括号括起来得到一个子表达式,后面的 \d{3}匹配连续3个数字,(?(1) \ ) | -) 是一个回溯引用条件,意思是,如果第1个子表达式存在(也就是找到了一个左括号),则必须匹配右括号")",否则必须匹配"-",这样就解决了两种号码格式的问题

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值