跳转至

正则表达式⚓︎

约 2746 个字 预计阅读时间 14 分钟

1. 什么是正则表达式 (Regular Expression)⚓︎

正则表达式(Regex)是用于描述字符排列和匹配规则的工具。换句话说,它是记录文本规则的代码。

  • 类比: 你可能使用过 Windows/Dos 下的文件查找通配符(Wildcard),如 *?。例如查找所有 Word 文档会搜 *.doc
  • 区别: 正则表达式比通配符更精确。通配符中的 * 代表任意字符串,而正则可以精确描述“以 0 开头,后跟 2-3 个数字,然后是连字号,最后是 7-8 位数字”这样的复杂规则。

2. 核心语法与元字符 (Metacharacters)⚓︎

元字符是正则表达式中具有特殊含义的字符,而不是字符本身。

2.1 常用元字符表⚓︎

掌握它们的关键在于:看它们是在“方括号外面”还是“方括号里面”

方括号分组[]⚓︎

12 个特殊字符分为三组。


第一组:[]内外不一样(3个):^, -, ]⚓︎

这几个字符在 [] 内外含义完全不同,是初学者最容易晕的地方。

字符 在 [] 外面 (默认) 在 [] 里面 (特殊语境) 例子对比
^ 行首锚点 匹配字符串的开头。 取反 (仅在开头时) 除了这些都匹配。 ^A: 必须以 A 开头 [^A]: 除了 A 以外的字符
- 普通字符 就是一个横线。 范围符号 (仅在中间时) 从 X 到 Y。 A-Z: 匹配 "A-Z" 这个文本 [A-Z]: 匹配大写字母
] 普通字符 (如果没有对应的 [ ) 结束符 表示方括号的结束。 []]: 匹配一个右方括号 (第一个 ] 必须转义或放首位)

第二组:在 [] 内普通(8个):., *, +, ?, (), |, {}, $⚓︎

这些字符在方括号外面法力无边,但一旦进入方括号里面,它们就变成了普普通通的标点符号。这是很多人的盲区!

字符 在 [] 外面 (元字符) 在 [] 里面 (变成普通字符)
. 通配符 匹配除换行外的任意字符。 小数点 只匹配 . 本身。
* 量词 重复 0 次或多次。 星号 只匹配 * 本身。
+ 量词 重复 1 次或多次。 加号 只匹配 + 本身。
? 量词 重复 0 次或 1 次 (可选)。 问号 只匹配 ? 本身。
( ) 分组 把几个字符绑在一起。 括号 只匹配 () 本身。
| 分支 (或) 左边或右边。
{ } 量词范围 例如 {3,5} 重复3到5次。 花括号 只匹配 {} 本身。
$ 行尾锚点 匹配字符串的结尾。 美元符号 只匹配 $ 本身。

极简记忆法:

如果你在 [] 里面想匹配一个点号,不需要写成 [\.],直接写 [.] 就行了。


第三组:万能-反斜杠 \⚓︎

\ (Escape Character) 是正则表达式中最大的 BOSS,不受[]控制。它有两个相反的作用:

  1. 让特殊的变普通: - 如果你想匹配一个真正的 .,必须写 \. - 如果你想匹配一个真正的 +,必须写 \+ - 如果你想匹配一个真正的 \,必须写 \\
  2. 让普通的变特殊: - d 是字母 d,但 \d 是数字。 - w 是字母 w,但 \w 是单词字符。 - s 是字母 s,但 \s 是空白符。

综合实战测试⚓︎

下面这个看似乱码的正则表达式,分析:

\([\^.]\+\)

拆解分析:

  1. []:这是一个字符集。
  2. \^:这里的 ^ 本来在 [ 开头表示“取反”,但因为前面加了反斜杠 \,它变成了普通的脱字符 ^
  3. .:在 [] 里面,点号自动失去法力,变成了普通的小数点 .
  4. +:在 [] 外面,它是量词,表示“前面的东西重复 1 次或多次”。

这个表达式匹配的是:由 ^ 符号或者 . 符号组成的连续字符串。

  • 匹配:^
  • 匹配:...
  • 匹配:^.^.
  • 匹配:abc (因为它没取反,只是匹配符号本身)

()分组⚓︎

主要是零宽断言的语法:?=<!

2.2 字符转义⚓︎

如果你想查找元字符本身(如 .*),需要使用 \ 进行转义。

  • 查找点号 .:使用 \. (Java 字符串写作 "\\.")
  • 查找星号 *:使用 \*
  • 查找反斜杠 \:使用 \\
  • 示例: deerchao\.cn 匹配 "deerchao.cn";C:\\Windows 匹配 "C:Windows"。

2.3 重复 (Quantifiers)⚓︎

用于指定前面的字符或分组出现的次数。

语法 说明 示例
* 重复零次或更多次 Windows\d+ 匹配 Windows 后跟至少 1 个数字
+ 重复一次或更多次 ^\w+ 匹配开头的第一个单词
? 重复零次或一次 home-?made 匹配 "homemade" 或 "home-made"
{n} 重复 n 次 \d{2} 匹配 2 个数字
{n,} 重复 n 次或更多次 \d{2,} 匹配至少 2 个数字
{n,m} 重复 n 到 m 次 \d{5,12} 匹配 5 到 12 位数字

2.4 字符类 (Character Classes)⚓︎

当没有预定义元字符时,使用方括号 [] 自定义字符集合。

  • [aeiou]: 匹配任意一个英文元音字母。
  • [.?!]: 匹配标点符号(. 或 ? 或 !)。
  • [0-9]: 等同于 \d
  • [a-z0-9A-Z_]: 等同于 \w (仅限英文环境)。

2.5 反义 (Negation)⚓︎

查找 不属于 某个字符类的字符。

语法 说明 示例
\W 匹配任意不是字母、数字、下划线、汉字的字符 [^\w]
\S 匹配任意不是空白符的字符 \S+ 匹配不包含空白符的字符串
\D 匹配任意非数字的字符 [^0-9]
\B 匹配不是单词开头或结束的位置 end\B 匹配 "ender" 中的 "end"
[^x] 匹配除了 x 以外的任意字符 [^aeiou] 匹配非元音字母
[^aeiou] 匹配除了 aeiou 这几个字母以外的任意字符
  • 示例: <a[^>]+> 匹配用尖括号括起来的以 a 开头的字符串。

3. 进阶规则与逻辑⚓︎

3.1 分支条件 (Alternation)⚓︎

使用 | 把不同的规则分隔开,满足其中任意一种即可。注意顺序:从左到右匹配,满足一个即停止。

  • 示例 1 (电话号码): 0\d{2}-\d{8}|0\d{3}-\d{7}
  • 匹配:3 位区号-8 位本地号 (010-12345678) 4 位区号-7 位本地号 (0376-2233445)。
  • 示例 2 (美国邮编): \d{5}-\d{4}|\d{5}
  • 匹配:5 位数字,或者 5 位-4 位。
  • 陷阱: 如果写成 \d{5}|\d{5}-\d{4},对于 9 位邮编只会匹配前 5 位(因为左边先满足了)。优先满足左边,和 if 的优先满足左侧表达式一致

3.2 分组 (Grouping)⚓︎

使用小括号 () 将多个字符作为子表达式(分组),可以对其使用重复量词。

  • 简单 IP 匹配: (\d{1,3}\.){3}\d{1,3}

  • 含义:(1-3位数字 加上点号) 重复 3 次,最后再加上 1-3位数字

  • 缺陷: 也会匹配 "256.300.888.999" 这种非法 IP。

  • 精确 IP 匹配:

((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
  • 解析 2[0-4]\d: 200-249
  • 解析 25[0-5]: 250-255
  • 解析 [01]?\d\d?: 0-199 (包括 0-9, 00-99, 100-199)

3.3 后向引用 (Back References)⚓︎

捕获组的内容可以在表达式后续部分被再次引用。

  • 原理:分组后,正则会在内存里为每个组依次匹配一个组号,可以调用组号 此外,组名还可以自己命名(不用默认组号)
  • 默认组号: 从左向右,以左括号 ( 出现顺序编号,1, 2, 3...
  • 引用语法: \1 代表分组 1 匹配到的文本。
  • 示例: \b(\w+)\b\s+\1\b
  • 匹配重复单词,如 "go go", "kitty kitty"。
  • 命名分组 (Named Groups):
  • 语法: (?<Word>\w+)(?'Word'\w+)
  • 引用: \k<Word>
  • 示例: \b(?<Word>\w+)\b\s+\k<Word>\b
//txt 图示
正则式:    (  [A-Z]{3}  )    -    \1
            └───┬────┘         │    └─ 指向分组1匹配到的真实值
                │              │
    Step 1: 引擎尝试匹配并捕获 ───┘
            匹配到 "TOM"
            此时内存 \1 = "TOM"
    Step 2: 遇到横杠 "-"
    Step 3: 遇到 \1
            引擎检查接下来的字符是不是 "TOM"
            如果是 -> 匹配成功!
            如果是 "JRY" -> 匹配失败。
------------------------------------------------
//嵌套分组
(  ( A ) ( B )  )
  │  │     │
  │  │     └─ \3
  │  └─────── \2
  └────────── \1
------------------------------------------------
//对比分析
表达式: ([a-z]) \1
过程:
  1. ([a-z]) 匹配到了 'd'
  2. 内存分组1 = 'd'
  3. \1 检查下一个字符是否为 'd'
  4. 结果匹配: "dd"

表达式: ([a-z]){2} \1
过程:
  1. ([a-z]) 第一次匹配到 'a'
  2. ([a-z]) 第二次匹配到 'b' (覆盖了 'a')
  3. 内存分组1 = 'b'
  4. \1 检查下一个字符是否为 'b'
  5. 结果匹配: "abb"

3.4 零宽断言 (Zero-width Assertions)⚓︎

用于查找在某些内容之前或之后的位置(类似 \b, ^, $,不消费字符,只匹配位置)。

  • 零宽 (Zero-width): 它不占位。匹配完成后,正则的“指针”停留在原处,不会跳过这些字符。
  • 断言 (Assertion): 它的作用是判断。就像在说:“我要求这个位置的前面(或后面)必须(或不能)是某些内容。”
名称 语法 说明 示例
正预测先行 (?=exp) 断言自身位置 后面 能匹配 exp \b\w+(?=ing\b) 匹配 "singing" 中的 "sing" (查找 ing 前面的部分)
正回顾后发 (?<=exp) 断言自身位置 前面 能匹配 exp (?<=\bre)\w+\b 匹配 "reading" 中的 "ading" (查找 re 后面的部分)
负预测先行 (?!exp) 断言自身位置 后面不能 匹配 exp \d{3}(?!\d) 匹配后面不跟数字的 3 位数字
负回顾后发 (?<!exp) 断言自身位置 前面不能 匹配 exp (?<![a-z])\d{7} 匹配前面不是小写字母的 7 位数字
  • 复杂示例: \b\w*q[^u]\w*\b 匹配包含 q 但 q 后面不是 u 的单词。
  • : 处理结尾 q 需更严谨,上面的表达式中,若 q 在结尾会出错。
  • 更严谨写法: \b((?!abc)\w)+\b 匹配不包含连续字符串 abc 的单词。
//e.g.1
表达式:\b\w+(?=ing\b)
- 匹配以ing结尾的单词的"前面部分"
- I'm singing while you're dancing.
- 它会匹配sing和danc。

表达式:(?<=\bre)\w+\b
- 匹配以re开头的单词的后半部分
- reading a book
- 匹配ading
//e.g.2--负向
表达式:\b\w*q[^u]\w*\b
- 会匹配Iraq fighting(其中[^u]匹配中间的空格了)
表达式:\b\w*q(?!u)\w*\b(判断但是不占位)

表达式:(?<![a-z])\d{7}

▲难度升级--表达式:(?<=<(\w+)>).*(?=<\/\1>)
- (?<=<(\w+)>):被尖括号括起来的单词(比如可能是<b>)
- .*(任意的字符串)
- (?=<\/\1>):\/是字符转义;\1则是一个反向引用
- 整个表达式匹配的是<b>和</b>之间的内容
- ☆☆☆☆☆☆再次提醒,不包括前缀和后缀本身

3.5 贪婪与懒惰 (Greedy vs Lazy)⚓︎

  • 贪婪 (默认): 匹配尽可能多的字符。
  • a.*b 匹配 aabab 的整个字符串 aabab
  • 懒惰 (Reluctant): 匹配尽可能少的字符。在限定符后加 ?
  • a.*?b 匹配 aabab 中的 aab (第一段) 和 ab (第二段)。
  • 常用懒惰限定符: *?, +?, ??, {n,m}?

3.6 注释⚓︎

使用 (?#comment) 包含注释。

  • 示例: 2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)
  • 建议开启“忽略模式里的空白符”选项以便排版。这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。

例如,我们可以前面的一个表达式(?<=<(\w+)>).*(?=<\/\1>)写成这样:

1
2
3
4
5
6
7
8
9
      (?<=    # 断言要匹配的文本的前缀
      <(\w+)> # 查找尖括号括起来的内容
              # (即HTML/XML标签)
      )       # 前缀结束
      .*      # 匹配任意文本
      (?=     # 断言要匹配的文本的后缀
      <\/\1>  # 查找尖括号括起来的内容
              # 查找尖括号括起来的内容
      )       # 后缀结束

4. 实战案例解析 (PPT 案例)⚓︎

案例 1: 复杂电话号码匹配⚓︎

需求: 匹配如 (010)88886666,022-22334455,02912345678。

正则:

\(?0\d{2}[) -]?\d{8}

分析:

  1. \(?: 转义左括号,出现 0 或 1 次。
  2. 0: 必须以 0 开头。
  3. \d{2}: 接着 2 个数字。
  4. [) -]?: 右括号、空格或连字号中的一个,出现 0 或 1 次。
  5. \d{8}: 最后 8 个数字。
  • : 此表达式也会匹配 010)12345678 这种不规范格式,更严谨需用分支条件。
  • 改进版 (分支条件): \(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}

案例 2: 作业练习⚓︎

任务: 在一段文本中查找电话、Email、URL、IP,并转为带超链接的 HTML。

  • 电话: 支持国内固话和手机。
  • URL: 支持 http://, https://, ftp:// 及无前缀网址。
  • Email: 标准邮箱格式。
  • IP: IPv4 (可选 IPv6)。
  • 输出格式: <a href="...">显示文本</a>
  • 参考思路:
  • 手机: 1[3-9]\d{9}
  • Email: \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
  • URL: (https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]

5. 记忆方法 (Mnemonics)⚓︎

正则元字符通常是英文单词的首字母缩写,理解这些词源能帮你快速记忆。

5.1 常用缩写⚓︎

  • \w - Word (单词)
  • 记忆逻辑: 代表构成“单词”的字符。
  • 解释: 在编程变量命名(Identifier)或英语单词中,最常用的字符就是字母、数字和下划线。虽然它不包含标点符号,但它包含了写一个“词”所需的基本元素。
  • \d - Digit (数字)
  • 记忆逻辑: D igit 的首字母。
  • 解释: 纯粹的数字 (0-9)。
  • \s - Space (空白)
  • 记忆逻辑: S pace 的首字母。
  • 解释: 看不见的“留白”,包括空格、制表符(Tab)、换行符。
  • \b - Boundary (边界)
  • 记忆逻辑: B oundary 的首字母。
  • 解释: 它不匹配任何字符,只匹配“单词与非单词之间的 界限”。

5.2 大小写互补规则 (Not)⚓︎

正则中有一个通用的规律:大写字母通常代表小写字母的“反义”(Not)

  • \w (Word) <-> \W (Not Word / 非单词字符)
  • \d (Digit) <-> \D (Not Digit / 非数字)
  • \s (Space) <-> \S (Not Space / 非空白)

5.3 位置符号⚓︎

  • ^ (Caret / 开头)
  • 记忆方法:
    1. 键盘上 6 的上面是个向上的箭头,代表“指向上方/顶端”,引申为 开头
    2. 这是个鼻子的形状,鼻子长在脸的最 前面
  • $ (Dollar / 结尾)
  • 记忆方法:
    1. 想象这是“结账”的时刻。钱($)总是最后才付的,所以代表结尾
    2. 在美国的日期写法或某些脚本中,变量结束符有时与 $ 有关,象征末尾。

常规表达式中不代表字符本身的:

序号 名称 字符 []外(常规情况下) []内 ()内
1 通配符 .
2 重复1 *
3 重复2 +
4 重复3 ?
5 括号1 ()
6 括号2 []
7 括号3 {}
8 分组 |
9 转义 \
10 开始 ^
11 结束 $
12 连接符 -
13 感叹号 !
14 <
15 =
16 #