2019独角兽企业重金招聘Python工程师标准>>>
正则表达式(Regular Expression,下文简称为Regular或正则)是开发中一个不可多得的利器,它广泛应用于字符串的查找、匹配以及替换等场景。以其简短的表现形式和高效的查找匹配效率赢得众多程序员的喜爱。本文旨在帮助大家入门正则并学会解决常见的正则问题,希望能帮到大家。
一. 揭开正则表达式的神秘面纱
1. 正则给人的直观印象
很多人觉得Regular很难,一般有两种情况:第一种是确实看的比较深入,这种大神太少了,至少我现在只认识了一个。另外一种情况就是被Regular那迷人的表达形式吓到了。本文主要是针对第二种人,我想说的是Regular真的不难,最起码学会初级和中级的应用不难。
2. 一个常见的正则小应用
相信很多人应该碰到过“检测用户输入的手机号或者邮箱是否合法”这种需求。这种例子用正则来做最合适不过了。比如下面的正则就可以判断一个邮箱是否合法。
^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$
我记得学正则之前看到这个表达方式后还是很迷惑的,什么鬼啊,完全看不出和邮箱有什么联系。不过现在看着就感觉很简单了,都是一些基础的正则符号,相信大家看完本文后,再回头看这个表达式会有豁然开朗的感觉。
二. 走进正则表达式的世界
上面说了那么多,目的是为了让大家对正则有个初步的概念。下面主要说明正则的基本语法。看完这部分之后,就能很轻松地看懂上面的那个匹配邮箱地址的正则了。
1. 元字符
元字符其实就是正则中的保留字符,这些字符在正则表达式中有着自己特殊的含义。就像Java中的class和interface关键字一样,他们不是普通的字符串,有着自己特殊的含义。
- 脱字节符:^
意思:代表一行文本的开头 用处:当我们想从一行文本的开头处匹配时,那么这个字符是个很好的选择。
- 美元符号:$
意思:代表一行文本文本的结尾 用处:当我们想匹配到一行文本的结尾时,那么这个字符是个很好的选择。
- 单词分界符:\b
意思:代表一个单词的开始或者结束 用处:当我们想匹配字符串中的某一个单词时,可以用这个符号匹配单词的开始和结束的位置
- 取非符号:^
意思:用在字符串组(下面会讲到)中,代表“非”的意思。 用处:这个符号和脱字节符号是同一个字符,只是用在不同的地方表示不同的意思,下面的字符组的例子我们会用到它。
- 点号通配符:.
意思:你没看错,这个小圆点,代指任何一个字符。 (除了换行符) 用处:当我们对某个字符没有任何要求时,可以用它通配任意一个字符。
2. 量词的三个分类
上面提到了一些基础的元字符,一般匹配某一个或某一类字符。下面介绍一下三个量词字符‘*’‘+’‘?’。它们用来修饰基本的正则表达式,表示正则的匹配次数。
分类 |
匹配次数 |
---|---|
* |
匹配零次或者多次 |
+ |
最少匹配一次,可以匹配多次 |
? |
匹配零次,或者匹配一次 |
比如,一个用来匹配单词的基本的正则表达式:
\b\w\w\b //匹配具有两个字母的单词。
那么,很显然,上面的正则只能匹配只有两个字符的单词,但是我们的目的是匹配所有的单词,那么该怎么搞呢,我们也没法确定这个单词到底有多少个字符。
这里就会用到量词了,如下:
\b\w\w*\b //这个正则和上面的那个比,只多了一个字符‘*’意义就完全变了。它表示“有任意多个\w”,着正好符号要求。
上面的例子中多了一个‘’,意思是,符号‘’前面的那个字符,出现零次或者多次。
当然,我们也可以改成下面的写法:
\b\w+\b //这个正则和上面的那个表达的意思一样。
为什么可以这样呢,因为“一个单词最少有一个字母”。这里之所以可以这么简单,是因为‘+’最少匹配一次,所以,被这个正则匹配的字符一定最少有一个字母。符合要求。
然而,这还不够,还有一种情况我们没有考虑。
假设我们想把下面的HTML代码中的第一个div标签的内容过滤出来,该怎么搞呢
第一个DIV第二个DIV
你可能会想到用:
.*
这个其实不行,它匹配的结果是:
第一个DIV第二个DIV
这明显不是我们想要的结果。也就是说‘*’可以匹配零个字符和多个字符,但是,当有多种合适的匹配结果时,其总是优先匹配字符最多的结果。
这就尴尬了。。。
怎么搞,怎么让‘*’匹配第一个,而不是同时匹配两个呢。
这就要再学习一个新知识了:
正则表达式的匹配模式有三种,分别是:贪婪模式(最多匹配模式),勉强模式(最少匹配模式)和占有模式。正则默认使用的是贪婪模式。
分类 |
量词 |
特性 |
---|---|---|
匹配优先量词 |
* + ? |
尽可能多的匹配 |
忽略优先量词 |
*? +? ?? |
尽可能少的匹配 |
占有优先量词 |
*+ ++ ?+ |
类似于匹配优先,但一旦匹配就不会退还,类似于“固化分组” |
通过上面的表格,我们可以知道,可以用‘’的勉强模式‘?’就可以达到效果。
所以,正确的Regular应该是:
.*?
学会了‘*’的勉强模式,那么‘+’和‘?’也就同理了。
3. 字符组
字符组是正则中一个很重要的概念。字符组匹配的是单个字符,这个字符可以是字符组中列出的任意一个字符。字符组的表现形式为:[....]。
当我们想匹配的某一个字符不是固定的,比如,我们想匹配一段文字中所有的数字,也就是说要匹配所有的0~9这十个字符。这时我们就需要用到字符组这个概念。
关于字符组其实很简单,这里举两个小例子。
记得以前看过一句话“一篇议论文中提到的数字概念越多,就越有说服力”。暂且不去考证这句话的真假。假设我们现在有一篇文章,需要找出文中所有的数字并统计数字的个数。那么我们该怎样用正则过滤出所有的数字呢。 首先,我们可以这样:
[0123456789] //该字符组匹配单个字符,这个字符可以是0123456789这十个数字中的任何一个
也可以简化一下变成这样
[0-9] //中间的‘-’的意思是‘从x到y所有字符’,该顺序遵循ASCII表的顺序,这里也同样表示0123456789这十个数字中的任何一个II
当然还有更简化的写法,上面也提到了
\d // ‘\d’这个符号代指任意一个字母,范围是:a-z 和 A-Z
再举个例子,还是上面的语境,我们要匹配所有的数字,除了数字‘0’和‘9’,也就是匹配‘0’和‘9’之外的所有数字 如果你前面的看懂了,那么这个问题就非常简单了
[^09] //‘^’用在字符组中是“取非”的意思,整个字符组的意思变成了“匹配单个字符,但这个字符不能是字符组中列出的任何一个”。注意:‘^’表示“取非”的意思时,必须放在字符组中字符的最前面
在来几个例子加深理解
[^a-f] //匹配单个字符,但是这个字符不能是‘abcdef’中的任何一个。 [^\d] //匹配单个字符,但是这个字符不能是数字。
字符组当然也有很多常用的快捷字符组:
字符组 |
匹配范围 |
---|---|
\d |
匹配单个字符,这个字符必须是数字 |
\D |
匹配单个字符,这个字符不能是数字,等于[\d] |
\w |
匹配单个字符,这个字符必须是字母 |
\W |
匹配单个字符,这个字符不能是字母,等于[\w] |
\s |
匹配单个字符,这个字符是一个空白字符(空格、制表符等等) |
\S |
匹配单个字符,这个字符不能是空白字符,等于[\s] |
4. 环视
什么是环视?
环视就是在匹配字符串的时候,规定字符串的前面或者后面的字符必须符合环视的要求。
先来整体看一下环视的分类和表现形式:
环视的种类 |
符号表示 |
具体含义 |
---|---|---|
顺序肯定环视 |
(?=…) |
某个字符后面有某个字符 |
顺序否定环视 |
(?!…) |
某个字符后面没有某个字符 |
逆序肯定环视 |
(?<=…) |
某个字符前面有某个字符 |
逆序否定环视 |
(?< !…) |
某个字符前面没有某个字符 |
可以看到,环视总共分为四种,并且具有各自的意思和表达方式。我们接下来举个例子来说明一下环视的用法。
实现数字的三位分割,也就是我们日常见到的金钱的表示方法,总是每三位加一个‘,’。比如余额为12345678元,往往被写成 12,345,678元 。 这个问题怎么解决呢,这个问题的关键是要找出需要插入‘,’的位置。
我们可以总结出一个规律,“从后往前看,都是三个数字一组”,也就是
(\d\d\d)+$ //三个数字一组,符合要求的有:12,345678 12,345,678
上面这个正则,只需要将所有的 (\d\d\d)+$
替换成 ,(\d\d\d)+$
就可以了 下面,我再分别针对环视的不同种类,分别举例说明一下具体的用法:
1.顺序肯定环视
比如我们想匹配”hellochillax helloxiao”里里面的“hello”,但是有个要求:在“hello”后面必须有”chillax”这个字符。
我们可以这样做:“hello(?=chillax)”
2.顺序否定环视
还是上面的这个字符串“hellochillax helloxiao”,这次,要求变了:在“hello”后面不能有”chillax”这个字符。
我们可以这样做:”hello(?!chillax)”
3.逆序肯定环视
比如我们想匹配”hellochillax xiaochillax”里里面的“chillax”,但是有个要求:在“chillax”前面必须有”hello”这个字符。
我们可以这样做:“(?<=hello)chillax”
4.逆序肯定环视
比如我们想匹配”hellochillax xiaochillax”里里面的“chillax”,但是有个要求:在“chillax”前面不能有”hello”这个字符。
我们可以这样做:“(?< !hello)chillax”
5. 捕获
这个功能其实是为了让我们更好地控制正则匹配的字符。有的时候我们为了获取到某些目的字符串,必须加入一些上下文元素,但是这些上下文元素并不是我们想要的,我们可以通过“捕获”来指出想要的部分,去掉不想要的部分。
比如,还是上面的那个过滤HTML中div标签的例子,如果我们只想过滤出第一个div标签里的内容,而不想要div标签,该怎么实现呢。
其实我们可以把想要的字符串对应的正则用括号括起来,就可以通过编程语言的一些函数获取到这个括号里的内容,从而达到除去上下文无用字符的目的。
待过滤HTML代码:
第一个DIV第二个DIV
过滤出‘
第一个DIV
’的正则是:
.*?//上面的例子,应该能看懂了
过滤出‘第一个DIV’的正则是:
(.*?)//比上面多了一对括号。我们可以通过直接获取括号里的内容来直接得到想要的字符串‘第一个DIV”
6. 模式修饰符(modifier)
在某些时候,我们需要对正则进行一些设定,用来满足某些特殊需求。
先来看一下常用的模式修饰符:
modifier |
作用 |
---|---|
(?i…) |
不区分大小写 |
(?-i…) |
取消不区分大小写 |
(?s…) |
点号通配模式 |
(?m…) |
增强的行锚点模式 |
这一块要一个一个解释了:
1. (?i…) 不区分大小写
有的时候我们想匹配某些字母,但是不区分大小写,比如我们想匹配字母‘ABCDabcd’,
最直观的,我们可以这样写:
[abcdABCD] //最直白的正则。。
也可以这样:
[a-dA-D] //使用‘-’,可以简化连续的字符的书写,比上面那个稍好。
也可以使用模式修饰符:
(?i:[abcd]) //在(?i:)里面的字符,不区分大小写,全部匹配
2. (?-i…) 取消不区分大小写
这个更简单,就是在上面那个符号内范围内,如果你想局部区分大小写,可以用这个。不举例了~
3. (?s…) 点号通配模式
这个有必要说一下,本文刚开始就介绍了一个特别有用的元字符‘.’,上面说它可以指代任何一个字符,除了换行符。那么,如果你想用“.*”来匹配一大段文字的话,里面有很多换行符,实现起来就很困难了。
所以,我们可以指定“.”暂时可以匹配换行符,所以可以写成:
(?s:.*) //在这个括号内,显式指定'.'匹配任何字符,包括换行符。
4. (?m…) 增强的行锚点模式(也成为多行文本模式)
增强的行锚点可以改变‘’和‘\(’的匹配效果。正常情况下,‘^’和‘\)’不会受到文本中换行符的干扰,也就是说如果一段文字中有多个换行符,那么正常情况下‘’和‘$’分别匹配这段文字的开头和结尾。
但是如果开启了增强的行锚点模式,‘’和‘$’就会分别匹配这段文字的第一个换行符之前的文字的开头和结尾。
例如:
My Life Getting Better \n NO1
然后有:
^.*$ //匹配结果为:My Life Getting Better \n NO1 (?m:^.*$) //匹配结果为:My Life Getting Better
可以看出明显的不同。。
三. 需要学习的还有很多
1. 正则表达式的效率
没错,正则表达式也是讲效率的,同一个目标字符串,同一个匹配要求,不同的正则表达式其效率可能差别很大。所以,作为一名合格的程序员,不仅要实现功能,还要时刻考虑效率的问题,这一点我会在文中多次提到这一点。希望能引起大家的注意。
2. 正则的流派和搜索引擎
正则是有很多流派的,不同的流派之间可能会有略微的不同,但是基本大同小异。
正则的驱动引擎分为两种:DFA和NFA。分别是确定型有限自动机和非确定型有限自动机,DFA的特点是“文本主导”,NFA的特点是“表达式主导”。
不同的编程语言可能属于不同的流派,也可能使用不同的驱动引擎,这会导致其在对正则的支持上会略有不同。比如NFA比DFA支持的正则特性要多。
当然,这些都可以先不用考虑,因为一般体会不到这种差别。。
3. 元字符转义
上面提到了很多正则里的元字符,它们出现在正则表达式中会有着自己特殊的含义。那么,在正则匹配过程中,如果我们就是想匹配这些字符呢。那就需要转意了,转意的表示方式是在被转意的元字符前面加一个反斜杠。
比如我们想匹配下面的字符串:
[私たち]
用下面的正则可以匹配么
[私たち] //这个正则的意思是:匹配单个代码点,这个代码点可以是‘私’、‘た’、‘ち’中的任意一个
当然不行。。
这里我们需要对“[”和“]”进行转意,变成这样
\[私たち\] //这里使用‘\’对元字符进行转意,使其变成一个普通的字符
当然,有些语言中,‘\’本身也需要转意,比如在Java中就需要下面这种表示:
\\[私たち\\]
其他的元字符同理~~
4. 正则的字符编码问题
上面多次提到,一个正则符号匹配单个或者多个“字符”,这个“字符”需要着重解释一下。
编码字符集有很多,比如Unicode、GBK、ASCII等等。。编程中最常用的编码字符集是Unicode。最常使用的编码格式是UTF-8 。UTF-8支持的字符范围和Unicode一样广泛,并且能够区分Unicode字符和ASCII字符,变长编码的方式也使得其存储效率较高,因此在编程中广泛被使用。
字符的存储方式是二进制编码,比如一个ASCII字符就占一个字节的空间,范围是0~127 。那么,每一个字符,在Unicode字符集中就对应着一个十六进制数字。我们把这个数字称为“代码点”(代码点指的是该字符在Unicode对应表中对应的数值)。我们需要注意的是,正则匹配时,匹配的“单个字符”其实并不准确,准确得说,应该是“单个代码点”。
绝大多数字符都对应一个代码点,有少数字符对应多个代码点。当我们用“.”去匹配这些字符时,会得不到我们想要的结果。
比如一个汉字对应一个代码点,所以我们可以用“.”去匹配单个汉字。
Unicode中有很多组合字符,这些字符看上去像是一个代码点,但是其实需要用多个代码点去表示。
比如,有兴趣的可以试一下用“.”去匹配下面这些字符:
กิิ ก้้ ก็็ ก็็ กิิ ก้้ ก็็ กิิ ก้้ กิิ ก้้ ก็็ ก็็ กิิ ก้้ ก็็ กิิ ก้้ //这里的每一个字符都对应着两个代码点
PS:说这些东西的目的是能够对编码有一定的了解。实际开发中基本用不到。不过对字符编码还是需要多了解一下,很重要~
四. 总结
精通正则表达式不仅要学会语法,更要在实际问题中不断练习。只有不断思考,不断尝试,才能将正则用在刀刃上,切切实实提升开发效率,达到应有的效果。
写了这么多,主要提到了开发中常用的一些正则知识和常见示例。好久没写技术博客了,可能表达上会有不好理解的地方。并且我对正则的了解也较为皮毛,文中难免会有不恰当甚至错误的地方。还请各位看官多多批评指正,定当虚心学习接受。
最后附上两个正则的教程,一个比较基础,另一个则是比较权威的教程。大家根据自己的需要选择吧。希望对大家能有一定的帮助。谢谢。
正则表达式30分钟入门教程
精通正则表达式(第三版)
原文发表时间:2018/4/6
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
发表于