# 正则表达式

正则测试站点 (opens new window)

# 正则的定义和功能

# 定义

正则,就是正则表达式,英文是 Regular Expression,简称 RE。顾名思义,正则其实就是一种描述文本内容组成规律的表示方式。

# 三大功能

  1. 校验数据的有效性
  2. 查找符合要求的文本
  3. 对文本进行切割和替换等操作

# 元字符

所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符,元字符是构成正则表达式的基本元件。

# 特殊单字符

. 任意字符(换行除外)

\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')]
上次更新: 2/13/2025, 3:29:47 AM