Regex All in One

正则表达式

正则表达式 中的字符, 可以划分为 普通字符元字符, 其中, 元字符起到控制作用或者代表一个字符集等, 普通字符则表示其字面本义. 我们姑且将一个正则表达式的最小表意单元称为 “正则构造”.

元字符可以分 4 个大部分进行介绍:

  1. 字符集: 用简短的正则构造来表示字符的集合
  2. 量词: 限定一个正则构造的重复次数
  3. 子表达式: 在一个表达式内部具有完整正则特性的子表达式
  4. 断言: 断定正则表达式外部情况的正则构造

由于编程语言在源代码中定义字符串时, 会将反斜杠 \ 转义. 而正则表达式中又多用反斜杠表示正则表达式的转义行为. 因此, 在源代码中定义正则表达式, 大多需要使用 \\ 来表示一个在正则表达式中可用的转义符.

如果是通过流传递字符串给程序进行正则表达式的设置, 那么就不需要用双反斜杠来表示转义符. 因为已经不会发生 编译器/解释器 的转义行为了.

字符集

一个字符集, 将使用较简短的写法来表示一系列字符组成的集合. 例如, 对于一个字符来说, 要求此表达式匹配 *&,. 其中之一, 就可以将这些字符放在一个字符集中: [*&,\.].

自定义字符集

自定义字符集使用方括号 [] 作为边界字符, 用方括号包含的字符, 将被定义到一个字符集中:

r"" 是 Python 中原始字符串的写法, 表示此字符串中的特殊字符不会被 Python 转义. 由于正则表达式中也需要转义字符, 所以采用这样的写法避免 \\ 这样的表达方式出现.

1
2
# []
r"[*&,\.]"

上面这个字符集, 可以匹配 *, &, ,, . 四个字符.

在字符集中, 一些元字符会表示其字面含义, 例如 *, + 等, 但一些预定义字符集则会以自身扩充此字符集, 例如表示(任意字符) 的 ., 如果要表示其字面含义, 需要转义.

也可以使用字符编码来代替字符, 例如 \x20 代表 空格, 在 ASCII 范围内的字符都可如此表示. 如果表示 Unicode, 那么不同的语言也许存在不同的表示方法, 但绝大多数语言采用 \u编码 的形式.

区间

在字符集中, 可以使用 - 表示一个连续的字符序列区间, 例如:

1
2
r"[1-9]"
# 表示 [123456789]

这个序列的顺序是按照字符编码顺序来排序的. 支持 Unicode 的语言都遵守 Unicode 码点顺序.

也可以连续使用:

1
2
r"[0-9A-Za-z]"
# 表示所有数字以及大小写字母

补集

自定义字符集中, 也可以使用 条件来创建一个 不在其中的字符所组成的集合, 只需要在字符集的第一位使用 ^ 脱字符就好:

1
2
# [^]
r"[^0-9]"

这样的字符将表示 “不在方括号中的其他字符所组成的集合”.

预定义字符集

在大多数正则表达式实现中, 都预先定义了一系列常用的字符集:

字符集表示法 含义
\d [0-9], 数字
\D [^0-9], 非数字
\w 数字或字母
\W 非数字或字母
\s 空白字符, 例如 , \t, \v 等( \n 一般不包括在内, 除非进行了特殊设置).
\S 非空白字符
. 任意字符

一般都是 \小写字母 表示一个字符集, 而对应的 \大写字母 表示它的补集.

量词

量词, 用于限制一个正则构造的重复次数. 例如, 如果要表示一个 11 位的手机号码, 可以如何编写?

1
2
3
4
# 不使用量词
r"\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d"
# 使用量词
r"\d{11}"

量词使用花括号 {} 来进行表示. 量词可以是一个确定的数字, 也可以是一个区间.

m, n 表示正整数且 m < n

量词 含义
{m} 重复 m 次
{m, n} 重复 m~n 次
{m,} 重复至少 m 次

量词可以对字符, 字符集, 子表达式使用.

预定义量词

预定义量词 含义
* {0,}
+ {1,}
? {0,1}

量词的贪婪与懒惰

模式 含义
贪婪 对于一个被量词修饰的正则构造, 在 整个表达式可以被匹配 的前提下, 尽可能多地为当前正则构造多匹配字符. 依次读取字符, 当字符满足当前正则构造则将其匹配如当前构造, 当整个表达式的匹配不被满足时(下一个正则构造无法匹配到字符), 就将此正则构造所匹配的最后一个字符丢弃(回溯), 将丢弃的字符匹配给下一个正则构造, 一直重复, 直到整个表达式被匹配完全或者字符串耗尽(匹配失败).
懒惰 对于一个被量词修饰的正则构造, 在 整个表达式可以被匹配 的前提下, 为当前的正则构造尽可能少地匹配字符. 依次读取字符, 每次尝试不读入字符进行匹配, 如果失败, 则读入一个字符进行匹配. 重复循环, 直到整个表达式被匹配完全或者字符串耗尽(匹配失败).

正则表达式默认以贪婪模式进行匹配, 如果要将一个正则构造设置为懒惰, 则在对应的量词后 多加一个 ? 问号.

1
2
3
4
import re
regp = re.compile(r"\d{1,5}")
regp_l = re.compile(r"\d{1,5}?")
# 这两个正则表达式都匹配 1 ~ 5 个数字, 一个是贪婪的, 另一个是非贪婪的

两者分别进行匹配:

1
2
3
4
5
>>> string = "abc0123456efg"
>>> regp.search(string)
<re.Match object; span=(3, 8), match='01234'>
>>> regp_l.search(string)
<re.Match object; span=(3, 4), match='0'>

可以看到, regp 匹配满了 5 个, 才结束了匹配, 而 regp_l 只匹配了一个, 就结束了匹配.

当多个贪婪或懒惰的正则构造连用时, 满足规律(在整个表达式可成功匹配时):

  1. 每个构造都能满足最低需求
  2. 优先满足贪婪构造的最高需求
  3. 同为贪婪构造, 优先满足左侧(头部)构造的需求
  4. 若为懒惰构造, 则多余的部分被抛弃

子表达式

正则表达式中可以使用 () 圆括号来表示一个子表达式. 子表达式和完整的正则表达式具有相同的特性: 可以使用一切正则语法, 包括内嵌子表达式.

子捕获组

子表达式和正则表达式一样, 都是捕获的. 捕获的意思就是说, 对于一个成功匹配的正则匹配结果, 可以将表达式所匹配到的内容提取出来.

1
2
3
4
5
6
7
8
>>> import re
>>> string = "[email protected]"
>>> regp = re.compile(r"(\S+)@outlook.com")
>>> match = regp.match(string)
>>> match.group(0)
[email protected]
>>> match.group(1)
zombie110year

所有的捕获组都有对应的索引值. 完整的正则表达式具有索引值 0, 内部的子捕获组索引则按照 1,2,3,4… 这样的顺序依次递增. 如果存在内嵌的子表达式, 则索引值对应的顺序为:

  1. 从外向内
  2. 如果属于同一层, 则从左到右

非捕获组

非捕获组使用 (?:), 用于表示那些需要在正则表达式中匹配, 但是不计入捕获组计数中的子表达式:

1
2
3
4
5
6
7
8
9
10
>>> import re
>>> string = "[email protected]"
>>> regp = re.compile(r"(\S+)(?:@)([\w\.]+)")
>>> match = regp.match(string)
>>> match.group(0)
[email protected]
>>> match.group(1)
zombie110year
>>> match.group(2)
outlook.com

命名捕获组

可以为捕获组取一个名字, 以便通过其名称以字符串作为索引取出该捕获组内容. 命名捕获组采用 (?<name>pattern) 的语法. pattern 是要匹配的模式, name 是这个捕获组的命名.

1
2
3
4
5
6
7
8
9
10
11
>>> import re
>>> regp = re.compile(r"(?P<username>\S+)@(?P<domain>[\w\.]+)")
# Python 中的命名捕获组使用 (?P<name>pattern) 语法
>>> regp.match("[email protected]")
<re.Match object; span=(0, 25), match='[email protected]'>
>>> _.group('username'), _.group('domain')
('zombie110year', 'outlook.com')
>>> regp.match("[email protected]")
<re.Match object; span=(0, 23), match='[email protected]'>
>>> _.group('username'), _.group('domain')
('zombie110year', 'gmail.com')

条件或

条件或使用 | 竖线符. 它表示 “在当前表达式层级匹配竖线左侧或右侧的结构”.

条件或可用在最外层表达式中: "cat|dog" 既可以匹配 "cat", 又可以匹配 "dog".

也可以用在子表达式中: "gr(e|a)y" 可以匹配 "grey""gray".

如果多个条件或连用, 则表示在当前表达式层级下, 竖线所分割的不同区块的或关系: "tom|jerry|spike" 可以匹配 "tom""jerry""spike".

捕获组的引用

捕获组可以通过继续的程序调用, 以编号或命名方式引用(提取). 也可以在正则表达式内部进行引用.

正向引用

就是通过程序调用进行引用, 不同语言实现方法不同, 不多阐述.

反向引用

在正则表达式内部进行引用. 在编译正则表达式时使用.

  • 如果通过索引值引用, 使用 \number$number 的方法, 例如 \1 $1, \2 $2, …, 不同语言的语法不一定相同. 注意不要使用 \0, 这代表整个正则表达式, 根据不同语言的实现方式, 会导致匹配结果永远为空或者因无限递归而程序崩溃.
  • 如果通过命名引用, 使用 \k<name>${name} 的方式引用, 不同的语言语法不一定相同.
  • Python 使用 \number(?P=name) 的语法

反向引用用于表达连续出现的相同字符串. 例如, 从一个字符串中找到连续重复出现三次的相同结构:

1
2
3
4
5
>>> import re
>>> string = "abdfjaklsfasdfk k kfjakfn"
>>> regp = re.compile(r"(\w) \1 \1")
>>> regp.search(string)
<re.Match object; span=(14, 25), match='12k 12k 12k'>

断言

断言用于限制正则表达式之外的字符串情况. 对于一个正则表达式, 它匹配字符串会导致流的读写位置发生变化. 断言可用于表示一个 在/不在 某某字符串一侧的字符串, 并且要求读写位置只变化到匹配部分的末尾, 而断言部分不会被计算在内.

断言又被称作 “零宽断言”, 就是表达了 断言不计算在匹配结果之内, 而且不会引起读写位置的变化 这个含义.

断言一般都用在正则表达式的两端.

预定义断言

元字符 含义
^ 字符串或行的首部
$ 字符串或行的末尾
\b, \<, \> 一个单词的首尾, \<\> 是 GNU 环境中常用的断言, 分别表示 首 尾, 而 \b 则更通用.
\B 一个单词的内部.

自定义断言

断言模式 含义
正前瞻断言 (?= ) 此断言放置在正则表达式的末尾, 使正则表达式匹配断言前方的内容
正后顾断言 (?<= ) 此断言防止在正则表达式的开头, 使正则表达式匹配断言后方的内容
负前瞻断言 (?! ) 同 “正前瞻断言”, 但是此断言内的 pattern 为 “非” 含义
负后顾断言 (?<! ) 同 “正后顾断言”, 但是此断言内的 pattern 为 “非” 含义

各种编程语言中的正则表达式实现

编程语言 正则实现
Python re 模块
JavaScript 基本数据类型. 可以使用 /正则表达式/ 来直接进行定义, 但得转义正则表达式中的 / 斜杠, 在处理路径, URL 等字符串时非常麻烦. 在 ES5 标准后, 也可以使用 RegExp 构造函数来定义正则表达式.
C GNU C Library 中的 regex.h, 对应的目标代码在 glibc 中, 因此不用进行额外的链接. 非 GNU 环境需要自己想办法.
C++ C++11 标准, STL 中提供了 regex 功能. #include <regex> 即可使用
Go regexp 模块
Java java.util.regex 库.