正则学习笔记

正则学习笔记

    • 1. 前言
    • 2. 正则基础
      • 2.1 正则中的元字符
      • 2.2 正则中的 4 种常用模式
      • 2.3 正则中的分组和引用
    • 3. 正则引擎原理
    • 4. 正则使用小结
    • 5. 巨人的肩膀

1. 前言

正则表达式可以被视为一门短小的编程语言,在文本处理上有着独特的优势,可以说易学难精。

(PS:这段是碎碎念,可以跳过)以前自己也学过一段时间正则,并且也在一些场景简单用过正则,但是自己并未真正入门正则。直到这次,学完了极客时间专栏《正则表达式入门课》以及细读了《精通正则表达式》前 4 章,自己才真正觉得正则入门了。因为极客时间专栏的作者刚好是自己的师兄,所以师兄每次写完稿都会让我帮忙校验文稿,这个过程中自己配合《精通正则表达式》和师兄探讨了一些疑惑的点,加深了对正则表达式的理解,知道了正则引擎是如何驱动的,以及一些正则的使用技巧。

本文的主要目的是分享正则学习的一些心得总结,包括正则基础、正则引擎、正则使用小结,最后是正则学习的一些优质资源。

学习正则表达式有什么用呢?
正则表达式的主要应用体现在以下 3 个方面:

  1. 校验数据的有效性。如校验手机号、邮箱。
  2. 查找符合要求的文本内容。比如查找符合某规则的号码。
  3. 对文本进行切割,替换等操作。比如用连续的空白符切割。

正则是一门工具,不过它也是一把双刃剑。用得好可以大大提升工作效率,用得不好也会导致严重的性能问题,比如这篇文章一个由正则表达式引发的血案。用正则解决问题,会把一个问题变成两个问题。所以入门了正则也要慎用正则,不是所有问题都要用正则来解决。

2. 正则基础

正则可以视为一门短小的编程语言,有着自己的语法规则,如何去高效地学习和掌握这些规则呢?方法主要是:1. 分类记忆法;2. 多加练习。

正则是描述字符串结构的一种模式,我们希望正则能够正确、高效地定位、匹配、校验、替换我们需要的文本。

因此学习正则的过程中可以带着以下问题去学习:

  1. 正则是如何去匹配文本的?
  2. 正则是如何定位文本的?
  3. 如何利用正则高效的处理文本?

2.1 正则中的元字符

正则之所以强大,就是在于其可以匹配符合某个规则的问文本。正则除了通过普通字符来匹配文本,如正则 a 匹配文本 a,更重要的是其具有元字符。元字符是正则表达式组成的基本元件,具有特殊意义的专用字符。学习元字符,最好采用分类记忆法元字符主要分为以下 5 类:

  1. 特殊单字符
  2. 空白符
  3. 范围
  4. 量词
  5. 断言

其中,特殊单字符、空白符、范围、量词是实现匹配文本的,断言是实现定位用的。下面分类罗列如下:

特殊单字符:

字符 说明
. 匹配除换行符外的任意一个字符
\d 匹配任意一个数字(0-9)
\w 匹配任意一个字母、数字或下划线
\s 匹配任意一个空白符

注意:以上 \d \w \s 如果把小写变为大写,就是取相反的意思,比如 \D 表示任意一个非数字字符。这种取反面的思想很用的,尤其是在断言定位的时候。

这里推荐一个学习正则的网站,regex101,这个网站可以很好地练习正则表达式,并且能够调试正则表达式,看到正则表达式是如何一步一步匹配文本的。

空白符:

字符 说明
\s 匹配任意一个空白符
\t 匹配一个制表符
\n 匹配一个换行符
\f 匹配一个换页符
\v 匹配一个垂直制表符
\r 匹配一个回车符

注意:主要还是用 \s 来匹配空白符。
上面列出的都是匹配某一个或一类字符,这样显然还不够灵活,因此正则引入了表示范围的元字符,实现了更灵活的匹配组合:

字符 说明
| 或,如 ab|bc 匹配 abbc
[...] 多选一,匹配括号中任意一个元素
[a-z] 匹配 a 到 z 之间任意一个元素(按照 ASCII 表,包含 a,z)
[^...] 取反,匹配不能是括号中的任意一个元素

上面的元字符可以实现匹配一个或者一类字符,但是无法实现匹配重复的一个或者一类字符,因此正则引入了量词元字符,来更加简单的匹配一个或一类字符重复出现的情况。

字符 说明
* 出现 0 到多次
+ 出现 1 到多次
出现 0 或 1 次,如 colou?r
{m} 出现 m 次
{m} 至少出现 m 次
{m,n} 出现 m 到 n 次

注意:

  1. 量词的使用要注意其作用范围,其一般只作用于前面的那个字符,如正则 a+ 表示匹配字符 a 1 次或多次。如果量词跟在分组 () 后,那么量词的作用范围就是前面 () 内的内容。
  2. 量词默认是贪婪的,即极可能多的去匹配字符。如果要实现把量词变为非贪婪的,那么要在量词后加个 ?,这样量词就变为非贪婪了。

通过前面的元字符,我们可以灵活地实现正则的匹配了,但是还有个问题,那就是怎么去定位我们要开始匹配的位置?正则中是通过断言来实现的,断言包括单词边界、行的开始结束以及环视:

字符 说明
\b 匹配单词的边界
^ 匹配行的开始,多行模式时,可以匹配任意行开头
$ 匹配行的结束,多行模式时,可以匹配任意行结尾
\A 只匹配整个字符串的开始,不支持多行模式
\Z 只匹配整个字符串的结束,不支持多行模式
(?<=Y)X 匹配前面是 Y 的 X
(? 匹配前面不是 Y 的 X
(?=Y)X 匹配后面是 Y 的 X
(?!Y)X 匹配后面不是 Y 的 X

注意:

  1. 反向引用里面“断言是没有带过去的”,复用的部分只是匹配到的文本内容,这段很重要,也是自己也开始没有意识到的。
  2. 断言只是匹配位置,准确地说就是只负责定位,不会匹配任何字符!这点很重要。
  3. 环视中 \W 不等于 !\w,测试如下:
>>> import re
>>> text = 'He is a boy'
>>> re.findall(r'(?, text)
['He', 'is', 'a', 'boy']
>>> re.findall(r'(?<=\W)(\w+)(?=\W)', text)
['is', 'a']
>>> re.findall(r'(?<=[^\w])(\w+)(?=[^\w])', text)
['is', 'a']

所以,Python 的环视中 \W[^\w] 不能匹配行开头和行结尾,\W 等于 [^\w],但是不等于 !\w

综上,我们基本可以利用正则实现定位和匹配文本了。不过正则还引入了几种模式,来改变正则的匹配行为,使其更加灵活。

2.2 正则中的 4 种常用模式

  1. 不区分大小写模式(Case-Insensitive)。作用:正则不区分英文字母的大小写。修饰符:(?i)
  2. 点号通配模式(Dot All)。作用:英文的点号可以匹配任何字符,包括换行。修饰符:(?s)。很多地方称为”单行匹配模式“,其实和多行匹配没有联系。JavaScript 不支持,Ruby 中的 Multiline 其实是单行匹配模式。等价于 [\s\S][\w\W],但更简洁。
  3. 多行模式(Multiline)。作用:^$ 默认是匹配整个字符串的开头或结尾,多行模式使得他们能够匹配每行的开头或结尾。修饰符:(?m)^ 匹配行的开始,多行匹配时,可以匹配任意行开头。$ 匹配行的结束,多行匹配时,可以匹配任意行结尾。
  4. 注释模式(Comment)。作用:正则可能很复杂,编写和阅读维护困难,添加注释方便理解。修饰符:(?#comment)。示例:(\w+) \1(?#word repeat again)

2.3 正则中的分组和引用

前面的学习已经可以帮助我们利用正则去定位和匹配字符了,那么如何实现替换和引用匹配的字符呢?这时候分组和引用就派上用场了。

正则中分组是通过 () 实现的,我们用 () 把想要的部分括起来,即实现了分组。分组的编号是通过数 ( 开括号的个数确定的。分组后,我们后面就可以通过引用来实现对 () 内匹配的内容进行操作。一般是通过 \ + 分组编号来实现引用分组的,不过不同语言存在一些区别,具体可以查查语言的相关文档。
我们也可以给分组取一些名字,方便记忆和引用,这叫命名分组,格式是:(?P<名称>正则)

注意:不要滥用分组,使用分组会耗费更多的资源,增大开销,所以对于后面不需要引用的分组,我们可以用非捕获分组,语法是:(?:正则)

3. 正则引擎原理

正则的引擎分为两种,确定型有穷自动机(Deterministic finite automaton, DFA)和非确定型有穷自动机(Non-deterministic finite automaton, NFA)。我们使用的 Python 和 Java 等语言基本都是使用的 NFA 引擎,因为 NFA 支持捕获分组、引用、环视等功能,更加的灵活。所以掌握 NFA 的原理,能够帮助我们更好的写出高效的正则表达式。

正则引擎的原理部分是最难理解的一部分,也是最花时间的一部分。
简单的说,DFA 是以文本主导,文本只会遍历一次,如果正则中有多个分支,那么会进行并行的比较,记录所有的可能,返回最长的匹配结果。因此 DFA 的时间复杂度是线性的,匹配速度比 NFA 要快。 DFA 的缺点是预编译需要更多的时间和内存,不支持环视、分组、引用等功能。
NFA 是以正则表达式主导,同一段文本可能会被反复遍历比较,如果正则中存在多选分支,NFA 会依次比较各个分支,遇到匹配成功的就结束匹配,报告匹配成功。因此多选结构中,多选分支的顺序问题需要仔细考虑。

NFA 中存在回溯。这也是导致正则性能问题的关键所在。广义的回溯是指:正则遇到选择时,会选择其中一条路进行尝试,把剩下的选择记录下来,作为备用状态,如果这条路匹配失败,就启用备用状态,启用了备用状态进行尝试就叫回溯。一旦有一个选择匹配成功,就报告成功,丢弃所有的备用状态。但是要直到所有的选择都匹配失败,才报告匹配失败。

简单的说,回溯就是启用了备用状态。只有需要做选择时,才有可能发生回溯,比如使用了量词,范围,多选分支 | 等时就可能存在回溯。回溯不可怕,可怕的是分支太多,所以要控制分支。

注意:独占模式可能会发生广义的回溯,但是独占模式不会吐出已经匹配的字符。所以独占模式的匹配效率更高,有时独占模式会很有用。但是注意确认自己使用的语言是否支持独占模式。

这里推荐一个网站,regexper.com,该网站可以直观的看到正则表达式生成的自动机,知道正则匹配的流程。

关于 DFA 和 NFA,以及回溯、传动、引擎等正则的知识,这里我推荐一个大佬写的博客 正则基础之——NFA 引擎匹配原理。
这位博主大佬写的正则文章,质量极其的高,配图、配色、排版、深度、广度都是目前我见过所有正则文章里面最好的,内容深入浅出,强烈推荐学习。

4. 正则使用小结

掌握的正则的基础知识和基本原理,后面就是一些工作中的实践和思考了。下面罗列一些个人觉得有用的建议:

  1. 使用正则前,先了解自己所用的编程语言或者编辑器对正则的支持情况,最好先简单翻翻相关文档。
  2. 写正则表达式时,我们需要在对欲检索文本的了解程度与检索精确性之间求得平衡。我们必须权衡匹配符合要求的文本,同时忽略不符合要求的文本。通常,按照预期获得成功的匹配要花去一半的功夫,另一半的功夫用来考虑如何忽略那些不符合要求的文本。
  3. 不要写难懂的正则,要考虑日后的维护问题。要在可读性和性能之间做出平衡。
  4. 写出正则后可以测试性能,防止正则注入或者可能引发的性能问题。
  5. 提前编译好正则,如:Python 中用的 re.compile() 方法。
  6. 尽量准确地表示匹配范围。
  7. 避免不同分支出现重复。
  8. 匹配引号内的字符串最简单的方法是使用:"[^"]*"
  9. 量词中 *? 永远不会匹配失败,所以需要小心!

5. 巨人的肩膀

站在前人的肩膀,可以更加高效快速的学习,故推荐一下正则学习资料:

  1. 《精通正则表达式》第三版,这是正则领域的权威书籍,前 6 章讲解了正则的基础、匹配原理和优化手段。后面的章节可以根据自己使用的语言作为工具书参看。
  2. 极客时间专栏《正则表达式入门课》,详细的介绍了正则的基础、原理、和优化,同时提供了大量的练习和案例,也非常值得学习。
  3. 正则基础。这位博主的专栏质量极其高,深入浅出,图例做的很好,10 年前的文章,就算放到 10 年后,也是经典之作。
  4. 各语言自己的官方文档。这个就不用多说了。

你可能感兴趣的:(正则表达式)