原书信息: | |
---|---|
书名: | 正则表达式必知必会 |
作者: | Ben Forta [美] |
译者: | 杨涛, 杨晓云, 王建桥 |
ISBN: | 978-7-115-16474-2 |
豆瓣介绍: | https://book.douban.com/subject/26285406/ |
正则表达式(以下简称“正则”)的作用:主要用字符串查找,替换。
1 从windows的“*”和“?”说起
我是比较晚接触正则的,最近才刚刚开始学习它。在接触正则之前,我在windows平台搜索文件的时候,有个通配符“*”, 它表示任意长度的任意内容,“?”表示任意单个字符。这个和正则比起来,是一个功能不强,也不怎么优雅的办法。但是却非常的实用。用户用他们搜索文件的时候,可以不必记住全部文件名,只需要记住其中具有特征的部分,举个例子
你想查找文件spring-expression-5.2.6.RELEASE.jar, 你可以这么查询:spring?expression*.jar, 甚至spring*.jar
如果按照本书的作者所说,纯文本也是正则的话,显然“*”和“?”也能算正则,但是正则里面却不是这么实现的。为什么呢?
实用归实用,但确实是功能比较弱,也不优雅。为什么呢?我们先看看正则怎么表达“任意长度的任意内容”,就明白了。
在正则里面,用.*来表示任意长度的任意内容。其中.表示任意字符, *表示任意个数的匹配。第一个强大的地方是:解耦了;第二个强大的地方是:因为解耦了,所以可以各自变化。
2 匹配字符
.
的含义太广泛了,正则有多种更严格的限制
2.1 任选一个——集合
集合用方括号表示:
F[0123456789]可以匹配F0,F1......,而 F12 只能匹配到F1,后面的2匹配不上
2.2 集合的简写形式——区间
[0123456789]的写法太复杂,因为从0-9是连续的,所以,可以写成:[0-9],和[0123456789]意思完全一样。
类似的还有:
[0-5] == [012345]
[A-Z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
[a-z] == [abcdefghijklmnopqrstuvwxyz]
需要注意的是:
[A-z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz]
这个是由他们在ASCII表中的位置来决定的
另外,一个集合可以包含多个区间:
[A-Za-z] == [ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]
不同区间之间不需要分隔符
实际上,字符集和区间是可以并存的,而且顺序也无所谓:
[Ff0-9a] = [Ff0123456789a]
2.3 取非
取非的符号是^:
[^0-9]匹配非数字
^虽然在[]里面,但是他表示对整个集合取非,所以会跟在[后面
2.4 一些简化
[^0-9]还能不能简化?能!
正则还预定义了一些语法糖,和集合相关的有:
\d == [0-9]
\w == [A-Za-z0-9_] //这个基本就是编程语言的标识符命名限制了
\s == [\f\n\r\t\v] //任意空白字符
他们的大写形式表示取非:
\D == [^0-9],其他依次类推
之所以定义这些简便写法,是因为这些很常用
3 重复匹配
[Ff0-9a] 是匹配一个字符,而不是3个字符。当我们用这个正则对F1ab进行全局匹配(后面会讲到全局匹配)的时候,是匹配到了3个结果、分别是F,1,a,而不是一个匹配结果。
如果我们想匹配一个结果F1a的话,应该这么写正则:
[Ff][0-9]a
3.1重复次数的区间
如果我们想匹配3~6个连续的数字,如 012, 3333, 33445, 778899 的话,怎么做呢?
好像比较发麻烦,如果次数固定的(比如3个)话,或许有办法:
[0-9][0-9][0-9]或者\d\d\d
这个既不灵活,也不优雅。
正则可以定义重复匹配的次数的区间。
集合使用[]定义,重复次数使用{}定义
想匹配3~6个连续的数字,可以这么写:
[0-9]{3,6} // {}定义的是一个闭区间,匹配长度为3或4或5或6
3.2 区间的简化形式
{3,6}这种形式在不同的情况下还可以简化
{3,} 至少重复3次,上不封顶
{3} == {3,3}
{1} == {1,1} 这个可以省略不写
和合集一样,依然有些语法糖:
+ == {1,} //一个或者多个
* == {0,} //零个或者多个
? == {0,1} //零个或者1个
3.3 贪婪和懒惰
假设用正则[0-9]{3,6}去匹配 012345,结果是012还是012345?
答案是012345。默认是贪婪模式。
懒惰模式的正则为[0-9]{3,6}?
同样,+*?
也都有懒惰模式:+?,*?,??
4 元字符和转义
所谓元字符就是在正则里面有特殊含义的字符,比如前面接触到的[, ], ., *等
如果要把元字符当普通字符使用,就要转义,转义使用"\",因此"\"也是元字符,做普通字符时,也需要转义:"\\"
在有些解析器中,如果[]没有匹配到[,不管是没出现,或者是被转义\[,那么]是不需要转义的,但是加上转义也可以。写的时候还是都转义,读的时候要注意。
4.1 反向转义
有些时候,为了表示不可见字符或者为了方便灵活,用转义字符"\"后面跟普通字符,表示一个特殊含义,我称之为反向转义。
下表是不可见字符的情况:
元字符 | 说明 |
---|---|
[\b] | 回退(并删除)一个字符(Backspace键) |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符(Tab键) |
\v | 垂直制表符 |
下表是一些常用的简写的情况:
元字符 | 说明 |
---|---|
\d | d表示digital,任何一个数字字符(等价于[0-9]) |
\D | 等价于[^0-9] |
\w | w表示word: 任何一个字母数字字符(大小写均可) 或下划线字符(等价于[a-zA-Z0-9_]) |
\W | 等价于[^a-zA-Z0-9_] |
\s | 任何一个空白字符(等价于[\f\n\r\t\v]) |
\S | 任何一个非空白字符,等价于[^\f\n\r\t\v] |
下表是表示数字进制的情况:
元字符 | 说明 |
---|---|
\x | 16进制表示,范围:0~FF(10进制0~255) |
\0 | 8进制表示,\后面是数字0,不是字母o,范围:0~77(10进制0~63) |
5 位置和边界
边界并不包含在匹配结果里面。
5.1单词边界\b
表示单词的开头或者结尾,
首先理解下什么是单词。正则不理解“单词”这个术语。单词可以理解为两个空白字符之间的非空白字符组成的部分。
然后要注意“或”。\babc表示匹配abc开头的单词,abc\b表示匹配abc结尾的单词,要只匹配单词abc,则\babc\b
\b还有取非的模式。也许你从前面注意到了,大写就是取非。所以\b取非就是\B
其含义如下:
\babc\b 能匹配ff abc ff中的abc,不能匹配ffabcff中的abc;
而
\Babc\B能匹配ffabcff中的abc,不能匹配ff abc ff中的abc
5.2 字符串边界
^匹配字符串开头,分行匹配模式下,匹配每一行的开头
$匹配字符串结尾,分行匹配模式下,匹配每一行的结尾,写在表达式尾部
6 子表达式 和 一致性匹配
6.1子表达式的定义
从语法上看,子表达式就是让表达式中的某些部分“优先计算”。和编程语言的算术表达式一样,使用()来实现。
举个简单的例子:
我们要匹配abab, 不能写ad{2}, 这种只能匹配abb,而应该写(ab){2}
再举个例子说明“优先计算”:
19|20\d{2},这个可以匹配2021,不能匹配1992,为什么呢?因为|的优先级比较低,这个正则的意思是:匹配19或者20\d{2},如果要搜索年份,同时匹配 1992 和 2021,应该写(19|20)\d{2}
子表达式是可以嵌套的
如:((ab){2}c){2}
匹配 ababcababc
6.2 回溯
从语法层面来说,用()定义的子表达式,不仅仅提高了计算的优先级,我们还可以像引用变量那样引用它,引用方法是反斜杠\后面跟一个数字,表示第几个,编号从1开始。
关于引用子表达式的编号:
(1)从1开始是因为0通常表示整个表达式
(2)对于嵌套的子表达式,从外向内编号,依次为:外层子表达式、内层子表达式、和外层平级的下一个子表达式
引用的规则不是说这里的子表达式和前面的子表达式一样,如果这样的话,直接重新写一遍就行了。引用的规则是:应用部分的内容应该和被引用表达式匹配到的内容完全一致。比较拗口,试举例说明:
([a-z])\d+\1 可以匹配 a21a, D47D, 不能匹配A53B, 也不能匹配D38d
因此这种引用被称为回溯引用。这种匹配被称为一致性匹配。
6.3 替换
子表达式的一个重要用途就是用来替换。
在替换的时候,引用子表达式,和查询略有不同。如何引用和实现引擎有关。以JS为例:
假设要给下面一段话中的Spring Boot加粗
With Spring Boot, your microservices can start small and iterate fast.
查询:(\bSpring \bBoot)
替换:<b>$1</b>
没错,在JS中替换时引用子表达式,用$, 而不是\。
替换的时候,可以改变大小写。如下表所示:
元字符 | 说明 |
---|---|
\u | 把下一个字符转换为大写 |
\l | 把下一个字符转换为小写 |
\U...\E | 把\U到\E之间的字符全部转换为小写 |
\L...\E | 把\L到\E之间的字符全部转换为小写 |
或许你已经注意到了,这里大写字母不是小写字母取非的意思。
例如,要把
micrOSErVice architectures are the ‘new normal’.
变成
Microservice architectures are the ‘new normal’.
查询:(^[a-z])(\w*)
替换:\u$1\L$2\E
7 前后查找
先理解下“消费”的概念。
我们写一个正则去匹配一段文本。被匹配到的文本如果在匹配结果里面,那就是我们“消费”了它,如果不在,那就是没有“消费”,举个生活中的例子,假设我借室友的钥匙去寝室拿了一鼠标,回来以后,钥匙还给了室友。那我消费的是鼠标,没有消费钥匙。
回到书中的例子,下面是个html文档,省略了部分内容:
<html lang='en'><head>...<title>Spring | Microservices</title>...</head><body>...</body></html>
我们要从中找出title的内容,如果没有前后查找的话,我们可以先用一个正则把<title>Spring | Microservices</title>出来,然后再用程序处理掉<title>和</title>,取中中间的内容。听起来就很累,还不如用DOM呢?
如果我们有办法用正则,一次就把Spring | Microservices拿到的话,我们就是使用了<title>,但没有消费<title>(</title>也一样)。
我们来一步一步做,看看能不能完成这个任务。
7.1 向前查找
向前查找使用?=,后面跟需要匹配但不消费的文本,并且用()包裹起来,返回的是匹配文本前面的内容,所以叫向前查找。
上例,我们可以这写:<title>.*(?=<\/title>)
。
注:实际中,根据html的规范,还要考虑title大小写的问题,此处假定原始文本全部小写
我们得到的是:<title>Spring | Microservices,</title>并没有返回。但这还不是我们想要的结果。
7.2 向后查找
这个根据“向前查找”自然而然就能想到的。向后查找使用?<=。
正则(?<=<title>).*<\/title>
匹配到的是:Spring | Microservices</title>。
把他们结合起来怎么样?
(?<=<title>).*(?=<\/title>)
,大功告成!
7.3 负向前、负向后查找
?=
的英文解释是Positive lookahead
?<=
的英文解释是Positive lookbehind
有Positive就有Negative
?!:
Negative lookahead
?<!:
Negative lookbehind
?! 和 ?<!解释起来太拗口,还是用作者的例子吧:
假设要从下面的文本中获取数量:
I paid 5 on this order.
这么写正则:(?<!\$\d*)\d+
解释下: 这个意图就是找出不是在表示
后面加\d*。
书作者特别提醒了:
(1)不是所有正则的实现都支持向后查找
(2)向后查找模式只能是固定长度——这是一条几乎所有的正则表达式实现都遵守的限制。
8 嵌入条件
嵌入条件就是先定义一个条件,在后面引用这个条件,如果条件成立的话,就怎样怎样。
8.1 回溯引用条件
为了聚焦重点,我们简化下作者的例子:
假设有如下文本:
<A href="/home"><IMG src="/images/home.gif"></A>
<IMG src="/images/spacer.gif">
<A href="/search"><IMG src="/images/spacer.gif">
我们的意图是:是找出IMG元素,或者包含IMG元素的完整的A元素。说人话就是匹配前两行,不匹配第3行(缺少</A>)
正则是这样的:^(<A.*>)?<IMG.*>(?(1)<\/A>)
解释下:
(1) 首先,为了更聚焦于条件,没有去兼容大小写。事实上,全局的大小写可以通过参数设定
(2) (<A.*>)?定义了一个条件A元素的开始,有更好的正则来匹配合法元素定义,这里忽略了
(3) ?(1)是引用前面条件,1表示第一个,如果成立的话,必须要有</A>,这个要优先计算,所以整体作为一个子表达式:(?(1)<\/A>)
(4) 用?()引用条件的时候,不需要转义成\1,因为这里没有歧义
(5) 最后说明下^,这个是从行首开始检查,否则第3行的IMG元素会以和第2行一样的规则被匹配
8.2 回溯引用匹配
假设有如下文本:
11111
22222
33333-
44444-4444
我们的意图是:匹配正点的电话号码,5个数字,或者后面再跟-和四个数字,匹配124,排除第3行
正则是这样的: \d{5}(?(?=-)-\d{4})
解释下:
(1) ?=- 是一个向前查找,找-,假设定义为C
(2) (?=-)前面的?表示条件,就像if一样
(3) (?=-)后面的-\d{4}在外层()里面,表示如果找到-(条件成立)的话,匹配-\d{4},假设定义为E
(4) 上面的前查找条件就是if(C)E
9 如何解读正则
现在有很多在线的正则工具帮我们解读正则,但是自己解读有助于熟练掌握正则,从而快速编写正则。理由如下:
有些时候,我们并不从头开始编写正则,而是拿一个正则来改一下。修改之前,要弄明白别人写的正则的含义。解读就很重要。
同时,解读多有有助于理解别人代码中正则的意图。
另外,也可以提高编写正则的能力。
解读的步骤:
(1)找出表达式中的元字符
正则复杂的一点是,元字符并不总是元字符。比如]在和[连用的时候才是元字符,单独使用,就是字符本身,可以转义,也可以不转义
(2)根据元字符的组合特征,将整个表达式拆成若干部分
(3)一次解读各部分,再拼起来
(4)对于复杂的子表达式,递归进行前面3步
试举1例
https?://(\w*:\w*@)?[-\w.]+(:\d+)?(/([\w/-.]*(\?\S+)?)?)?
先拆分
Line | Parts | Comments |
---|---|---|
1 | http |
普通字符 |
2 | s? |
字符s是可选的 |
3 | :// |
普通字符 |
4 | (\w*:\w*@)? |
可选的。 |
5 | \w*
|
任意数量的字母数字下划线的任意组合 |
6 | :
|
普通字符 |
7 | \w*
|
同上 |
8 | @
|
普通字符 |
9 | [-\w.]+ |
1个以上(含)的-、字母、数字、下划线的任意组合 |
10 | (:\d+)? |
:后面跟数字 |
11 | (/([\w/_.]*(\?\S+)?)?)? |
|
12 | /
|
普通字符 |
13 | ([\w/-.]*(\?\S+)?)?
|
|
14 | [\w/-.]*
|
任意数量的字母、数字、下划线、/、-的任意组合 |
15 | (\?\S+)?
|
整体加了可选 |
16 | \?
|
字符? |
17 | \S+
|
一个以上(含)的非空白字符 |
说明:
(1) 第1、2、3行,可以看出这个很可能是个url的正则,支持http或者https协议
(2) 第4行比较少见,如果在URL中,又有:和@,可能是用户名和密码
(3) 第9行是域名或者ip地址,类似www.abc.com这种
(4) 第10行从上下文看是端口,是可选项
(5) 第14行是路径,是可选项
(6) 第15行是查询条件,是可选的。如果有的话,必须是从?开始。这里也可以用查找条件表达式,但因为需求简单,现在的表达式更简洁,也更加高效
-------- (完) --------