快速入门
教程
工具和语言
示例
参考
书评
正则表达式教程
简介
目录
特殊字符
不可打印字符
正则表达式引擎内部
字符类
字符类减法
字符类交集
简写字符类
单词边界
交替
可选项目
重复
分组和捕获
反向引用
反向引用,第 2 部分
命名组
相对反向引用
分支重置组
自由间距和注释
Unicode
模式修饰符
原子分组
独占量词
前瞻和后顾
环视,第 2 部分
将文本排除在匹配之外
条件
平衡组
递归
子例程
无限递归
递归和量词
递归和捕获
递归和反向引用
递归和回溯
POSIX 方括号表达式
零长度匹配
继续匹配
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书评
可打印 PDF
关于本网站
RSS Feed 和博客
RegexBuddy—Better than a regular expression tutorial!

零长度正则表达式匹配

我们看到,单词边界环视匹配位置,而不是匹配字符。这意味着当正则表达式仅包含一个或多个锚、单词边界或环视时,它可能导致零长度匹配。根据情况,这可能非常有用或不受欢迎。

例如,在电子邮件中,通常在引用的每行消息前加上“大于”符号和一个空格。在VB.NET中,我们可以使用Dim Quoted As String = Regex.Replace(Original, "^", "> ", RegexOptions.Multiline)轻松做到这一点。我们正在使用多行模式,因此正则表达式^匹配引用的消息开头和每个换行符之后。Regex.Replace 方法从字符串中删除正则表达式匹配,并插入替换字符串(大于符号和空格)。由于匹配不包含任何字符,因此不会删除任何内容。但是,匹配确实包含一个起始位置。替换字符串会插入到那里,就像我们想要的那样。

使用 ^\d*$ 测试用户是否输入数字时,会产生不良结果。它会导致脚本将空字符串作为有效输入接受。我们来看看原因。

空字符串中只有一个“字符”位置:字符串后的空位。正则表达式中的第一个标记是 ^。它匹配字符串后空位之前的位置,因为它前面是字符串前的空位。下一个标记是 \d*。星号的一个作用是使 \d(在本例中)变为可选。引擎尝试将 \d 与字符串后的空位匹配。这将失败。但星号将 \d 的失败变成零长度的成功。引擎继续执行下一个正则表达式标记,而不前进字符串中的位置。因此,引擎到达 $ 和字符串后的空位。它们匹配。此时,整个正则表达式已匹配空字符串,并且引擎报告成功。

解决方案是使用正则表达式 ^\d+$,其中包含适当的量词,要求至少输入一个数字。如果您始终确保您的正则表达式无法找到零长度匹配(除了匹配每行的开头或结尾等特殊情况),那么您可以省去阅读本主题其余部分的麻烦。

跳过零长度匹配

并非所有风格都支持零长度匹配。在 Delphi XE5 及更早版本中,TRegEx 类始终跳过零长度匹配。在 XE5 及更早版本中,TPerlRegEx 类默认也这样做,但允许你通过 State 属性更改此设置。在 Delphi XE6 及更高版本中,TRegEx 从不跳过零长度匹配,而 TPerlRegEx 默认不跳过它们,但仍然允许你通过 State 属性跳过它们。PCRE 默认查找零长度匹配,但如果你设置 PCRE_NOTEMPTY,它可以跳过它们。

在零长度正则表达式匹配后前进

如果正则表达式可以在字符串中的任何位置找到零长度匹配,它就会这样做。正则表达式 \d* 匹配零个或多个数字。如果主题字符串不包含任何数字,则此正则表达式会在字符串中的每个位置找到一个零长度匹配。它在字符串 abc 中找到 4 个匹配,每个字母前一个,字符串末尾一个。

当正则表达式可以在任何位置找到零长度匹配以及某些非零长度匹配时,事情变得棘手。假设我们有正则表达式 \d*|x,主题字符串 x1,并且正则表达式引擎允许零长度匹配。当遍历所有匹配时,我们会得到哪些匹配以及得到多少个匹配?答案取决于正则表达式引擎在零长度匹配后如何前进。无论哪种方式,答案都很棘手。

第一次匹配尝试从字符串开头开始。\d 无法匹配 x。但 * 使 \d 可选。第一个备选方案在字符串开头找到一个零长度匹配。到目前为止,所有允许零长度匹配的正则表达式引擎都执行相同的操作。

现在,正则表达式引擎处于棘手的情况。我们要求它遍历整个字符串以查找所有不重叠的正则表达式匹配。第一个匹配在字符串开头结束,第一次匹配尝试从那里开始。正则表达式引擎需要一种方法来避免陷入无限循环,该循环永远在字符串开头找到相同的零长度匹配。

大多数正则表达式引擎使用的最简单的解决方案是,如果前一个匹配为零长度,则从前一个匹配结束后的一个字符开始进行下一次匹配尝试。在这种情况下,第二次匹配尝试从字符串中 x1 之间的位置开始。 \d 匹配 1。到达字符串的末尾。量词 * 满足一次重复。 1 作为整体匹配返回。

另一种解决方案是 Perl 使用的,无论前一个匹配是否为零长度,总是从前一个匹配的末尾开始下一次匹配尝试。如果是零长度,引擎会记下这一点,因为它不允许在相同位置进行零长度匹配。因此,Perl 也从字符串的开头开始第二次匹配尝试。第一个备选方案再次找到一个零长度匹配。但这并不是一个有效的匹配,因此引擎通过正则表达式回溯。 \d* 被迫放弃其零长度匹配。现在尝试正则表达式中的第二个备选方案。 x 匹配 x,找到第二个匹配。第三次匹配尝试从字符串中 x 后的位置开始。第一个备选方案匹配 1,找到第三个匹配。

但正则表达式引擎还没有完成。在匹配 x 之后,它从字符串的末尾开始进行一次匹配尝试。在这里,\d* 也找到了一个零长度匹配。因此,根据引擎在零长度匹配后前进的方式,它会找到三个或四个匹配。

一个例外是 JGsoft 引擎。JGsoft 引擎在零长度匹配后前进一个字符,就像大多数引擎一样。但它有一个额外的规则,即跳过前一个匹配结束位置的零长度匹配,因此你永远不会在非零长度匹配的旁边立即进行零长度匹配。在我们的示例中,JGsoft 引擎只找到两个匹配:字符串开头处的零长度匹配和 1

Python 3.6 及更早版本在零长度匹配后会继续前进。gsub() 函数用于搜索和替换,它会跳过上一个非零长度匹配结束位置处的零长度匹配,但 finditer() 函数会返回这些匹配。因此,在 Python 中进行搜索和替换会得到与 Just Great Software 应用程序相同的结果,但列出所有匹配项时会在字符串末尾添加零长度匹配。

Python 3.7 改变了这一切。它像 Perl 一样处理零长度匹配。gsub() 现在会替换与另一个匹配相邻的零长度匹配。这意味着可以在 Python 3.7 和更早版本的 Python 之间找到零长度匹配的正则表达式不兼容。

PCRE 8.00 及更高版本和 PCRE2 通过回溯像 Perl 一样处理零长度匹配。它们不再像 PCRE 7.9 那样在零长度匹配后前进一个字符。

RPHP 中的正则表达式函数基于 PCRE,因此它们通过像 PCRE 一样回溯来避免卡在零长度匹配上。但 R 中用于搜索和替换的 gsub() 函数也会跳过上一个非零长度匹配结束位置处的零长度匹配,就像 Python 3.6 及更早版本中的 gsub() 一样。R 中的其他正则表达式函数和 PHP 中的所有函数都允许零长度匹配紧邻非零长度匹配,就像 PCRE 本身一样。

程序员注意事项

诸如 $ 之类的正则表达式本身可以在字符串末尾找到零长度匹配。如果你向引擎查询字符位置,它将返回字符串的长度(如果字符串索引从 0 开始)或长度+1(如果字符串索引在你的编程语言中从 1 开始)。如果你向引擎查询匹配的长度,它将返回零。

你必须注意,String[Regex.MatchPosition] 可能会导致访问冲突或分段错误,因为 MatchPosition 可以指向字符串后的空位。如果字符串中的最后一个字符是换行符,则在 多行模式 下使用 ^^$ 时也会发生这种情况。