? 是正则表达式中一个用途非常广泛的符号,能实现很多不同的功能。本文将带你深入了解它在正则表达式中的各种应用,并逐一讲解每种用法。


1. 量词的“非贪婪”匹配(取消贪婪)

在正则表达式中,量词(如 *+{n,m})默认是 贪婪的,即尽可能多地匹配字符。然而,在某些情况下,我们可能只希望匹配最少的字符,这时可以通过在量词后加上 ? 来将其转换为 非贪婪 模式。

1.1 贪婪匹配 vs 非贪婪匹配

贪婪匹配:
a.*b

这个表达式会尽可能多地匹配字符,直到找到最后一个 b。例如,匹配 a1234b5678b

非贪婪匹配:
a.*?b

这个表达式会匹配从 a 到第一个 b,即只匹配最少的字符,如 a1234b

解释

  • * 表示匹配 0 次或多次,贪婪会尽可能匹配更多字符。
  • *? 表示非贪婪匹配,尽量匹配最少字符。

常见的非贪婪量词:

  • *?:匹配 0 次或多次,尽量少匹配字符。
  • +?:匹配 1 次或多次,尽量少匹配字符。
  • ??:匹配 0 次或 1 次,尽量少匹配字符。
  • {n,m}?:匹配介于 nm 次之间,尽量少匹配字符。

2. 环视(Lookahead 和 Lookbehind)

环视是正则表达式中的高级特性,用于指定某个模式前后必须(或不能)出现的条件,而不消耗字符。环视可以分为四种类型:

2.1 正向环视(Positive Lookahead)(?=...)

正向环视用于匹配后面必须跟随某个模式,但不会包括该模式的内容。例如:

\d(?=\D)

这表示匹配一个数字,且它后面必须是一个非数字字符。

解释

  • (?=\D) 表示后面必须跟着一个非数字字符。
  • \d 匹配数字。

2.2 负向环视(Negative Lookahead)(?!...)

负向环视与正向环视相似,只是要求后面不能跟随某个模式。例如:

\d(?!\d)

这表示匹配一个数字,但它后面不能是另一个数字。

解释

  • (?!\d) 表示后面不能跟着另一个数字。
  • \d 匹配数字。

2.3 正向回溯环视(Positive Lookbehind)(?<=...)

正向回溯环视匹配某个位置,要求在此位置之前满足某个模式。例如:

(?<=@)\w+

这表示匹配一个单词(\w+),前提是它前面有一个 @ 符号。

解释

  • (?<=@) 表示匹配位置前面有 @ 符号。
  • \w+ 匹配一个单词字符(字母、数字或下划线)。

2.4 负向回溯环视(Negative Lookbehind)(?<!...)

负向回溯环视与正向回溯环视类似,只是要求匹配的前面不能是某个模式。例如:

(?<!@)\w+

这表示匹配一个单词(\w+),前提是它前面没有 @ 符号。

解释

  • (?<!@) 表示匹配位置前面没有 @ 符号。
  • \w+ 匹配一个单词字符。

3. 命名捕获组 (?P<name>...)

命名捕获组让你可以给捕获的分组指定名称,便于后续引用和匹配。通过命名捕获组,你不仅可以使正则表达式更具可读性,还可以更方便地引用捕获的内容。对于多个捕获分组,你可以通过组名来引用,而不必记住它们的编号。

例如:

(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})

这个正则表达式会捕获日期中的年份、月份和日期,并将它们命名为 yearmonthday,便于后续引用。

解释

  • (?P<year>\d{4}) 捕获 4 位数字并命名为 year
  • (?P<month>\d{2}) 捕获 2 位数字并命名为 month
  • (?P<day>\d{2}) 捕获 2 位数字并命名为 day

通过命名捕获组,你可以使用组名(如 year)来引用捕获内容,而不需要记住组号。

3.1 使用后向引用

在正则表达式中,后向引用(如 \1\2)用于引用之前捕获的分组。当正则表达式有多个分组时,后向引用会按顺序分配索引,例如 \1 引用第一个分组,\2 引用第二个分组,依此类推。在实际应用中,使用后向引用可以实现对重复内容的匹配。

如果你使用了命名捕获组,可以通过 \k<name> 来引用命名捕获组,避免使用数字索引,从而增强可读性。例如,\k<year> 引用名为 year 的捕获组。

举个例子:

(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})\k<year>

根据上面的正则表达式:最后的 \k<year> 表示匹配与 year 捕获组相同的内容。也就是说,这个正则要求日期的年份与后面的年份部分相同,形成一个对称的匹配。

匹配成功的文本示例:

  • 2025-08-15-2025
  • 1999-12-31-1999
  • 2023-01-01-2023

匹配失败的文本示例:

  • 2025-08-15-2024
  • 1999-12-31-2000
  • 2023-01-01-2024

需要注意的是,尽管命名捕获组可以通过名字引用,但它们仍会占用一个数字索引。所有捕获组都会根据它们在正则表达式中的位置分配索引,从 \1 开始。第一个捕获组(无论是命名的还是未命名的)会被分配为 \1,第二个为 \2,以此类推。因此,命名捕获组的作用主要是提升语义的清晰度,但它们仍然会按顺序分配数字索引。最后强调一点,环视不是捕获组,其本身也不存在后向引用的说法。


4. 非捕获分组 (?:...)

有时你只需要匹配某些内容,但不希望将其存储为捕获组,这时可以使用非捕获分组 (?:...)。它可以避免将匹配的内容存入内存,从而提升效率并使正则表达式更加简洁。

例如:

(?:\d{3})-(\d{2})-(\d{4})

这个正则表达式会匹配类似 123-45-6789 的号码。这里,(?:\d{3}) 是一个非捕获分组,因此不会创建捕获组,也就不会生成后向引用。而 (\d{2})(\d{4}) 分别是捕获组,匹配的内容可以通过 \1\2 来引用。

概念辨析:

  • 命名捕获组:使用 (?P<name>...) 来为捕获的分组命名,方便后续引用。对于有多个分组的正则表达式,使用命名捕获组比记住数字组号更易于管理。
  • 后向引用:使用 \1\2 等来引用先前捕获的分组,命名捕获组也可以使用 \k<name> 进行引用。
  • 非捕获分组:通过 (?:...) 创建不需要后续引用的分组,适用于只需要匹配但不捕获的情况。

5. 其他用途

5.1 可选匹配 ?

? 还可以用来表示前面的元素是 可选的,即它可以匹配 0 次或 1 次。例如:

colou?r

这个正则表达式会匹配 colorcolouru 字母是可选的。

5.2 条件表达式(Conditional Expressions)(?(id)yes-pattern|no-pattern)

条件表达式允许基于捕获组是否已匹配来决定使用哪个模式。例如:

^(?(1)\d{3}-\d{2}-\d{4}|\d{5})$

这个正则表示,如果捕获组 1 匹配了,就匹配类似 123-45-6789 的社会安全号码格式,否则匹配 5 位数字。


6. 总结

? 在正则表达式中有多种重要用途,具体包括:

  1. 非贪婪匹配:通过在量词后加 ?,将默认的贪婪模式转为非贪婪模式,尽量匹配最少的字符。
  2. 环视? 用于实现正向环视((?=...))、负向环视((?!...))、正向回溯环视((?<=...))和负向回溯环视((?<!...))。
  3. 命名捕获组:通过 (?P<name>...) 为捕获组命名,便于引用和提高正则的可读性。
  4. 非捕获分组:使用 (?:...) 创建非捕获分组,以避免不必要的内存占用。
  5. 可选匹配? 可用于将前面的元素或分组设为可选,即匹配 0 次或 1 次。
  6. 条件表达式(?(id)yes-pattern|no-pattern) 用于根据捕获组的匹配与否,决定使用不同的匹配模式。

通过灵活应用 ?,你可以编写更加精确、高效的正则表达式,满足各种复杂的匹配需求。

更多推荐