# 正则表达式
# 正则的定义和功能
# 定义
正则,就是正则表达式,英文是 Regular Expression,简称 RE。顾名思义,正则其实就是一种描述文本内容组成规律的表示方式。
# 三大功能
- 校验数据的有效性
- 查找符合要求的文本
- 对文本进行切割和替换等操作
# 元字符
所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符,元字符是构成正则表达式的基本元件。
# 特殊单字符
. 任意字符(换行除外)
\d 任意数字 \D 任意非数字 测试案例 (opens new window)
\w 任意字母数字下划线 \W 任意非字母数字下划线 测试案例 (opens new window)
\s 任意空白符 \S 任意非空白符
# 空白符
\r 回车符
\n 换行符
\f 换页符
\t 制表符
\v 垂直制表符
\s 任意空白符
# 范围
| 或,如ab|bc代表ab或bc
[...] 多选一,括号中任意单个元素
[a-z] 匹配a到z之间任意单个元素
[^...] 取反,不能是括号中的任意单个元素
# 量词
* 0到多次
+ 1到多次
? 0到1次
{m} 出现m次
{m,} 出现至少m次
{m,n} m到n次
# 断言
在有些情况下,我们对要匹配的文本的位置也有一定的要求。为了解决这个问题,正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言。
常见的断言有三种:
- 单词边界
- 行的开始或结束
- 环视
# 单词边界(Word Boundary)
比如:tom 匹配 tom tomorrow,默认会匹配到两个。如果我们只想匹配前一个tom怎么办呢?
单词的组成一般可以用元字符 \w+ 来表示,\w 包括了大小写字母、下划线和数字(即 [A-Za-z0-9_])。那如果我们能找出单词的边界,也就是当出现了\w 表示的范围以外的字符,比如引号、空格、标点、换行等这些符号,我们就可以在正则中使用\b 来表示单词的边界。 \b 中的 b 可以理解为是边界(Boundary)这个单词的首字母。
\btom表示以tom开头,tom\b表示以tom结尾,\btom\b表示单词tom。
# 行的开始或结束
如果我们要求匹配的内容要出现在一行文本开头或结尾,就可以使用 ^ 和 $ 来进行位置界定。
在多行模式下,^ 和 $ 符号可以匹配每一行的开头或结尾。大部分实现默认不是多行匹配模式,但也有例外,比如 Ruby 中默认是多行模式。所以对于校验输入数据来说,一种更严谨的做法是,使用 \A 和 \z (Python 中使用 \Z) 来匹配整个文本的开头或结尾。
# 环视( Look Around)
环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则,有些地方也称环视为零宽断言。
- (?<=Y) 左边是Y,
(?<=\d)th要求左边是数字,比如9th - (?<!Y) 左边不是Y,
(?<!\d)th要求左边不是数字,比如health - (?=Y) 右边是Y,
six(?=\d)要求右边是数字,比如six6 - (?!Y) 右边不是Y,
hi(?!\d)要求右边不是数字,比如high
口诀:左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思。
环视中虽然也有括号,但不会保存成子组。保存成子组的一般是匹配到的文本内容,后续用于替换等操作,而环视是表示对文本左右环境的要求,即环视只匹配位置,不匹配文本内容。
# 贪婪与非贪婪
# 贪婪匹配
在正则中,表示次数的量词默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。
# 非贪婪匹配
那么如何将贪婪模式变成非贪婪模式呢?我们可以在量词后面加上英文的问号 (?),非贪婪模式会尽可能短地去匹配。
# 举例
In [1]: import re
# 贪婪模式
In [2]: re.findall(r'".+"', '"hello" seems "hi"')
Out[2]: ['"hello" seems "hi"']
# 非贪婪模式
In [3]: re.findall(r'".+?"', '"hello" seems "hi"')
Out[3]: ['"hello"', '"hi"']
# 独占模式
python,go原生不支持独占模式,python可以通过regex包实现
# 回溯
对于下面的例子:
import regex
regex.findall(r'xy{1,3}yz', 'xyyz')
首先xy{1,3}匹配到xyy字符,但发现yz无法匹配到剩余的z字符,此时会发生回溯,即xy{1,3}匹配到xy字符,由yz匹配剩下的yz字符
回溯会根据需要「返回部分已经匹配到的字符」以匹配接下来的字符。
当匹配的字符很长的时候,回溯会造成大量CPU占用,比如xy{1,5}yyyyz去匹配xyyyyyz时,要经历18个步骤,因为xy{1,5}由于贪婪匹配会匹配到xyyyyy然后发现z不能匹配,又会不断尝试yz,yyz,yyyz是否可以匹配。
# 独占模式
独占模式:在贪婪模式的基础上取消了回溯的机制,可以在量词的基础上添加+号实现。
对于xy{1,5}+yyyyz去匹配xyyyyyz时,首先xy{1,5}+贪婪匹配到xyyyyy,剩下的z无法匹配,于是匹配失败,不再进行回溯。
import regex
import regex
regex.findall(r'xy{1,5}+yyyyz', 'xyyyyyz')
# []
# 分组与引用
# 分组与编号
括号在正则中可以用于分组,被括号括起来的部分“子表达式”会被保存成一个子组。
那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。
# 不保存子组
在括号里面的会保存成子组,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用?: 不保存子组,此时括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。
比如\d{15}(?:\d{3})
# 括号嵌套
在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组怎么办?不要担心,其实方法很简单,我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组。
比如((\d{4})-(\d{2}))中,分组1为((\d{4})-(\d{2})),分组2为(\d{4}),分组3为(\d{2})。
# 命名分组
由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,还可能导致编号发生变化,因此一些编程语言提供了命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。命名分组的格式为(?P<分组名>正则)。
比如django中,url(r'^profile/(?P<username>\w+)/$', view_func)
命名分组和前面一样,也会分配一个编号,不过你可以使用名称,不用编号。
# 分组引用
知道了分组引用的编号 (number)后,大部分情况下,我们就可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引用,而 JavaScript 中是通过$编号来引用,如$1。
# 在查找中使用
re.search(r'(\w+) \1', 'hello hello world')
# <re.Match object; span=(0, 11), match='hello hello'>
# 在替换中使用
test_str = "2020-05-10 20:23:05"
regex = r"((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))"
subst = r"日期\1 时间\5 \2年\3月\4日 \6时\7分\8秒"
re.sub(regex, subst, test_str)
# 日期2020-05-10 时间20:23:05 2020年05月10日 20时23分05秒
# 匹配模式
所谓匹配模式,指的是正则中一些改变元字符匹配行为的方式,比如匹配时不区分英文字母大小写。
常见的匹配模式有 4 种,分别是
- 不区分大小写模式
- 点号通配模式
- 多行模式
- 注释模式。
# 不区分大小写模式(Case-Insensitive)
模式修饰符是通过 (? 模式标识) 的方式来表示的。 我们只需要把模式修饰符放在对应的正则前,就可以使用指定的模式了。
在不区分大小写模式中,由于不分大小写的英文是 Case-Insensitive,那么对应的模式标识就是 I 的小写字母 i,所以不区分大小写的 cat 就可以写成(?i)cat。 测试案例 (opens new window)
如果我们想要前面匹配上的结果,和第二次重复时的大小写一致,那该怎么做呢?我们只需要用括号把修饰符和正则 cat 部分括起来,加括号相当于作用范围的限定,让不区分大小写只作用于这个括号里的内容。测试案例 (opens new window)
如果用正则匹配,实现部分区分大小写,另一部分不区分大小写,这该如何操作呢?就比如说我现在想要,the cat 中的 the 不区分大小写,cat 区分大小写,可以写成((?i)the) cat 。测试案例 (opens new window)
JS中使用/regex/i 来指定匹配模式
Python中使用re.findall(r"cat", "CAT Cat cat", re.IGNORECASE)来指定
# 点号通配模式(Dot All)
你还记得英文的点(.)有什么用吗?它可以匹配上任何符号,但不能匹配换行。当我们需要匹配真正的“任意”符号的时候,可以使用 [\s\S] 或 [\d\D] 或 [\w\W] 等。
但是这么写不够简洁自然,所以正则中提供了一种模式,让英文的点(.)可以匹配上包括换行的任何字符。
这个模式就是点号通配模式,有很多地方把它称作单行匹配模式,但这么说容易造成误解,毕竟它与多行匹配模式没有联系。
单行的英文表示是 Single Line,单行模式对应的修饰符是(?s),比如(?s).+
JS不支持此模式
# 多行匹配模式(Multiline)
通常情况下,^匹配整个字符串的开头,$ 匹配整个字符串的结尾。多行匹配模式改变的就是 ^ 和 $ 的匹配行为。
多行模式的作用在于,使 ^ 和 $ 能匹配上每行的开头或结尾,我们可以使用模式修饰符号(?m)来指定这个模式。
比如(?m)^the|cat$
这个模式有什么用呢?在处理日志时,如果日志以时间开头,有一些日志打印了堆栈信息,占用了多行,我们就可以使用多行匹配模式,在日志中匹配到以时间开头的每一行日志。
值得一提的是,正则中还有 \A 和 \z(Python 中是 \Z) 这两个元字符容易混淆,\A 仅匹配整个字符串的开始,\z 仅匹配整个字符串的结束,在多行匹配模式下,它们的匹配行为不会改变,如果只想匹配整个字符串,而不是匹配每一行,用这个更严谨一些。
# 注释模式(Comment)
我们在写代码的时候,通常会在一些关键的地方加上注释,让代码更易于理解。很多语言也支持在正则中添加注释,让正则更容易阅读和维护,这就是正则的注释模式。正则中注释模式是使用(?#comment)来表示。
比如(\w+)(?#word) \1(?#word repeat again)
# x模式
在 x 模式下,所有的换行和空格都会被忽略。
为了换行和空格的正确使用,我们可以通过把空格放入字符组中,或将空格转义来解决换行和空格的忽略问题。
很多编程语言中也提供了 x 模式来书写正则,也可以起到注释的作用。
regex = r'''(?mx)
^ # 开头
(\d{4}) # 年
[ ] # 空格
(\d{2}) # 月
$ # 结尾
'''
re.findall(regex, '2020 06\n2020 07')
# 输出结果 [('2020', '06'), ('2020', '07')]