正则表达式可以被视为一门短小的编程语言,在文本处理上有着独特的优势,可以说易学难精。
(PS:这段是碎碎念,可以跳过)以前自己也学过一段时间正则,并且也在一些场景简单用过正则,但是自己并未真正入门正则。直到这次,学完了极客时间专栏《正则表达式入门课》以及细读了《精通正则表达式》前 4 章,自己才真正觉得正则入门了。因为极客时间专栏的作者刚好是自己的师兄,所以师兄每次写完稿都会让我帮忙校验文稿,这个过程中自己配合《精通正则表达式》和师兄探讨了一些疑惑的点,加深了对正则表达式的理解,知道了正则引擎是如何驱动的,以及一些正则的使用技巧。
本文的主要目的是分享正则学习的一些心得总结,包括正则基础、正则引擎、正则使用小结,最后是正则学习的一些优质资源。
学习正则表达式有什么用呢?
正则表达式的主要应用体现在以下 3 个方面:
正则是一门工具,不过它也是一把双刃剑。用得好可以大大提升工作效率,用得不好也会导致严重的性能问题,比如这篇文章一个由正则表达式引发的血案。用正则解决问题,会把一个问题变成两个问题。所以入门了正则也要慎用正则,不是所有问题都要用正则来解决。
正则可以视为一门短小的编程语言,有着自己的语法规则,如何去高效地学习和掌握这些规则呢?方法主要是:1. 分类记忆法;2. 多加练习。
正则是描述字符串结构的一种模式,我们希望正则能够正确、高效地定位、匹配、校验、替换
我们需要的文本。
因此学习正则的过程中可以带着以下问题去学习:
正则之所以强大,就是在于其可以匹配符合某个规则的问文本。正则除了通过普通字符来匹配文本,如正则 a
匹配文本 a
,更重要的是其具有元字符。元字符是正则表达式组成的基本元件,具有特殊意义的专用字符。学习元字符,最好采用分类记忆法元字符主要分为以下 5 类:
其中,特殊单字符、空白符、范围、量词是实现匹配文本的,断言是实现定位用的。下面分类罗列如下:
特殊单字符:
字符 | 说明 |
---|---|
. |
匹配除换行符外的任意一个字符 |
\d |
匹配任意一个数字(0-9) |
\w |
匹配任意一个字母、数字或下划线 |
\s |
匹配任意一个空白符 |
注意:以上 \d
\w
\s
如果把小写变为大写,就是取相反的意思,比如 \D
表示任意一个非数字字符。这种取反面的思想很用的,尤其是在断言定位的时候。
这里推荐一个学习正则的网站,regex101,这个网站可以很好地练习正则表达式,并且能够调试正则表达式,看到正则表达式是如何一步一步匹配文本的。
空白符:
字符 | 说明 |
---|---|
\s |
匹配任意一个空白符 |
\t |
匹配一个制表符 |
\n |
匹配一个换行符 |
\f |
匹配一个换页符 |
\v |
匹配一个垂直制表符 |
\r |
匹配一个回车符 |
注意:主要还是用 \s
来匹配空白符。
上面列出的都是匹配某一个或一类字符,这样显然还不够灵活,因此正则引入了表示范围的元字符,实现了更灵活的匹配组合:
字符 | 说明 |
---|---|
| |
或,如 ab|bc 匹配 ab 或 bc |
[...] |
多选一,匹配括号中任意一个元素 |
[a-z] |
匹配 a 到 z 之间任意一个元素(按照 ASCII 表,包含 a,z) |
[^...] |
取反,匹配不能是括号中的任意一个元素 |
上面的元字符可以实现匹配一个或者一类字符,但是无法实现匹配重复的一个或者一类字符,因此正则引入了量词元字符,来更加简单的匹配一个或一类字符重复出现的情况。
字符 | 说明 |
---|---|
* |
出现 0 到多次 |
+ |
出现 1 到多次 |
? |
出现 0 或 1 次,如 colou?r |
{m} |
出现 m 次 |
{m} |
至少出现 m 次 |
{m,n} |
出现 m 到 n 次 |
注意:
a+
表示匹配字符 a
1 次或多次。如果量词跟在分组 ()
后,那么量词的作用范围就是前面 ()
内的内容。?
,这样量词就变为非贪婪了。通过前面的元字符,我们可以灵活地实现正则的匹配了,但是还有个问题,那就是怎么去定位我们要开始匹配的位置?正则中是通过断言来实现的,断言包括单词边界、行的开始结束以及环视:
字符 | 说明 |
---|---|
\b |
匹配单词的边界 |
^ |
匹配行的开始,多行模式时,可以匹配任意行开头 |
$ |
匹配行的结束,多行模式时,可以匹配任意行结尾 |
\A |
只匹配整个字符串的开始,不支持多行模式 |
\Z |
只匹配整个字符串的结束,不支持多行模式 |
(?<=Y)X |
匹配前面是 Y 的 X |
(? |
匹配前面不是 Y 的 X |
(?=Y)X |
匹配后面是 Y 的 X |
(?!Y)X |
匹配后面不是 Y 的 X |
注意:
\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
。
综上,我们基本可以利用正则实现定位和匹配文本了。不过正则还引入了几种模式,来改变正则的匹配行为,使其更加灵活。
(?i)
。[\s\S]
或 [\w\W]
,但更简洁。^
或 $
默认是匹配整个字符串的开头或结尾,多行模式使得他们能够匹配每行的开头或结尾。修饰符:(?m)
。^
匹配行的开始,多行匹配时,可以匹配任意行开头。$
匹配行的结束,多行匹配时,可以匹配任意行结尾。(?#comment)
。示例:(\w+) \1(?#word repeat again)
。前面的学习已经可以帮助我们利用正则去定位和匹配字符了,那么如何实现替换和引用匹配的字符呢?这时候分组和引用就派上用场了。
正则中分组是通过 ()
实现的,我们用 ()
把想要的部分括起来,即实现了分组。分组的编号是通过数 (
开括号的个数确定的。分组后,我们后面就可以通过引用来实现对 ()
内匹配的内容进行操作。一般是通过 \
+ 分组编号来实现引用分组的,不过不同语言存在一些区别,具体可以查查语言的相关文档。
我们也可以给分组取一些名字,方便记忆和引用,这叫命名分组,格式是:(?P<名称>正则)
。
注意:不要滥用分组,使用分组会耗费更多的资源,增大开销,所以对于后面不需要引用的分组,我们可以用非捕获分组,语法是:(?:正则)
。
正则的引擎分为两种,确定型有穷自动机(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 引擎匹配原理。
这位博主大佬写的正则文章,质量极其的高,配图、配色、排版、深度、广度都是目前我见过所有正则文章里面最好的,内容深入浅出,强烈推荐学习。
掌握的正则的基础知识和基本原理,后面就是一些工作中的实践和思考了。下面罗列一些个人觉得有用的建议:
re.compile()
方法。"[^"]*"
。*
和 ?
永远不会匹配失败,所以需要小心!站在前人的肩膀,可以更加高效快速的学习,故推荐一下正则学习资料: