《正则表达式必知必会》学习笔记

原书信息:
书名: 正则表达式必知必会
作者: 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 30 for 100 apples, 50 oranges, and 60 pears. I saved5 on this order.
这么写正则:(?<!\$\d*)\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行是查询条件,是可选的。如果有的话,必须是从?开始。这里也可以用查找条件表达式,但因为需求简单,现在的表达式更简洁,也更加高效

-------- (完) --------

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容