27、《正则表达式深入解析与应用》上半部分

《正则表达式深入解析与应用》上半部分

正则表达式在编程中是一个强大的工具,特别是在 .NET 和 PowerShell 环境中。下面将详细介绍正则表达式的访问、选项以及调试等相关内容。

1. 正则表达式的访问与特殊模式

在正则表达式的使用中,有一些特殊的匹配情况和模式值得关注。
- 从右到左匹配 :引擎会以从后到前的顺序处理标记和字符串。虽然两者都被反转,但匹配结果仍然是连贯的。例如,在某些情况下,从右到左的匹配结果是按照字符串中原本的顺序呈现,如匹配结果为 ‘c2’ 而不是 ‘2c’。在这种模式下,由于引擎从字符串末尾开始搜索,模式中较后的贪婪标记会先进行匹配,因此两个能匹配相同字符的贪婪标记在从右到左模式下的字符共享方式会有所不同。不过,捕获的索引分配仍然是从左到右的,即左侧的 L 组编号为 1,右侧的 R 组编号为 2,并且环视的方向不会改变。另外,在一些 .NET 方法中可用的起始索引,仍然是相对于字符串起始位置的偏移量,只是在从右到左模式下,引擎会从该索引开始向后搜索,从而产生不同的行为。
- RegexOptions.ECMAScript 模式 :.NET 和 PowerShell 的正则表达式引擎支持模仿 ECMAScript 的替代操作模式,在这种模式下,引擎主要有以下三种不同的行为:
- 无 Unicode 支持 :与默认状态下支持 Unicode 匹配不同,在 ECMAScript 模式下,一些字符类的匹配范围发生了变化:
- \w 仅匹配 [A - Za - z0 - 9_]
- \s 仅匹配 [ \f\n\r\t\v]
- \d 仅匹配 [0 - 9]
- \p{...} 不是有效的语言元素
- 数字反斜杠转义的解释 :通常,当反斜杠后面跟着单个十进制数字时,它会被解释为反向引用。如果不存在具有该数字名称的捕获,引擎会抛出异常。但在 ECMAScript 模式下,不存在的捕获会被解释为文字数字。当反斜杠后面跟着多个数字时,引擎会尝试将其解释为十进制反向引用。如果不存在具有该索引的捕获,它会假设为八进制字符代码(最大到 \377),并将尾随数字按字面解释。在 ECMAScript 模式下,引擎会尽可能多地尝试找到八进制数字的反向引用并将其转换为十进制,如果不存在则按上述规则处理。
- 自引用捕获组 :在 ECMAScript 模式下,引擎会在每次迭代时更新包含对自身反向引用的捕获,这使得自反向引用能够在捕获的第一次迭代中匹配捕获的一部分。以下是一个示例代码,展示了 ECMAScript 模式和标准正则模式在匹配自引用捕获时的行为差异:

$MyPattern = '(\d\1)+'
'NORM, 1: ' + [regex]::Match('1', $MyPattern).Value
'ECMA, 1: ' + [regex]::Match('1', $MyPattern, 'ECMAScript').Value
'NORM, 11: ' + [regex]::Match('11', $MyPattern).Value
'ECMA, 11: ' + [regex]::Match('11', $MyPattern, 'ECMAScript').Value
'NORM, 111: ' + [regex]::Match('111', $MyPattern).Value
'ECMA, 111: ' + [regex]::Match('111', $MyPattern, 'ECMAScript').Value

运行结果如下:

NORM, 1: 
ECMA, 1: 1
NORM, 11: 
ECMA, 11: 1
NORM, 111: 
ECMA, 111: 111
2. 正则表达式选项
  • RegexOptions.CultureInvariant :该选项会改变 IgnoreCase 选项的行为。正常情况下,引擎会使用当前文化来确定哪些大写和小写字符是等效的。但对于一些比较操作,如文件路径或 URI,不同文化模式之间的差异可能会导致问题。而使用不变文化可以避免这个问题,它使用一个预先确定的字符集,不受当前文化的影响。在 PowerShell 会话中,可以使用 $Host.CurrentCulture 来获取当前文化。
  • 组合正则表达式选项 :可以通过多种方式组合正则表达式选项。不过,有些选项只有与其他选项结合使用时才有效,有些选项在其他选项存在时则无效。主要的组合规则如下:
    • 只有 IgnoreCase 和 Multiline 可以与 ECMAScript 一起使用。
    • CultureInvariant 仅在 IgnoreCase 存在时适用。

组合位标志时,可以使用 -bor 运算符。通过位逻辑,还可以使用 -bnot 创建过滤掩码来移除某个选项。也可以使用数值结果并将其转换为 RegexOptions 类型,或者将用逗号分隔名称(不区分大小写)的字符串转换为 [RegexOptions] 类型。以下是组合位正则表达式选项的示例代码:

[System.Text.RegularExpressions.RegexOptions]::Multiline -bor
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase

# Regex options use 10 bits, with bit 3 (128) unused
# IgnoreCase = 0000000001 = 1
# Multiline = 0000000010 = 2
# Both = 0000000011 = 3
[System.Text.RegularExpressions.RegexOptions]3

[System.Text.RegularExpressions.RegexOptions]3 -band -bnot
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase

[System.Text.RegularExpressions.RegexOptions]'multiline, IGNORECASE'

运行结果为:

IgnoreCase, Multiline
IgnoreCase, Multiline
Multiline
IgnoreCase, Multiline
  • 内联选项 :可以在正则表达式模式中使用内联选项来开启或关闭本章讨论的五个选项。
3. 正则表达式模式的调试

设计好正则表达式模式后,需要确保它能按预期工作。下面介绍一些常见的错误情况和调试方法。
- 无效模式错误 :当看到 “Invalid pattern” 错误时,意味着正则表达式存在问题。常见的导致无效模式的原因包括:
- 未对元字符(如括号 () [] ,锚点 ^ $ ,量词 ? * + 或反斜杠 \ )进行转义。
- 未用 ) 关闭组或用 ] 关闭字符类。
- 引用了不存在的捕获组名称或索引(如 (a).+\2 )。
- 在字符类中反转了范围引用(如 [z - a] )。
- 在不需要转义的地方使用了转义(如 he\x digits )。
- 未在可扩展字符串中对特殊 PowerShell 字符进行转义(如 "\$(\d)" )。

以下是一个常见的正则表达式错误示例:

'$12.00 ($12.99 inc. tax)' -match '(\$(\d+\.\d{2}) inc. tax'

运行时会报错:

OperationStopped: Invalid pattern '(\$(\d+\.\d{2}) inc. tax' at offset 23. Not enough )'s.
OperationStopped: Invalid pattern

这是因为未对括号进行转义,导致模式中缺少右括号。

  • 有效但错误的模式 :并非所有错误都会导致无效模式,许多错误的模式本身是有效的正则表达式,但无法按预期工作。例如,在以下示例中:
'$12.00 ($12.99 inc. tax)' -match '\($(\d+\.\d{2}) inc. tax'
'$12.00 ($12.99 inc. tax)' -match '\(\$(\d+\.\d{2}) inc. tax'
$Matches[1]

第一个匹配尝试中,由于未对美元符号 $ 进行转义,引擎将其解释为字符串结束锚点,导致匹配失败,结果为 False ;而第二个匹配尝试中,对美元符号进行了转义,匹配成功,结果为 True ,并且 $Matches[1] 的值为 12.99

除了上述提到的情况,有效但错误的模式的其他原因还包括:
- 未对正则表达式中有特殊含义的标记进行转义(如 $3.50 中的 $ 作为锚点)。
- 未在可扩展字符串中对特殊 PowerShell 字符进行转义(如 "\$3.50" 中的 $ 可能被解释为 PowerShell 变量)。
- 在字面字符串中尝试转义特殊 PowerShell 字符(如 '\ $(\d)’ 中的反引号)。 - 在字符类中插入范围之间的逗号(如 [a - z,0 - 9]`)。
- 在区分大小写时未使用正确的大小写。
- 无意中在模式中插入了空格。
- 未考虑换行符及其与多行模式的交互。
- 零长度匹配(交替中的零个或多个量词)。
- 混淆了捕获组的数字顺序。

4. 从 NFA 引擎的角度看正则表达式

PowerShell 和 .NET 中的正则表达式引擎属于非确定性有限自动机(NFA)类型。与文本控制的确定性正则表达式引擎(DFA)不同,NFA 引擎的操作由正则表达式模式驱动。虽然背后的数学和计算理论超出了本文范围,但从实际应用角度理解其影响有助于更好地使用 .NET 正则表达式。可以在 Regex 101 网站上观察正则表达式引擎逐步处理模式和字符串的过程,该网站使用的引擎虽然与 .NET 不同,但在这个示例中具有足够的相似性,在每一步中,调试器会突出显示正则表达式模式中的当前标记以及当前的匹配情况。

下面通过一个简单的 mermaid 流程图来展示 NFA 引擎匹配过程的大致逻辑:

graph TD;
    A[开始匹配] --> B{是否匹配成功};
    B -- 是 --> C[匹配结束];
    B -- 否 --> D[回溯到决策点];
    D --> E{是否还有其他决策};
    E -- 是 --> F[尝试下一个决策];
    F --> B;
    E -- 否 --> G[匹配失败];

综上所述,在使用正则表达式时,需要注意各种匹配模式、选项的使用以及可能出现的错误情况,从引擎的角度思考问题有助于构建更有效的正则表达式模式。

《正则表达式深入解析与应用》下半部分

5. 回溯与分支

回溯是正则表达式匹配过程中的一个重要概念。以 Regex 101 章节中的示例 2 为例,该模式包含一周中每天的交替匹配,引擎需要决定使用哪个替代方案进行匹配尝试。

$MyString = 'It rained on Friday, but Monday will be clear.'
$MyPattern = 'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday'
$MyString -match $MyPattern

正则表达式引擎会逐个处理 $MyPattern 中的标记,并尝试将每个标记与输入文本中的当前目标进行匹配,具体步骤如下:
1. 引擎在第一个字符 ‘I’ 上未找到任何替代方案的匹配,因此回溯到模式的开头并继续前进。
2. 当引擎到达第二个字符 ‘t’ 时,由于 -match 运算符不区分大小写,它匹配了 “Tuesday” 替代方案中的 ‘T’。
3. 然而,下一个(第三个)字符是空格(0x20),与 “Tuesday” 中的 ‘u’ 不匹配,所以引擎继续尝试其他替代方案。
4. 当引擎尝试 “Thursday” 替代方案时,也出现了类似的情况。
5. 引擎在接下来的字符(第 3 到 13 个)‘ rained on ’ 上未找到任何替代方案的匹配,因此对每个字符都回溯到模式的开头。
6. 当引擎到达 ‘Friday’ 中的 ‘F’ 时,它匹配了 “Friday” 替代方案中的 ‘F’,然后尝试将下一个元素 ‘r’ 与下一个字符 ‘r’ 进行匹配。
7. 这次匹配成功,引擎继续将模式中的元素与字符串中的字符进行匹配,直到模式中的 “Friday” 与 $MyString 中的 “Friday” 完全匹配。
8. 由于 -match 运算符在找到第一个匹配项后停止,所以引擎停止并返回 $true

在这个场景中,回溯发生的原因是模式中包含交替结构(即 “要么/要么” 模式)。这些决策是 NFA 引擎中回溯的基础。与 DFA 引擎不同,NFA 引擎会按顺序处理每个标记,并记住这些决策点(回溯位置)。如果模式在后续某个点无法匹配,引擎会回溯到该点并尝试下一个决策。这实际上创建了一个新的可能性分支,引擎会沿着这个分支继续处理,直到找到匹配项或到达模式的末尾。只有当引擎尝试了所有决策后仍然无法找到匹配项时,才会放弃。这种方法虽然确保了引擎能够找到任何可行的匹配项,但也可能导致大量的回溯,从而增加匹配所需的时间。

下面用表格总结上述回溯过程:
|步骤|当前字符|匹配情况|操作|
| ---- | ---- | ---- | ---- |
|1|I|无匹配|回溯到模式开头,继续前进|
|2|t|匹配 “Tuesday” 中的 T|继续匹配后续字符|
|3| |不匹配 “Tuesday” 中的 u|尝试其他替代方案|
|4| |不匹配 “Thursday” 中的字符|继续尝试其他替代方案|
|5| |无匹配|对每个字符回溯到模式开头|
|6|F|匹配 “Friday” 中的 F|尝试匹配后续字符|
|7|r|匹配 “Friday” 中的 r|继续匹配后续字符|
|8| |完全匹配 “Friday”|停止匹配,返回 $true |

6. 灾难性回溯

虽然回溯机制有助于找到匹配项,但过多的回溯点可能会导致指数级增长的排列组合,从而引发灾难性回溯。以一个简单的 GUID 匹配模式为例:

$Opts = [System.Text.RegularExpressions.RegexOptions]::None
$Time = [timespan]::FromSeconds(5)
$Guid = '87db9d39-ddc8-413c-84ac-0be925a8230a'
$Pattern = '([0-9a-f]+-?)+\Z'
Write-Host "'$Guid'"
Write-Host ([regex]::IsMatch($Guid, $Pattern, $Opts, $Time))
$GuidSpace = $Guid + ' '
Write-Host "'$GuidSpace'"
Write-Host ([regex]::IsMatch($GuidSpace, $Pattern, $Opts, $Time))

当输入字符串是有效的 GUID 时,引擎能在不到一毫秒的时间内找到完整匹配。但如果输入字符串末尾有一个多余的空格,就会触发灾难性回溯,导致 5 秒的超时机制生效。

问题的关键在于匹配失败时,嵌套的一个或多个 + 量词会导致灾难性回溯。当引擎匹配到除空格之外的所有字符时,接下来要匹配的模式标记是字符串结束 \Z 锚点,但输入字符串的下一个字符是空格,匹配失败,引擎开始回溯。内部的 + 量词最初贪婪地匹配了整个十六进制块,现在会放弃一个字符,例如从 (87db9d39-)(ddc8-)(413c-)(84ac-)(0be925a8230a) 变为 (87db9d39-)(ddc8-)(413c-)(84ac-)(0be925a8230)a 。但由于整个组可以匹配一个或多个十六进制数字,它会继续匹配,变为 (87db9d39-)(ddc8-)(413c-)(84ac-)(0be925a8230)(a) 。每次内部 + 量词放弃一个字符,都会产生新的排列组合,并且数量呈指数级增长,最终导致引擎耗尽时间。

可以在 Regex 101 网站上观察该模式在有空格和无空格情况下的匹配过程:
- 无空格:https://regex101.com/debugger?flags=i&flavor=pcre2&regex=(%5B0-9a-f%5D%2B-%3F)%2B%5CZ&testString=87db9d39-ddc8-413c-84ac-0be925a8230a
- 有空格:https://regex101.com/debugger?flags=i&flavor=pcre2&regex=(%5B0-9a-f%5D%2B-%3F)%2B%5CZ&testString=87db9d39-ddc8-413c-84ac-0be925a8230a%20

7. 原子组

为了避免灾难性回溯,可以使用原子组 (?>...) ,也称为非回溯组。当引擎将原子组与输入文本进行匹配时,不会放弃其匹配的任何部分。以下是使用原子组避免灾难性回溯的示例:

$Opts = [System.Text.RegularExpressions.RegexOptions]::None
$Time = [timespan]::FromSeconds(5)
$Guid = '87db9d39-ddc8-413c-84ac-0be925a8230a'
$Pattern = '(?>[0-9a-f]+-?)+\Z'
Write-Host "'$Guid'"
Write-Host ([regex]::IsMatch($Guid, $Pattern, $Opts, $Time))
$GuidSpace = $Guid + ' '
Write-Host "'$GuidSpace'"
Write-Host ([regex]::IsMatch($GuidSpace, $Pattern, $Opts, $Time))

当引擎无法匹配第二个字符串中的空格时,外部 + 量词的任何匹配组件都不会放弃字符,引擎只会继续遍历输入字符串,并尝试在后续位置开始匹配。由于可供尝试的排列组合很快耗尽,处理过程会停止。可以在 Regex 101 网站上观察该模式在有尾随空格时的匹配过程:https://regex101.com/debugger?flags=i&flavor=pcre2&regex=(%3F%3E%5B0-9a-f%5D%2B-%3F)%2B%5CZ&testString=87db9d39-ddc8-413c-84ac-0be925a8230a%20

8. 功能限制与替代方案
  • 无子程序支持 :子程序是指在正则表达式中能够在输入字符串的不同位置重用子表达式的功能,它与反向引用不同,反向引用仅重用捕获子表达式的捕获结果。子程序可以通过重用重复元素来显著缩短模式,但 .NET 正则表达式不具备此功能,从 Accessing Regexes 章节的示例 33 中的重复部分可以明显看出这一点。
  • 无递归支持 :递归匹配在一些正则表达式实现中可用,它允许引擎在输入字符串的当前位置重新应用整个模式或捕获组。如果到达递归标记,引擎会进入更深的递归级别。这种功能为正则表达式匹配带来了更多可能性,但同时也牺牲了简单性和效率。.NET 正则表达式不支持递归,但提供了平衡组作为替代方案,可以在本章的高级子表达式和反向引用部分了解更多关于平衡组的信息。
  • 占有式量词与原子组 :在许多正则表达式实现中,可以使用占有式贪婪量词来防止回溯,通常在量词后面加一个额外的加号 + 来指定。.NET 不支持占有式量词,但由于它们在功能上等同于将标记包装在原子组中,因此这并不会带来实际的劣势。在其他正则表达式引擎中使用占有式量词(如 \w++ )的地方,只需将其替换为原子组(如 (?>\w+) )即可。此外,原子组还可以与懒惰量词(如 (?>\w+?) )一起使用。
  • 可变长度后瞻 :.NET 正则表达式的一个不常见特性是支持在所有环视中使用完整的正则表达式语法。后瞻的大小可变性通常限于交替、有限量词或两者。虽然这有助于提高模式的特异性,但也可能导致效率问题。如果后瞻可以无限向后扩展,引擎将不得不检查到输入字符串的开头,从而增加处理时间。可以在本章的高级子表达式和反向引用部分了解更多关于环视的信息。
9. 模式解构与解释

在使用他人创建的正则表达式模式或忘记自己创建的模式时,需要对模式进行解构和解释,以理解其工作原理并解决匹配问题。
首先,将模式转换为更易读的扩展形式,具体步骤如下:
- 拆分子表达式/组定义和自定义类为块,并缩进块内的内容。
- 将替代方案和管道符号放在单独的行上,缩进替代方案,但不缩进管道符号,保持管道符号与组的开闭括号或行首的缩进一致。
- 尽可能将量词与其关联的标记放在一起。
- 可以将每个标记 - 量词对放在单独的行上,或者将相关标记分组在一起,将长序列的标记拆分为具有相同缩进的单独行。

以下是 Accessing Regexes 章节示例 33 中的模式重写为扩展形式的示例:

^
# Beginning of string
(?:
    # Noncapturing group
    (?<Octets>
        # Capturing group named "Octets"
        # First alternative, matches 250 - 255
        25[0 - 5]
        # Match "25" then 0 - 5
        |
        #
        # Second alternative, matches 200 - 249
        2[0 - 4][0 - 9]
        # Match "2" then 0 - 4 then 0 - 9
        |
        #
        # Third alternative, matches 0 - 199
        [01]?
        # Match "1" or "0" optionally
        [0 - 9]{1,2}
        # Match 0 - 9, 1 or 2 times
    )
    #
    \.
    # Match period/full - stop
){3}
# Match this group 3 times
#
(?<Octets>
    #
    Exactly the same "Octets" group for 4th octet,
    25[0 - 5]
    # since .NET regex doesn't support
    |
    #
    subroutines/recursion
    2[0 - 4][0 - 9]
    #
    |
    #
    [01]?[0 - 9]{1,2}
    #
)
#
$
# End of string

将模式解构后,需要对其进行解释。可以像示例中那样,为每个标记或标记组添加注释,这样就得到了一组易于理解的指令。要从引擎的角度思考每个标记从开始到结束的解释方式,以及不同的输入如何影响匹配结果。例如,在 “Octets” 组中,第三个替代方案可以匹配以 2 开头的块的前两个数字,如果该替代方案排在第一位,可能会导致 2xx 数字匹配不完整,从而使最后一位数字无法匹配。此外,在前三个块中,必须跟随一个强制的句点字符,否则匹配将失败。

综上所述,正则表达式的使用涉及到多个方面的知识,包括匹配模式、选项、回溯机制、功能限制以及模式的解构与解释等。深入理解这些内容,并从引擎的角度思考问题,能够帮助我们构建更有效、更可靠的正则表达式模式,从而更好地应对各种文本处理需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值