This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="01 | 元字符:如何巧妙记忆正则表达式的基本元件?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ca/a9/cab5b6b584b68e12933076b518e7aaa9.mp3"></audio>
你好,我是涂伟忠。今天是课程的第一讲,我们一起来学习组成正则表达式的基本单元——元字符。
## 元字符的概念
在开篇词中,我们提到了正则常见的三种功能,它们分别是:校验数据的有效性、查找符合要求的文本以及对文本进行切割和替换等操作。<br>
<img src="https://static001.geekbang.org/resource/image/54/1b/54a184548d369ca04ad4b69cd15dac1b.png" alt="">
那你可能会好奇,正则是如何完成这些工作的呢?让我们先从简单的字符串查找和替换讲起。
我相信你一定在办公软件比如Word、Excel 中用过这个功能。你可以使用查找功能快速定位关注的内容,然后使用替换,批量更改这些内容。
<img src="https://static001.geekbang.org/resource/image/68/33/682df773ff276084ca4f934af53fa233.png" alt="">
让我们再回过头看看正则表达式。正则表达式,简单地说就是描述字符串的规则。在正则中,普通字符表示的还是原来的意思,比如字符 a它可以匹配“Hanmeimei is a girl”中的 H 之后的 a也可以匹配 is 之后的 a这个和我们日常见到的普通的字符串查找是一样的。
<img src="https://static001.geekbang.org/resource/image/c5/f6/c54ddfaa8a56453afed49f31022a8df6.png" alt="">
但除此之外,正则还可以做到普通的查找替换做不到的功能,**它真正的<strong><strong>强大之处**</strong>就在于可以查找符合某个规则的文本</strong>
举个例子假如你想查找文本中的所有数字如果不会正则可能需要手动敲数字从0到9这样操作10次一个个去查找很麻烦。但如果用正则的话就方便很多了我们直接使用 **\d** 就可以表示 0-9 这10个数字中的任意一个如下图所示。
<img src="https://static001.geekbang.org/resource/image/3a/ab/3af8dd848b2d625db7831aa8adcd6fab.png" alt=""><br>
如果我们在后面再加上量词,就可以表示单个的数字出现了几次。比如 \d{11} 表示单个数字出现11次即11位数字如果文本中只有姓名和手机号我们就可以利用这个查找出文本中的手机号了如下图所示。
<img src="https://static001.geekbang.org/resource/image/5a/bd/5a0ce54804cc08eb3622f418c3af45bd.png" alt=""><br>
那么到这里,你有没有发现正则的不同呢?像查找数字一样,在正则中,我们不需要像往常一样输入一个确定的内容,只需要敲入特殊的符号就可以帮我们完成查找和替换,像上面案例中提到的 \d 和 {11}在正则中有一个专门的名称——元字符Metacharacter
**所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符**,元字符是构成正则表达式的基本元件。正则就是由一系列的元字符组成的,看到这里相信你也能明白元字符的重要性了吧。
但是,因为元字符很基础,又比较多,所以很多人看见正则就头疼。那么今天,我就通过分类的方式,教你理解并且巧妙地记忆、使用元字符。
## 元字符的分类与记忆技巧
正则表达式中有很多的“元字符”,比如刚刚提到的 \d它在正则中不代表 \ (反斜杠) 加字母 d而是代表任意数字这种表示特殊含义的字符表示就是元字符。正则表达式中元字符非常多那么我们如何才能记住它们呢
这里我给你介绍一个方法,就是分类记忆。元字符虽然非常多,但如果我们按照分类法去理解记忆,效果会好很多。事实上,这个方法不光可以用在记忆元字符上,也可以用在记忆各种看似没有章法的内容上。
首先我可以把元字符大致分成这几类表示单个特殊字符的表示空白符的表示某个范围的表示次数的量词另外还有表示断言的我们可以把它理解成边界限定我会在后面的章节中专门讲解断言Assertions相关的内容。<br>
<img src="https://static001.geekbang.org/resource/image/97/bb/97f9aa05196ecba15a433e6e517093bb.png" alt="">
那么接下来,我们就按照前面说的元字符的分类,来逐一讲解下。
### 1.特殊单字符
首先,我们来看下表示特殊单个字符的元字符,比如英文的点(.)表示换行以外的任意单个字符,\d 表示任意单个数字,\w 表示任意单个数字或字母或下划线,\s 表示任意单个空白符。另外,还有与之对应的三个 \D、\W 和 \S分别表示着和原来相反的意思。
<img src="https://static001.geekbang.org/resource/image/19/4b/199562249878c0967dde9f23c0b4904b.png" alt="">
现在我们来看一下测试,我把常见数字,字母,部分标点符号作为文本,用 \d 去查找可以看到只能匹配上10个数字。
<img src="https://static001.geekbang.org/resource/image/13/a5/1380b43ba8a8ebf4a307a5d0a4e35aa5.png" alt="">
这是元字符 **\d** 测试用例的链接,你不妨测试一下:[https://regex101.com/r/PnzZ4k/1](https://regex101.com/r/PnzZ4k/1)
元字符 \w 能匹配所有的数字、字母和下划线,如下图所示:
<img src="https://static001.geekbang.org/resource/image/60/f1/60041ca73688567b0bae04950f8be0f1.png" alt=""><br>
同样的,元字符 **\w** 测试用在这里:[https://regex101.com/r/PnzZ4k/2](https://regex101.com/r/PnzZ4k/2)
你可以自己去尝试一下 \W\D\s 和 \S ,以及英文的点的匹配情况,这里我不展开了。
### 2.空白符
除了特殊单字符外,你在处理文本的时候肯定还会遇到空格、换行等空白符。其实在写代码的时候也会经常用到,换行符 **\n**TAB制表符 **\t** 等。
有编程经验的程序员肯定都知道不同的系统在每行文本结束位置默认的“换行”会有区别。比如在Windows 里是 **\r\n**,在 Linux 和 MacOS 中是 **\n**。
在正则中,也是类似于 \n 或 \r 等方式来表示空白符号,只要记住它们就行了。平时使用正则,大部分场景使用 \s 就可以满足需求,\s 代表任意单个空白符号。
<img src="https://static001.geekbang.org/resource/image/01/66/01b6c8de6ee6c440471c15f96d00d466.png" alt="">
我们可以看到, **\s** 能匹配上各种空白符号,也可以匹配上空格。换行有专门的表示方式,在正则中,空格就是用普通的字符英文的空格来表示。
### 3.量词
刚刚我们说到的“基础”的元字符也好,“空白符”也好,它们都只能匹配单个字符,比如\d只能匹配一个数字。但更多时候我们需要匹配单个字符或者某个部分“重复N次”“至少出现一次”“最多出现三次”等等这样的字符这个时候该怎么办呢
这就需要用到表示量词的元字符了。
在正则中,英文的星号(*代表出现0到多次加号+代表1到多次问号?代表0到1次{m,n}代表m到n次。
<img src="https://static001.geekbang.org/resource/image/2b/c3/2b03098dcc203c648a40f89a0ba77fc3.png" alt="">
比如,在文本中“颜色”这个单词,可能是带有 u 的 colour也可能是不带 u 的 color我们使用 colou?r 就可以表示两种情况了。在真实的业务场景中比如某个日志需要添加了一个user字段但在旧日志中这个是没有的那么这时候可以使用问号来表示出现0次或1次这样就可以表示user字段**存在和不存在**两种情况。
下面这段文本由三行数字组成,当我们使用 \d+ 时能匹配上3个但使用 \d* 时能匹配上6个详细匹配结果可以参考下面的图片
<img src="https://static001.geekbang.org/resource/image/f0/4e/f0696d3fbc55c5a75832472e227a614e.png" alt="">
我把正则 \d+ 示例链接放在下面了,你可以看一下:[https://regex101.com/r/PnzZ4k/8](https://regex101.com/r/PnzZ4k/8)
其它的比如表示 m 到 n 次的,你可以自己去尝试,一定要多加练习,这样你才能记得牢。
### 4.范围
学习了量词,我们就可以用 \d{11} 去匹配所有手机号但同时也要明白这个范围比较大有一些不是手机号的数字也会被匹配上比如11个0那么我们就需要在一个特殊的范围里找符合要求的数字。
再比如我们要找出所有元音字母aeiou的个数这又要如何实现呢在正则表达式中表示范围的元字符可以轻松帮我们搞定这样的问题。
在正则表达式中,表示范围的符号有四个分类,如下图所示。
<img src="https://static001.geekbang.org/resource/image/84/22/84625cc80a90dba2c629282e3f213622.png" alt="">
首先是管道符号,我们用它来隔开多个正则,表示满足其中任意一个就行,比如 **ab|bc** 能匹配上 ab也能匹配上 bc在正则有多种情况时这个非常有用。
中括号[]代表多选一,可以表示里面的任意单个字符,所以任意元音字母可以用 **[aeiou]** 来表示。另外,中括号中,我们还可以用中划线表示范围,比如 **[a-z]** 可以表示所有小写字母。如果中括号第一个是脱字符(^),那么就表示非,表达的是不能是里面的任何单个元素。
比如某个资源可能以 http:// 开头,或者 https:// 开头,也可能以 ftp:// 开头,那么资源的协议部分,我们可以使用 (https?|ftp):// 来表示。
<img src="https://static001.geekbang.org/resource/image/22/3d/22b3ef8c95b01adddebad77fed106a3d.png" alt="">
同样地,我把示例链接也放在了下面,你可以参考一下:[https://regex101.com/r/PnzZ4k/5](https://regex101.com/r/PnzZ4k/5)
## 总结
今天我通过大量的示例讲解了元字符,希望可以帮助你加强对正则各类元字符的理解,想办法记住它们,这是掌握正则这个强大工具的第一步。
我在这里给你强调一下学习的方法。你可以看到我在每一个案例中加入了测试链接,目的就是希望你能借此多做练习。在学习过程中,一定要找时间去练习,去观察匹配的结果,这样可以帮助你加深记忆。如果只是看和听,没有足够的练习,实际上很难记得牢,所以一定要自己多动手操作尝试。
还有一个方法,你可以把学到的知识,试着讲给其它的同事或同学,最好的学习方法就是去教别人,一个知识点,如果你能给别人讲明白,证明你真的搞懂了,真的掌握了这个知识。
好了,学习完今天的内容,最后我来给你总结一下。正则表达式中元字符的分类记忆,你可以在脑海中回忆一下。今天我们学习了正则表达式的部分元字符,特殊单字符、空白符、范围、量词等。我整理成了一张脑图,你可以看一下,对照着练习、记忆。
<img src="https://static001.geekbang.org/resource/image/ff/52/ffd9cd49a79599e7efa4aba069faee52.png" alt="">
## 思考题
通过今天的学习,不知道你元字符掌握到何种程度了呢?那么不妨练习一下吧!我在这里给出一些手机号的组成规则:
1. 第1位固定为数字1
1. 第2位可能是3456789
1. 第3位到第11位我们认为可能是0-9任意数字。
你能不能利用今天学到的知识,写出一个“更严谨”的正则来表示手机号呢?
好,今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,让我们一起进步,共同掌握正则表达式这个强大的工具。

View File

@@ -0,0 +1,246 @@
<audio id="audio" title="02丨量词与贪婪小小的正则也可能把CPU拖垮" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/c3/abcd1e70644114b1366756966c5e75c3.mp3"></audio>
你好,我是涂伟忠。在上一讲中,我们已经学习了正则中和一些元字符相关的内容。这一节我们讲一下正则中的三种模式,贪婪匹配、非贪婪匹配和独占模式。
这些模式会改变正则中量词的匹配行为,比如匹配一到多次;在匹配的时候,匹配长度是尽可能长还是要尽可能短呢?如果不知道贪婪和非贪婪匹配模式,我们写的正则很可能是错误的,这样匹配就达不到期望的效果了。
## 为什么会有贪婪与非贪婪模式?
由于本节内容和量词相关的元字符密切相关,所以我们先来回顾一下正则中表示量词的元字符。
<img src="https://static001.geekbang.org/resource/image/2b/c3/2b03098dcc203c648a40f89a0ba77fc3.png" alt="">
在这6种元字符中我们可以用 {m,n} 来表示 *+? 这3种元字符
<img src="https://static001.geekbang.org/resource/image/38/74/38ceb28add7794fe9ed069e08fb1b374.jpg" alt="">
表示量词的星号(*)和 加号(+)可能没你想象的那么简单,我用一个例子给你讲解一下。我们先看一下加号(+),使用 a+ 在 aaabb 中查找,可以看到只有一个输出结果:
<img src="https://static001.geekbang.org/resource/image/2b/08/2b3e3f549e69fdd398c15d6b0bd44e08.png" alt="">
对应的Python代码如下
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'a+', 'aaabb')
['aaa']
```
加号应该很容易理解,我们再使用 a* 在 aaabb 这个字符串中进行查找这次我们看到可以找到4个匹配结果。
<img src="https://static001.geekbang.org/resource/image/b0/4c/b0c582cbf8ec081bc798296b5471804c.png" alt="">
使用Python示例如下我们可以看到输出结果也是得到了4个匹配结果
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'a*', 'aaabb')
['aaa', '', '', '']
```
但这一次的结果匹配到了三次空字符串。为什么会匹配到空字符串呢?因为星号(*代表0到多次匹配0次就是空字符串。到这里你可能会有疑问如果这样aaa 部分应该也有空字符串,为什么没匹配上呢?
这就引入了我们今天要讲的话题,贪婪与非贪婪模式。这两种模式都必须满足匹配次数的要求才能匹配上。贪婪模式,简单说就是尽可能进行最长匹配。非贪婪模式呢,则会尽可能进行最短匹配。正是这两种模式产生了不同的匹配结果。
## 贪婪、非贪婪与独占模式
### 贪婪匹配Greedy
首先,我们来看一下贪婪匹配。在正则中,表示次数的量词默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。
首先,我们来看一下在字符串 aaabb 中使用正则 a* 的匹配过程。
<img src="https://static001.geekbang.org/resource/image/a7/ca/a7d62eee986938327d31e170cdd3caca.jpg" alt="">
<img src="https://static001.geekbang.org/resource/image/63/97/63e5c750b66f6eb914c73befdba43f97.jpg" alt="">
a* 在匹配开头的 a 时,会尝试尽量匹配更多的 a直到第一个字母 b 不满足要求为止匹配上三个a后面每次匹配时都得到了空字符串。
相信看到这里你也发现了,贪婪模式的特点就是尽可能进行最大长度匹配。所以要不要使用贪婪模式是根据需求场景来定的。如果我们想尽可能最短匹配呢?那就要用到非贪婪匹配模式了。
### 非贪婪匹配Lazy
那么如何将贪婪模式变成非贪婪模式呢?我们可以在量词后面加上英文的问号(?),正则就变成了 a*?。此时的匹配结果如下:<br>
<img src="https://static001.geekbang.org/resource/image/10/bc/10e40baa1194b17dcc57a089524a37bc.png" alt="">
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'a*', 'aaabb') # 贪婪模式
['aaa', '', '', '']
&gt;&gt;&gt; re.findall(r'a*?', 'aaabb') # 非贪婪模式
['', 'a', '', 'a', '', 'a', '', '', '']
```
这一次我们可以看到这次匹配到的结果都是单个的a就连每个a左边的空字符串也匹配上了。
到这里你可能就明白了,非贪婪模式会尽可能短地去匹配,我把这两者之间的区别写到了下面这张图中。
<img src="https://static001.geekbang.org/resource/image/3f/d1/3f95a3648980c1eb3c550fb34b46fad1.png" alt="">
为了让你加深理解,我们再来看一个示例,这一次让我们查找一下引号中的单词。
从下面这个示例中,我们可以很容易看出两者对比上的差异。左右的文本是一样的,其中有两对双引号。不同之处在于,左边的示例中,不加问号时正则是贪婪匹配,匹配上了从第一个引号到最后一个引号之间的所有内容;而右边的图是非贪婪匹配,找到了符合要求的结果。
<img src="https://static001.geekbang.org/resource/image/40/79/40c03d7a2cb990b35e4801589eca1379.png" alt="">
### 独占模式Possessive
不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。
你可能会问,那什么是回溯呢?我们来看一些例子,例如下面的正则:
>
regex = “xy{1,3}z”
>
text = “xyyz”
在匹配时y{1,3}会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会**向前回溯**,吐出当前字符 z接着用正则中的 z 去匹配。
<img src="https://static001.geekbang.org/resource/image/7a/88/7a9636b588963e5af9619837fe5a6888.png" alt="">
如果我们把这个正则改成非贪婪模式,如下:
>
regex = “xy{1,3}?z”
>
text = “xyyz”
由于 y{1,3}? 代表匹配1到3个 y尽可能少地匹配。匹配上一个 y 之后,也就是在匹配上 text 中的 xy 后,正则会使用 z 和 text 中的 xy 后面的 y 比较,发现正则 z 和 y 不匹配,这时正则就会**向前回溯**,重新查看 y 匹配两个的情况,匹配上正则中的 xyy然后再用 z 去匹配 text 中的 z匹配成功。
<img src="https://static001.geekbang.org/resource/image/21/0c/2177c740a2d5dd805f3157d54636500c.png" alt="">
了解了回溯,我们再看下独占模式。
独占模式和贪婪模式很像,独占模式会尽可能多地去匹配,如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。具体的方法就是在量词后面加上加号(+)。
>
regex = “xy{1,3}+yz”
>
text = “xyyz”
<img src="https://static001.geekbang.org/resource/image/96/cb/96635e198c2ff6cf7b8ea2a0d18f8ecb.png" alt="">
需要注意的是 Python 和 Go 的标准库目前都不支持独占模式,会报错,如下所示:
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r'xy{1,3}+yz', 'xyyz')
error: multiple repeat at position 7
```
报错显示,加号(+)被认为是重复次数的元字符了。如果要测试这个功能,我们可以安装 PyPI 上的 regex 模块。
```
注意:需要先安装 regex 模块pip install regex
&gt;&gt;&gt; import regex
&gt;&gt;&gt; regex.findall(r'xy{1,3}z', 'xyyz') # 贪婪模式
['xyyz']
&gt;&gt;&gt; regex.findall(r'xy{1,3}+z', 'xyyz') # 独占模式
['xyyz']
&gt;&gt;&gt; regex.findall(r'xy{1,2}+yz', 'xyyz') # 独占模式
[]
```
你也可以使用 Java 或 Perl 等其它语言来测试独占模式,查阅相关文档,看一下你所用的语言对独占模式的支持程度。
如果你用 a{1,3}+ab 去匹配 aaab 字符串a{1,3}+ 会把前面三个 a 都用掉,并且不会回溯,这样字符串中内容只剩下 b 了,导致正则中加号后面的 a 匹配不到符合要求的内容,匹配失败。如果是贪婪模式 a{1,3} 或非贪婪模式 a{1,3}? 都可以匹配上。
<img src="https://static001.geekbang.org/resource/image/1d/b7/1dbf7d9fed42390edb3bf9ef9e0da7b7.jpg" alt="">
这里我简单总结一下独占模式性能比较好可以节约匹配的时间和CPU资源但有些情况下并不能满足需求要想使用这个模式还要看具体需求比如我们接下来要讲的案例另外还得看你当前使用的语言或库的支持程度。
## 正则回溯引发的血案
学习到了这里,你是不是觉得自己对贪婪模式、非贪婪模式,以及独占模式比较了解了呢?其实在使用过程中稍不留神,就容易出问题,在网上可以看到不少因为回溯引起的线上问题。
这里我们挑选一个比较出名的是阿里技术微信公众号上的发文。Lazada卖家中心店铺名检验规则比较复杂名称中可以出现下面这些组合
<li>
英文字母大小写;
</li>
<li>
数字;
</li>
<li>
越南文;
</li>
<li>
一些特殊字符,如“&amp;”,“-”“_”等。
</li>
负责开发的小伙伴在开发过程中使用了正则来实现店铺名称校验,如下所示:
```
^([A-Za-z0-9._()&amp;'\- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])+$
```
这个正则比较长,但很好理解,中括号里面代表多选一,我们简化一下,就成下面这样:
```
^([符合要求的组成1]|[符合要求的组成2])+$
```
脱字符(^)代表以这个正则开头,美元符号($)代表以正则结尾,我们后面会专门进行讲解。这里可以先理解成整个店铺名称要能匹配上正则,即起到验证的作用。
你需要留意的是,正则中有个加号(+表示前面的内容出现一到多次进行贪婪匹配这样会导致大量回溯占用大量CPU资源引发线上问题我们只需要将贪婪模式改成独占模式就可以解决这个问题。
我之前说过,要根据具体情况来选择合适的模式,在这个例子中,匹配不上时证明店铺名不合法,不需要进行回溯,因此我们可以使用独占模式,但要注意并不是说所有的场合都可以用独占模式解决,我们要首先保证正则能满足功能需求。
仔细再看一下 这个正则,你会发现 “组成1” 和 “组成2” 部分中A-Za-z 英文字母在两个集合里面重复出现了,这会导致回溯后的重复判断。这里要强调一下,并不是说有回溯就会导致问题,你应该尽量减少回溯后的计算量,这些在后面的原理讲解中我们会进一步学习。
另外,腾讯云技术社区​也有类似的技术文章,你如果感兴趣,可以点击这里[进行](https://zhuanlan.zhihu.com/p/38229530)查看。
说到这里,你是不是想起了课程开篇里面提到的一句话:
>
如果你有一个问题,你想到可以用正则来解决,那么你有两个问题了。
>
Some people, when confronted with a problem, think “I know, Ill use regular expressions.” Now they have two problems.
所以一个小小的正则有些时候也可能会把CPU拖垮这也提醒我们在写正则的时候一定要思考下回溯问题避免使用低效的正则引发线上问题。
## 最后总结
最后我来给你总结一下:正则中量词默认是贪婪匹配,如果想要进行非贪婪匹配需要在量词后面加上问号。贪婪和非贪婪匹配都可能会进行回溯,独占模式也是进行贪婪匹配,但不进行回溯,因此在一些场景下,可以提高匹配的效率,具体能不能用独占模式需要看使用的编程语言的类库的支持情况,以及独占模式能不能满足需求。
<img src="https://static001.geekbang.org/resource/image/1a/75/1ad3eb0d011ba4fc972b9e5191a9f275.png" alt="">
## 课后思考
最后,我们来做一个小练习吧。
有一篇英文文章,里面有很多单词,单词和单词之间是用空格隔开的,在引号里面的一到多个单词表示特殊含义,即引号里面的多个单词要看成一个单词。现在你需要提取出文章中所有的单词。我们可以假设文章中除了引号没有其它的标点符号,有什么方法可以解决这个问题呢?如果用正则来解决,你能不能写出一个正则,提取出文章中所有的单词呢(不要求结果去重)?
>
we found “the little cat” is in the hat, we like “the little cat”
>
其中 the little cat 需要看成一个单词
好了,今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,并把文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,190 @@
<audio id="audio" title="03 | 分组与引用:如何用正则实现更复杂的查找和替换操作?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/20/9236c5813089965fed178ba3d62e6e20.mp3"></audio>
你好,我是伟忠。今天我打算和你聊聊分组与引用。那什么场合下会用到分组呢?
假设我们现在要去查找15位或18位数字。根据前面学习的知识使用量词可以表示出现次数使用管道符号可以表示多个选择你应该很快就能写出\d{15}|\d{18}。但经过测试你会发现这个正则并不能很好地完成任务因为18位数字也会匹配上前15位具体如下图所示。
<img src="https://static001.geekbang.org/resource/image/a7/b2/a72ad4ccc3eb769562c331f230b9c6b2.png" alt="">
为了解决这个问题你灵机一动很快就想到了办法就是把15和18调换顺序即写成 **\d{18}|\d{15}**。你发现,这回符合要求了。
<img src="https://static001.geekbang.org/resource/image/ba/3c/ba18101e2109df87288d935b5767c83c.png" alt="">
为什么会出现这种情况呢?因为在大多数正则实现中,多分支选择都是左边的优先。类似地,你可以使用 “北京市|北京” 来查找 “北京” 和 “北京市”。另外我们前面学习过问号可以表示出现0次或1次你发现可以使用“北京市?” 来实现来查找 “北京” 和 “北京市”。
<img src="https://static001.geekbang.org/resource/image/fd/30/fdb97d69e376306e68c4e36d5ddbf830.png" alt="">
同样针对15或18位数字这个问题可以看成是15位数字后面3位数据有或者没有你应该很快写出了 **\d{15}\d{3}?** 。但这样写对不对呢?我们来看一下。
在上一节我们学习了量词后面加问号表示非贪婪,而我们现在想要的是 \d{3} 出现0次或1次。
示例一:<br>
\d{15}\d{3}? 由于 \d{3} 表示三次加问号非贪婪还是3次
示例二:<br>
\d{15}(\d{3})? 在 \d{3} 整体后加问号,表示后面三位有或无
这时候,必须使用括号将来把表示“三个数字”的\d{3}这一部分括起来,也就是表示成\d{15}(\d{3})?这样。现在就比较清楚了:括号在正则中的功能就是用于分组。简单来理解就是,由多个元字符组成某个部分,应该被看成一个整体的时候,可以用括号括起来表示一个整体,这是括号的一个重要功能。其实用括号括起来还有另外一个作用,那就是“复用”,我接下来会给你讲讲这个作用。
## 分组与编号
括号在正则中可以用于分组,被括号括起来的部分“子表达式”会被保存成一个子组。
那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。这么说可能不好理解,我们来举一个例子看一下。
这里有个时间格式 2020-05-10 20:23:05。假设我们想要使用正则提取出里面的日期和时间。
<img src="https://static001.geekbang.org/resource/image/87/8a/87d896f423780c43199222e32c4e428a.png" alt="">
我们可以写出如图所示的正则,将日期和时间都括号括起来。这个正则中一共有两个分组,日期是第 1 个,时间是第 2 个。
#### 不保存子组
在括号里面的会保存成子组,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 ?: 不保存子组。
如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以不保存子组可以提高正则的性能。除此之外呢,这么做还有一些好处,由于子组变少了,正则性能会更好,在子组计数时也更不容易出错。
那到底啥是不保存子组呢?我们可以理解成,括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。
<img src="https://static001.geekbang.org/resource/image/d6/18/d6a3d486a8c575bc1961b7db5a153d18.png" alt="">
<img src="https://static001.geekbang.org/resource/image/4b/fb/4b14f91e4307580bb482c58232c3f1fb.png" alt="">
#### 括号嵌套
前面讲完了子组和编号,但有些情况会比较复杂,比如在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组怎么办?不要担心,其实方法很简单,我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组。
在阿里云简单日志系统中,我们可以使用正则来匹配一行日志的行首。假设时间格式是 2020-05-10 20:23:05 。
<img src="https://static001.geekbang.org/resource/image/08/40/083b6a8af68f56f3120b7c8875329340.png" alt="">
日期分组编号是 1时间分组编号是 5年月日对应的分组编号分别是 234时分秒的分组编号分别是 678。
#### 命名分组
前面我们讲了分组编号但由于编号得数在第几个位置后续如果发现正则有问题改动了括号的个数还可能导致编号发生变化因此一些编程语言提供了命名分组named grouping这样和数字相比更容易辨识不容易出错。命名分组的格式为`(?P&lt;分组名&gt;正则)`
比如在Django的路由中命名分组示例如下
```
url(r'^profile/(?P&lt;username&gt;\w+)/$', view_func)
```
需要注意的是,刚刚提到的方式命名分组和前面一样,给这个分组分配一个编号,不过你可以使用名称,不用编号,实际上命名分组的编号已经分配好了。不过命名分组并不是所有语言都支持的,在使用时,你需要查阅所用语言正则说明文档,如果支持,那你才可以使用。
### 分组引用
在知道了分组引用的编号 number大部分情况下我们就可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引用,而 JavaScript中是通过`$`编号来引用,如`$`1。
我给到了你一些在常见的编程语言中,分组查找和替换的引用方式:
<img src="https://static001.geekbang.org/resource/image/c4/94/c4eef43e2ccf55978b949a194a175594.jpg" alt="">
这些内容不要求你完全记住,只要有个印象就好,最关键的是要知道正则可以实现这样的功能,
需要用到的时候查一下相应的文档,就知道怎么用了。
## 分组引用在查找中使用
前面介绍了子组和引用的基本知识,现在我们来看下在正则查找时如何使用分组引用。比如我们要找重复出现的单词,我们使用正则可以很方便地使“前面出现的单词再次出现”,具体要怎么操作呢?我们可以使用 \w+ 来表示一个单词,针对刚刚的问题,我们就可以很容易写出 (\w+) \1 这个正则表达式了。
<img src="https://static001.geekbang.org/resource/image/39/e6/3951b939651d32402e9efe63a83e7de6.png" alt="">
## 分组引用在替换中使用
和查找类似,我们可以使用反向引用,在得到的结果中,去拼出来我们想要的结果。还是使用刚刚日期时间的例子,我们可以很方便地将它替换成, 2020年05月10日这样的格式。
<img src="https://static001.geekbang.org/resource/image/b2/14/b2465f3f8c50432b622ec8704dc8a214.png" alt="">
由于这个例子稍微复杂一些,这里我给出一个[示例链接](https://regex101.com/r/2RVPTJ/2)方便你学习,不知道学到这里,你有没有觉得子组和引用非常强大呢?
你可能很好奇那在编程语言中如何实现这个功能呢我下面以Python3为例给出一个示例。
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; test_str = &quot;2020-05-10 20:23:05&quot;
&gt;&gt;&gt; regex = r&quot;((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))&quot;
&gt;&gt;&gt; subst = r&quot;日期\1 时间\5 \2年\3月\4日 \6时\7分\8秒&quot;
&gt;&gt;&gt; re.sub(regex, subst, test_str)
'日期2020-05-10 时间20:23:05 2020年05月10日 20时23分05秒'
```
在Python中 sub 函数用于正则的替换,使用起来也非常简单,和在网页上操作测试的几乎一样。
## 在文本编辑器中使用
### Sublime Text 3 简介
接下来我用Sublime Text 3 来当例子给你讲解一下正则查找和替换的使用方式。Sublime Text 3 是一个跨平台编辑器,非常小巧、强悍,虽然是一个收费软件,但可以永久试用,你自行可以下载安装。
当熟练使用编辑器之后,你会发现在很多工作里都可以使用它,不需要编写代码就可以完成。
下面我以文本编辑器 Sublime Text 3 为例,来讲解正则查找和替换的使用方式。首先,我们要使用的“查找”或“替换”功能,在菜单 Find 中可以找到。
<img src="https://static001.geekbang.org/resource/image/e5/43/e54e9cedb2fe132b206c3eb3ba0fae43.png" alt="">
下面是对编辑器查找-替换界面的图标简介Find 输入栏第一个 .* 图标,表示开启或关闭正则支持。
<img src="https://static001.geekbang.org/resource/image/58/05/588f3618f31cb91dba29264ea0ab6f05.png" alt="">
### 编辑器中进行正则查找
接下来,我们来演示用编辑器查找引号引起来的内容,课程中使用到的文本,建议你用 chrome 等浏览器等,打开极客时间网页版本 [https://time.geekbang.org](https://time.geekbang.org/),点击右键查看源代码,把看到的代码复制到 Sublime Text 3 中。
<img src="https://static001.geekbang.org/resource/image/31/ab/3119dea0ab1c2c93fb6bd2dc500476ab.png" alt="">
输入相应的正则,我们就可以看到查找的效果了。这里给一个小提示,如果你点击 Find All然后进行剪切具体操作可以在菜单中找到 Edit -&gt; Cut也可以使用快捷键操作。剪切之后找一个空白的地方粘贴就可以看到提取出的所有内容了。
我们可以使用正则进行资源链接提取,比如从一个图片网站的源代码中查找到图片链接,然后再使用下载工具批量下载这些图片。
### 在编辑器中进行正则替换
接着,我们再来看一下在编辑器中进行文本替换工作。你可以在编辑器中输入这些文本:
the little cat cat is in the hat hat, we like it.
如果我们要尝试从中查找连续重复出现两次的单词,我们可以用 \w+ 代表单词,利用我们刚刚学习的知识,相信你可以很快写出正则 **(\w+)** **\1****。**
<img src="https://static001.geekbang.org/resource/image/db/46/dbe5ce11d8968387402bb48b733a5146.png" alt="">
接着点击菜单中的 Find -&gt; Replace在替换栏中输入子组的引用 **\1** ,然后点击 Replace All 就可以完成替换工作了。
<img src="https://static001.geekbang.org/resource/image/cc/99/ccdbb32b1e41ce365fc7a296feba2699.png" alt="">
这样,通过少量的正则,我们就完成了文本的处理工作了。
几乎所有主流编辑器都是支持正则的,你可以在你喜欢的编辑器中尝试一下这个例子,在后面的工作中,也可以尝试使用它来完成一些复杂的文本查找和替换工作。
## 总结
好了,今天的内容讲完了,我来带你总结回顾一下。
今天我们学习到了正则中的分组和子组编号相关内容。括号可以将某部分括起来,看成一个整体,也可以保存成一个子组,在后续查找替换的时候使用。分组编号是指,在正则中第几个括号内就是第几个分组,而嵌套括号我们只要看左括号是第几个就可以了。如果不想将括号里面的内容保存成子组,可以在括号里面加上?:来解决。
搞懂了分组的内容,我们就可以利用分组引用,来实现将“原文本部分内容”,在查找或替换时进行再次利用,达到实现复杂文本的查找和替换工作。甚至在使用一些文本编辑器的时候,不写代码,我们就可以完成文本的查找替换处理工作,这往往可以节约很多开发时间。
<img src="https://static001.geekbang.org/resource/image/dd/99/dd29e757e0d4352e06eaee3486d73e99.png" alt="">
## 课后思考
最后,我们来做一个小练习吧。有一篇英文文章,里面有一些单词连续出现了多次,我们认为连续出现多次的单词应该是一次,比如:
>
the little cat cat is in the hat hat hat, we like it.
>
其中 cat 和 hat 连接出现多次,要求处理后结果是
>
the little cat is in the hat, we like it.
<img src="https://static001.geekbang.org/resource/image/97/16/97ce94dbc562c7a5e9e9eeb9b9cfeb16.png" alt="">
这个题目我给出了相应的地址 [https://regex101.com/r/2RVPTJ/3](https://regex101.com/r/2RVPTJ/3),你可以直接在网页上进行测试,写入相应的 “正则查找部分” 和 “正则替换部分”,让结果符合预期。多动手练习,你才能更好地掌握学习的内容。
好,今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论。也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,156 @@
<audio id="audio" title="04 | 匹配模式一次性掌握正则中常见的4种匹配模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/69/c9/69ce6916cdd7c660f51b8bc40d149ac9.mp3"></audio>
你好我是涂伟忠。今天我们一起来学习正则中的匹配模式Match Mode
所谓匹配模式,指的是正则中一些**改变元字符匹配行为**的方式比如匹配时不区分英文字母大小写。常见的匹配模式有4种分别是不区分大小写模式、点号通配模式、多行模式和注释模式。我们今天主要来讲一下这4种模式。
需要注意的是这里的“模式”对应的是英文中的mode而不是pattern。有些地方会把正则表达式pattern也翻译成模式你在网上看到的技术文章中讲的正则模式有可能指的是正则表达式本身这一点你需要注意区别。
## 不区分大小写模式Case-Insensitive
首先,我们来看一下不区分大小写模式。它有什么用呢?学一个知识的时候,我一般喜欢先从它的应用出发,这样有时候更能激发我学习的兴趣,也更容易看到学习成果。
下面我来举个例子说明一下。在进行文本匹配时我们要关心单词本身的意义。比如要查找单词cat我们并不需要关心单词是CAT、Cat还是cat。根据之前我们学到的知识你可能会把正则写成这样**[Cc][Aa][Tt]**,这样写虽然可以达到目的,但不够直观,如果单词比较长,写起来容易出错,阅读起来也比较困难。
<img src="https://static001.geekbang.org/resource/image/33/9d/334501a163b809125f62821edb1cfb9d.png" alt="">
那么有没有更好的办法来实现这个需求呢?这时候不区分大小写模式就派上用场了。
我们前面说了,不区分大小写是匹配模式的一种。当我们把**模式修饰符**放在整个正则前面时,就表示整个正则表达式都是不区分大小写的。模式修饰符是通过**(?模式标识)**的方式来表示的。 我们只需要把模式修饰符放在对应的正则前就可以使用指定的模式了。在不区分大小写模式中由于不分大小写的英文是Case-**I**nsensitive那么对应的模式标识就是 I 的小写字母 i所以不区分大小写的 cat 就可以写成 **(?i)**cat。
<img src="https://static001.geekbang.org/resource/image/f3/01/f30f895ed1754e1d1a576a59835b9701.png" alt="">
你看,和**[Cc][Aa][Tt] **相比,这样是不是清晰简洁了很多呢?
我们也可以用它来尝试匹配两个连续出现的 cat如下图所示你会发现即便是第一个 cat 和第二个 cat 大小写不一致,也可以匹配上。
<img src="https://static001.geekbang.org/resource/image/ee/e0/eeb074edfd6d052407130311aff8cae0.png" alt="">
我给到了你一个测试链接,你可以在这里试试不区分大小写模式:
[https://regex101.com/r/x1lg4P/1](https://regex101.com/r/x1lg4P/1)。
如果我们想要前面匹配上的结果,和第二次重复时的大小写一致,那该怎么做呢?我们只需要用括号把**修饰符和正则cat部分**括起来,加括号相当于作用范围的限定,让不区分大小写只作用于这个括号里的内容。同样的,我在[这里](https://regex101.com/r/x1lg4P/2)给你放了一个测试链接,你可以自己看一下。
<img src="https://static001.geekbang.org/resource/image/b6/7e/b630d1374a88eb1591f223e86f11c37e.png" alt=""><br>
需要注意的是,这里正则写成了 ((?i)cat) \1而不是((?i)(cat)) \1。也就是说我们给修饰符和cat整体加了个括号而原来 cat 部分的括号去掉了。如果 cat 保留原来的括号,即 ((?i)(cat)) \1这样正则中就会有两个子组虽然结果也是对的但这其实没必要。在上一讲里我们已经讲解了相关的内容如果忘记了你可以回去复习一下。
到这里我们再进阶一下。如果用正则匹配实现部分区分大小写另一部分不区分大小写这该如何操作呢就比如说我现在想要the cat 中的 the 不区分大小写cat 区分大小写。
通过上面的学习,你应该能很快写出相应的正则,也就是 (**(?i)**the) cat。实现的效果如下
<img src="https://static001.geekbang.org/resource/image/80/7e/802f69e2c0fddf341ada804c20a8767e.png" alt="">
我把部分区分大小写,部分不区分大小写的测试链接放在[这里](https://regex101.com/r/x1lg4P/3),你可以看一下。
有一点需要你注意一下,上面讲到的通过**修饰符指定匹配模式**的方式,在大部分编程语言中都是可以直接使用的,但在 JS 中我们需要使用 /regex/**i** 来指定匹配模式。在编程语言中通常会提供一些预定义的常量,来进行匹配模式的指定。比如 Python 中可以使用 re.IGNORECASE 或 re.I ,来传入正则函数中来表示不区分大小写。我下面给出了你一个示例,你可以看一下。
```
&gt;&gt;&gt; import re
&gt;&gt;&gt; re.findall(r&quot;cat&quot;, &quot;CAT Cat cat&quot;, re.IGNORECASE)
['CAT', 'Cat', 'cat']
```
到这里我简单总结一下不区分大小写模式的要点:
1. 不区分大小写模式的指定方式,使用模式修饰符 (?i)
1. 修饰符如果在括号内,作用范围是这个括号内的正则,而不是整个正则;
1. 使用编程语言时可以使用预定义好的常量来指定匹配模式。
## 点号通配模式Dot All
在基础篇的第一讲里,我为你讲解了元字符相关的知识,你还记得英文的点(.)有什么用吗?它可以匹配上任何符号,但不能匹配换行。当我们需要匹配真正的“任意”符号的时候,可以使用 [\s\S] 或 [\d\D] 或 [\w\W] 等。
<img src="https://static001.geekbang.org/resource/image/e2/84/e2184c4583ff4f32a14c1cb6f1bc9184.png" alt="">
但是这么写不够简洁自然,所以正则中提供了一种模式,让英文的点(.)可以匹配上包括换行的任何字符。
这个模式就是**点号通配模式**,有很多地方把它称作单行匹配模式,但这么说容易造成误解,毕竟它与多行匹配模式没有联系,因此在课程中我们统一用更容易理解的“点号通配模式”。
单行的英文表示是 **S**ingle Line单行模式对应的修饰符是 **(?s)**我还是选择用the cat来给你举一个点号通配模式的例子。如下图所示
<img src="https://static001.geekbang.org/resource/image/59/52/59b4b56c3d5852b3412185dc3a3de052.png" alt="">
需要注意的是JavaScript不支持此模式那么我们就可以使用前面说的[\s\S]等方式替代。在Ruby中则是用Multiline来表示点号通配模式单行匹配模式我猜测设计者的意图是把点.)号理解成“能匹配多行”。
## 多行匹配模式Multiline
讲完了点号通配模式,我们再来看看多行匹配模式。通常情况下,`^`匹配整个字符串的开头,$ 匹配整个字符串的结尾。多行匹配模式改变的就是 `^` 和 $ 的匹配行为。
<img src="https://static001.geekbang.org/resource/image/09/b9/09cbdacb73c7c66423a878f452c87fb9.png" alt="">
多行模式的作用在于,使 ^ 和 $ 能匹配上**每行**的开头或结尾,我们可以使用模式修饰符号 (?m) 来指定这个模式。
<img src="https://static001.geekbang.org/resource/image/e3/19/e3bf8bd8f9d594472a940d4a7e4f2f19.png" alt="">
这个模式有什么用呢?在处理日志时,如果日志以时间开头,有一些日志打印了堆栈信息,占用了多行,我们就可以使用多行匹配模式,在日志中匹配到以时间开头的每一行日志。
值得一提的是,正则中还有 \A 和 \zPython中是 \Z 这两个元字符容易混淆,\A 仅匹配整个字符串的开始,\z 仅匹配整个字符串的结束,在多行匹配模式下,它们的匹配行为不会改变,如果只想匹配整个字符串,而不是匹配每一行,用这个更严谨一些。
## 注释模式Comment
在实际工作中,正则可能会很复杂,这就导致编写、阅读和维护正则都会很困难。我们在写代码的时候,通常会在一些关键的地方加上注释,让代码更易于理解。很多语言也支持在正则中添加注释,让正则更容易阅读和维护,这就是正则的注释模式。正则中注释模式是使用(?#comment) 来表示。
比如我们可以把单词重复出现一次的正则 (\w+) \1 写成下面这样,这样的话,就算不是很懂正则的人也可以通过注释看懂正则的意思。
```
(\w+)(?#word) \1(?#word repeat again)
```
<img src="https://static001.geekbang.org/resource/image/44/ef/44f064fc587d5e5fd3538849b78039ef.png" alt="">
在很多编程语言中也提供了x模式来书写正则也可以起到注释的作用。我用Python3给你举了一个例子你可以参考一下。
```
import re
regex = r'''(?mx) # 使用多行模式和x模式
^ # 开头
(\d{4}) # 年
(\d{2}) # 月
$ # 结尾
'''
re.findall(regex, '202006\n202007')
# 输出结果 [('2020', '06'), ('2020', '07')]
```
需要注意的是在x模式下所有的换行和空格都会被忽略。为了换行和空格的正确使用我们可以通过把空格放入字符组中或将空格转义来解决换行和空格的忽略问题。我下面给了你一个示例你可以看看。
```
regex = r'''(?mx)
^ # 开头
(\d{4}) # 年
[ ] # 空格
(\d{2}) # 月
$ # 结尾
'''
re.findall(regex, '2020 06\n2020 07')
# 输出结果 [('2020', '06'), ('2020', '07')]
```
## 总结
最后,我来给你总结一下,正则中常见的四种匹配模式,分别是:不区分大小写、点号通配模式、多行模式和注释模式。
1. 不区分大小写模式,它可以让整个正则或正则中某一部分进行不区分大小写的匹配。
1. 点号通配模式也叫单行匹配,改变的是点号的匹配行为,让其可以匹配任何字符,包括换行。
1. 多行匹配说的是 ^ 和 $ 的匹配行为,让其可以匹配上每行的开头或结尾。
1. 注释模式则可以在正则中添加注释,让正则变得更容易阅读和维护。
<img src="https://static001.geekbang.org/resource/image/f3/a5/f36c2bca74f9bfcc54bb3e4ed53d4aa5.png" alt="">
## 思考题
最后我们来做一个小练习吧。HTML标签是不区分大小写的比如我们要提取网页中的head 标签中的内容,用正则如何实现呢?
<img src="https://static001.geekbang.org/resource/image/74/69/74504bef5656171815b42899f1600969.png" alt="">
你可以动手试一试,用文本编辑器或你熟悉的编程语言来实现,经过不断练习你才能更好地掌握学习的内容。
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论。也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。