Skip to content

正则表达式

正则表达式(简称 Regex)是一种用于描述文本匹配规则的字符串模式。他通过特定语法组合普通字符和特殊字符(元字符)来定义字符串的搜索、验证或替换逻辑。被广泛应用于文本处理、编程和数据分析领域。

发展历史

正则表达式的理论在十九世纪五十年代开始逐渐完善。在六十年代正式被 Ken Thompson(肯大叔,Unix、B语言、UTF8、GO语言的发明者)引入编辑器 QED、随后又引入到 Unix 上的 ed 编辑器,以及后面大名鼎鼎的 grep 工具。自此正则表达式开始被广泛使用。之后又出现了两大流派他们也被正式确定为 POSIX 规范:

  • BRE(Basic Regular Expression)即基本型正则表达式: grep、vi、sed 都属于 BRE
  • ERE(Extended Regular Expression)即扩展型正则表达式: egrep、awk 都属于 ERE,他最重要的特性就是元字符不需要转移就可以使用

而在编程语言领域 1987 年出现的 Perl 引入了PCRE(Perl Compatible Regular Expression)即 Perl 兼容的正则表达式,他兼容 ERE 语法并且引入了非贪婪量词(*?)、命名捕获组(?<name>...)等特性。这些创新性设计也被其他大多数编程语言接受。

Tips

正则表达式的发展本质上遵循 理论数学 -> 工程实践 -> 标准化 -> 功能扩展 的演化路径。

PCRE 表达式全集

以下是 PCRE 表达式全集,BRE 和 ERE 是 PCRE 的子集。

Tips

这里的介绍主要依托于 Python 版本的 RCRE

基础语法

基础语法中大家都兼容,而且 BRE 也不需要进行转义。

语法结构 说明 示例
普通字符 匹配任意普通字符(特殊字符需要转移) 如 a 匹配字符"a"
. 匹配非换行符(\n)外的所有字符 gr*p 匹配 gr 后接一个任意字符,然后是 p
^ 锚定行的开始 ^grep 匹配所有以grep开头的行
$ 锚定行的结尾 grep$ 匹配所有以grep结尾的行
[xyz] 字符集合,匹配所包含的任意一个字符,注意这可以匹配特殊字符 如要匹配一个点可以 [.]
[^xyz] 排除型字符集合,匹配不包含在其中的任意一个字符 [^.] 就是不包含点的字符串
[a-z]/[^a-z] 比较特殊的写法,匹配字母 -
[a-z]/[^a-z] 比较特殊的写法,匹配数字 -

量词

量词语法用于修饰前面的表达式来规定出现的次数的目的:

语法结构 说明 示例 兼容性说明
* 重复 0 次或多次 .* 表示匹配任意不包含换行符的字符串,可以是空字符串 BRE 需要转义为 \*
+ 重复 1 次或多次 .+ 表示匹配任意不包含换行符的字符串并且至少包含一个字符 BRE 需要转义为 \+
? 重复 0 次或 1 次 - BRE 不支持
{m} 重复 m 次 0{5} 匹配包含 5 个 0 的行 BRE 需要转义为 \{m\}
{m, } 至少 m 次 0{5,} 匹配至少包含 5 个 0 的行 BRE 需要转义为 \{m,\}
{m, n} 至少 m 次,不多于 n 次 0{5, 10} 匹配至少包含 5 个 0 的行且不多于 10 个 0 BRE 需要转义为 \{m, n\}

非贪婪匹配

默认情况下正则表达式是贪婪匹配,例如 0{5, 10} 中他会尽可能多的匹配 0 的数量(主要意义在于分组中使用),在 PCRE 中引入了一种新的语法来实现非贪婪匹配:

语法结构 说明 示例 兼容性说明
量词? 表示前面的量词非贪婪匹配 {m, n}? 表示匹配 m 个即可返回 PCRE 特有,grep 等需要特殊的参数来实现
Python
In [4]: re.findall(r'(a{2,4}?)', 'aaaabcd')
Out[4]: ['aa', 'aa']

In [5]: re.findall(r'(a{2,4})', 'aaaabcd')
Out[5]: ['aaaa']

分组和引用

他是一个很强大的功能,他能够实现对前面已经匹配的内容的后续引用:

语法结构 说明 兼容性说明
(pat) 基础分组 BRF需要转义 \(pat\)
(?:pat) 非捕获分组,仅用于组合表达式 -
(?P<name>pat) 命名捕获组 -
\1 反向引用分组捕获的结果 兼容
(?P=name) 方向引用命名分组捕获的结果 -
Python
# 相当于匹配两个重复的单词
# 命名匹配同理,他只是更加的可读和编写方便
In [7]: re.findall(r'(.+) \1', "a ha ha")
Out[7]: ['ha']

断言

断言分为前向断言和后向断言,他们是一个很高级的用法。还有就是断言本身并不消耗字符,也就是匹配的结果中是不包含断言中匹配到的字符的:

语法结构 说明
(?=...) 正向肯定断言(后面有则匹配)
(?!...) 正向否定断言(后面没有则匹配)
(?<=...) 反向肯定断言(前面有则匹配)
(?<!...) 反向肯定断言(前面没有则匹配)
Python
# 后面有 10 或 11 才匹配
In [11]: re.findall(r'Windows(?=10|11)', 'Windows11')
Out[11]: ['Windows']

# 前面有 $ 才匹配
In [12]: re.search(r'(?<=\$)\d+',  '$100')
Out[12]: ['100']

Tips

还有一种断言是 \b 来表示单词边界,他们被放置到转义序列中讲解

标识修饰符

用于修改正则表达式的一些行为,他们以 (?[aiLmsux]) 的形式放置在正则表达式的开头,其中可以包含一个或多个具体含义如下:

修饰符 含义
a 仅限 ASCII 匹配
i 忽略大小写
L 特定语言区域匹配
m 启用多行,控制 ^ $ 的行为
s .匹配所有字符
u Unicode 匹配
x 详细模式,允许使用(?#comment)添加注释

其他

语法结构 说明 兼容性说明
pat1\|pat2 分支,匹配 pat1 或者 pat2 BRF不支持
(?#comment) 添加注释 -

转义序列

转义序列主要有反斜杠(\)定义,他有几种用法:

  • 如果后面跟的不是数字或者字母,他用于消除该字符可能具有的特殊含义,核心用法就是将元字符作为普通字符看待,当然对于 \= 这样的形式也没什么意义他几乎等价于 =
Bash
$ ls | grep '\.bash'
  • 如果后面是 ASCII 数字和字母这具有特殊意义,还需要注意如果后面是非 ASCII 数字和字母特点就是大于 127 码点的字符则他被视为字面量,通过他也引入了很多特殊的用法其中包括

    • 表示非打印字符,这个通常是 ASCII 中规定的大多数编程语言都支持的方式
    转义序列 说明
    \a 响铃(BEL)
    \b 退格符(BS)
    \f 换页符(FF)
    \n 换行符(LF)
    \r 回车符(CR)
    \t 水平制表符(TAB)
    \v 垂直制表符(VT)
    \000 八进制数 000 表示的字符
    \xhh 十六进制数 hh 表示的字符
    • 如果跟数字表示引用,他需要配合分组来完成
    Python
    In [7]: re.findall(r'(.+) \1', "a ha ha")
    Out[7]: ['ha']
    
    • 还有就是用于通用角色匹配,他通常能够被元字符替代,但是由于他非常常用因此使用转义序列来表示来简化表达式
    转义序列 说明
    \d 任意十进制数,ASCII 模式下等价于 [0-9]
    \D 非十进制数的其他任意字符,ASCII 模式下等价于 [^0-9]
    \s 匹配任意空白符,ASCII 模式下等价于[ \t\n\r\f\v]
    \S 匹配任意不属于空白符外的字符,ASCII 模式下等价于[^ \t\n\r\f\v]
    \w 匹配被视为字母数组的字符,ASCII 下等价于 [a-zA-Z0-9_] 对应合法的标识符序列
    \W \w 取反
    • 最后就是一些简单的断言,他们通常也能够被元字符替代,但是有他们能够极大的简化表达式
    转义序列 说明
    \b 单词边界匹配,他视 \w 为单词,即所有的 \W 都属于单词边界,例如 \bat\b 将匹配到 'at' 'at.' '(at)' 'as at' 等但不能匹配到 'attempt'
    \B 匹配非单词边界,at\B 就可以匹配 'attempt' 但是没有办法匹配到 'at.' 这类的
    \A 匹配字符串开始,与 ^ 不同的是在多行模式下依然匹配字符串开始, ^ 则匹配行开始
    \Z 匹配字符串结尾,与 % 不同的是在多行模式下依然匹配字符串结尾, $ 则匹配行结尾

shell 中转义处理

在 shell 中他会默认给你进行转义,例如 grep \.bash 实际上发送给 grep 的是转义后的结果 .bash 这就违背了我们的意愿,而要想在 shell 中不转义可以使用单引号包围:

Bash
# 必须添加单引号
$ ls | grep '\.bash'

# 还有一种写法就是, 此时不需要单引号,当然单引号是一个很好的习惯
$ ls | grep [.]bash

Tips

因此在 SHELL 中推荐所有的正则表达式都用单引号包围,有点类似 Python 中的 r"" 字符串模式

元字符

整个正则表达式中存在 12 个元字符,他们被解释为不同的含义:

  • \: 通常用于转义字符,也可以跟普通字符来扩展语义
  • ^: 断言字符串的开始
  • $: 断言字符串的结束
  • .: 匹配任意除了 \n 外的所有字符
  • [ ]: 用于定义字符集合
  • |: 用于定义替代分支
  • ( ): 用于定义分组
  • *: 量词表示 0 或更多
  • +: 量词表示 1 或更多
  • ?: 量词表示 0 或 1,如果修饰量词表示非贪婪匹配
  • { }: 量词表示指定大小

而特殊的方括号用于定义字符集合,大多数元字符在其中都被解释为普通字符,当然也有一些例外:

  • \: 通用转义字符
  • ^: 必须位于第一个字符表示取反
  • -: 指定字符范围,标准的就是 a-z0-9 当然 1-6 也可以,他避免了用户写 [0123456789] 这样复杂的语法
  • [ ]: 匹配他们自身需要转义 \[ \]