正则表达式深度剖析
1. 避免 IP 地址匹配中的幻影匹配
在处理 IP 地址匹配时,正则表达式引擎可能会出现意外的匹配情况。例如,在提取 IP 地址时,如果没有适当的锚点或前瞻断言,可能会导致匹配不完整。为了避免这种情况,可以使用替代方案和前瞻断言。
以下是一个示例代码,展示了如何避免 IP 地址匹配中的幻影匹配:
# The alternatives are reversed in this example,
# so the tokens matching 0-199 come first
$Template = '(?:(?<Octets>[01]?[0-9]{{1,2}}|2[0-4][0-9]|25[0-5])\.){{3}}' +
'(?<Octets>[01]?[0-9]{{1,2}}{0}|2[0-4][0-9]|25[0-5])'
# Create 3 patterns using a format string,
# with "{" and "}" doubled up to escape them
$Nothing = $Template -f ''
$Lookahead = $Template -f '(?![0-9])'
$WordBoundary = $Template -f '\b'
$IpsToMatch = @(
'Address: 198.51.100.42',
'Address: 198.51.100.193',
'Address: 198.51.100.254'
)
foreach ($IpAddress in $IpsToMatch) {
[pscustomobject]@{
Input = $IpAddress
Nothing = [regex]::Match($IpAddress, $Nothing).Value
Lookahead = [regex]::Match($IpAddress, $Lookahead).Value
WordBoundary = [regex]::Match($IpAddress, $WordBoundary).Value
}
}
运行上述代码后,输出结果如下:
| Input | Nothing | Lookahead | WordBoundary |
| — | — | — | — |
| Address: 198.51.100.42 | 198.51.100.42 | 198.51.100.42 | 198.51.100.42 |
| Address: 198.51.100.193 | 198.51.100.193 | 198.51.100.193 | 198.51.100.193 |
| Address: 198.51.100.254 | 198.51.100.25 | 198.51.100.254 | 198.51.100.254 |
从输出结果可以看出,对于最后一个 IP 地址,如果没有使用锚点,最后一个八位字节只匹配到了 ‘25’。而使用单词边界和负前瞻断言可以避免这种情况。
2. 高级语法
2.1 Unicode 类别和块
.NET 正则表达式原生支持 Unicode。可以使用
\uHHHH
转义序列直接匹配 Unicode 字符,其中
HHHH
是十六进制的 UTF - 16 代码点(大端字节序)。例如,
\u00A9
匹配版权符号
©
(U + 00A9),
\u2021
匹配双剑号符号
‡
(U + 2021)。
还可以使用
\p{...}
转义序列匹配 Unicode 块和类别:
- 使用
\p{Is...}
匹配块,其中
...
是块名称。例如,
\p{IsLatin - 1Supplement}
匹配 Latin - 1 补充块(U + 0080 - U + 00FF)中的所有字符,包括版权符号。
- 使用
\p{X}
或
\p{Xy}
匹配类别,其中
X
和
Xy
是类别和子类别缩写。例如,
\p{Po}
匹配其他标点符号子类别中的字符,包括双剑号符号。
以下是一个匹配 Unicode 字符、块和类别的示例代码:
$BEUnicode = [System.Text.Encoding]::BigEndianUnicode
$CopyrightSymbol = $BEUnicode.GetString(@(0x00, 0xA9))
$DDaggerSymbol = $BEUnicode.GetString(@(0x20, 0x21))
$CopyrightSymbol -match '\u00A9'
$CopyrightSymbol -match '\p{IsLatin-1Supplement}'
$DDaggerSymbol -match '\u2021'
$DDaggerSymbol -match '\p{Po}'
$DDaggerSymbol -match '\p{P}'
运行上述代码后,输出结果均为
True
。
与单词
\w
、十进制
\d
和空白
\s
类缩写一样,Unicode 类缩写有一个反向的
\P{...}
,它匹配除该类别或块之外的所有内容。
2.2 UTF - 16
在处理 Unicode 字符时,需要注意 PowerShell 和 .NET 使用 UTF - 16 编码。正则表达式引擎在匹配时也遵循此编码。引擎将基本多语言平面(BMP,U + 0000 - U + FFFF)之外的字符存储为 UTF - 16 代理对。
每个代理对由以下两部分组成:
- 高代理 U + D800 - U + DBFF(高 10 位,1024 个字符)
- 低代理 U + DC00 - U + DFFF(低 10 位,1024 个字符)
以下是一个处理 UTF - 16 代理对的示例代码:
$HedgehogBytes = @(0xF0, 0x9F, 0xA6, 0x94)
$HedgehogEmoji = [System.Text.Encoding]::UTF8.GetString($HedgehogBytes)
# Show emoji
$HedgehogEmoji
# U+1F994 is in category Other Symbols (So), but match fails
$HedgehogEmoji -match '\p{So}'
# Matching high surrogate followed by low surrogate succeeds
$HedgehogEmoji -match '\p{IsHighSurrogates}\p{IsLowSurrogates}'
# Show surrogate codepoints
$HedgehogEmoji.ToCharArray().ForEach{
'0x' + [convert]::ToString([int]$_, 16).ToUpper()
} -join ', '
# Interpret actual text elements from UTF-16 string
[System.Globalization.StringInfo]::new($HedgehogEmoji) | Format-Table
# Get Unicode scalar (non-surrogate code point)
$HedgehogEmoji.EnumerateRunes() | Format-Table
在这个示例中,直接使用
\p{So}
匹配刺猬表情符号会失败,而使用
\p{IsHighSurrogates}\p{IsLowSurrogates}
可以成功匹配。
2.3 字符类减法
字符类减法是少数正则表达式实现中可用的功能。通过它,可以过滤自定义字符类,限制它们匹配的内容。
以下是一个字符类减法的示例代码:
$Array = 'a' .. 'p'
-join $Array # Input string
-join ($Array -match '[a-k]') # Match a-k
-join ($Array -match '[a-k-[d]]') # Match a-k except d
运行上述代码后,输出结果如下:
abcdefghijklmnop
abcdefghijk
abcefghijk
从输出结果可以看出,
[a-k-[d]]
匹配了
a - k
范围内除
d
之外的字符。
当进行字符类减法时,需要注意以下几点:
- 正向减法
- [...]
从初始类中移除字符或范围。
- 反向减法
- [^...]
从初始类中移除除减法类中的字符或范围之外的所有内容。
- 可以进行嵌套减法。
- 不能通过在减法中包含初始类中不存在的字符来添加它们(双重否定不起作用)。
- 引擎会忽略初始类中不存在的字符或范围(超出范围的减法不会报错)。
以下是一个展示字符类减法限制的示例代码:
$Array = 'a' .. 'p'
-join ($Array -match '[a-k-[h-z]]') # Matches a-k except h-k, l-z ignored
-join ($Array -match '[a-k-[^j-z]]') # Matches nothing but j-k, l-z ignored
-join ($Array -match '[a-k-[b-g-[cd]]]') # Matches a-k, except b and e-g
运行上述代码后,输出结果如下:
abcdefg
jk
acdhijk
2.4 使用内联选项
.NET 提供了一系列正则表达式选项修饰符。可以在模式中更改其中五个选项,以改变从该点开始的行为,或者仅在一个范围内生效。一个有用的助记符是
msnix
(“MS - nix”)。
相关选项如下表所示:
| [RegexOptions] 标志 | 修饰符 | 名称 |
| — | — | — |
| Multiline | m | 多行模式 |
| Singleline | s | 单行模式 |
| ExplicitCapture | n | 仅显式捕获 |
| IgnoreCase | i | 不区分大小写模式 |
| IgnorePatternWhitespace | x | 忽略模式中的空白 |
有两种方法可以将这些选项应用于模式:
- 第一种方法是在选项修饰符之后将它们应用于模式的其余部分。使用表中的字母打开选项,在字母前加连字符(减号)关闭选项。一般形式为
(?msnix - msnix)
。
以下是一个内联选项修饰符的示例代码:
$MyRegex = [regex]::new(@'
(?xm-i)
# Ignore white space and multiline ON, case insensitivity OFF
^
# Start of line because multiline is on
[a-z]{2}
# 2 lowercase letters
(?i)
# Turn on case insensitivity
-[a-z]{2}
# Hyphen followed by 2 letters of any case
'@, 'IgnoreCase')
$MyRegex.Matches(@'
en-US
en-us
EN-us
en-GB
'@).ForEach{ $_.Value }
运行上述代码后,输出结果如下:
en-US
en-us
en-GB
在这个示例中,正则表达式选项标志打开了
IgnoreCase
,但
- i
修饰符覆盖了它,在模式内禁用了大小写不敏感。
m
修饰符打开了多行模式,允许模式从每行匹配一个文化代码,而不仅仅是字符串开头之后的那一行。
修饰符在模式中的当前位置生效,因此它不会改变之前任何标记的行为。
以下是另一个展示内联选项修饰符特异性的示例代码:
$MyRegex = [regex]::new('(\w)(?n)(\w)(?-n)(\w)')
$MyRegex.Match('abc').Groups.ForEach{
Write-Host $_.Name $_.Value
}
运行上述代码后,输出结果如下:
0 abc
1 a
2 c
在这个示例中,引擎没有捕获第二个组,但捕获了第一个和第三个组。显式捕获仅在
(?n)
和
(?-n)
之间生效。
2.5 使用选项范围
还可以使用带有修饰符和冒号的子表达式语法,在受限范围内应用选项修饰符。一般语法为
(?msnix - msnix:... )
。使用这种方法,更改后的选项仅适用于子表达式。
以下是一个选项范围(子表达式)的示例代码:
$MyRegex = [regex]::new('(\w)(?-n:(\w))(\w+)', 'ExplicitCapture')
$MyRegex.Match('abc').Groups.ForEach{
Write-Host $_.Name $_.Value
}
运行上述代码后,输出结果如下:
0 abc
1 b
在这个示例中,虽然存在
ExplicitCapture
选项,但引擎捕获了第二个未命名组,因为该组位于关闭了
ExplicitCapture
的子表达式内。
2.6 正则表达式中的注释
和任何编程语言一样,许多正则表达式实现支持注释,也称为备注。.NET 正则表达式也不例外,支持两种形式的注释:
- 注释范围
(?#...)
无论存在哪些正则表达式选项都可用。
- 行尾注释
#...
仅在
IgnorePatternWhitespace
存在时适用。
以下是一个在 .NET 正则表达式中使用注释的示例代码:
if ('abcdefg hijklmn' -match '(?:(\w)(?# Matches letter pairs)(\w))+') {
$Matches
}
运行上述代码后,输出结果如下:
Name Value
---- -----
2 f
1 e
0 abcdef
需要注意的是,使用
- match
时,只能获得第一个匹配项,并且只能访问每个组的最后一个捕获。
当
IgnorePatternWhitespace
标志存在时,可以使用行尾注释。引擎会忽略哈希符号
#
之后直到行尾的所有文本。
以下是一个使用行尾注释的示例代码:
$MyString = 'SWdlbCBzaW5kIHRvbGwh'
# Matches valid base-64 strings
$MyPattern = @'
(?nx)
# Ignore pattern white space, explicit captures
(
# Unnamed group
[A-Za-z0-9+/]{4}
# Match 4 B64 chars (A-Z, a-z, 0-9, +, and /)
)+
# Match one or more instances of group
(
# Unnamed group
# 1st alternative
[A-Za-z0-9+/]{3}=
# Match 3 B64 chars and "="
|
#
# 2nd alternative
[A-Za-z0-9+/]{2}==
# Match 2 B64 chars and "=="
)?
# Match group optionally
'@
if ($MyString -match $MyPattern) {
Write-Host ('Match: {0}' -f
-join ([System.Convert]::FromBase64String($Matches[0]) -as [char[]]))
}
运行上述代码后,输出结果如下:
Match: Igel sind toll!
在这个示例中,
(?x)
内联选项打开了
IgnorePatternWhitespace
。
3. 高级替换模式
之前没有深入讨论替换模式,因为引擎对它们的处理方式不同,并且它们使用不同的语言元素。替换模式是正则表达式引擎在替换操作中如何替换文本的指令,也被称为替换模式。
替换模式中唯一的元字符是美元符号
$
,引擎将其他所有文本视为字面量。美元符号后面的字符会改变替换的性质。
3.1 命名和数字捕获
正则表达式模式中从输入字符串捕获的任何组都可以在替换操作中进行替换。通过数字索引替换捕获的语法是
$NNN
,其中
NNN
是模式中捕获组从左到右的有序索引。命名捕获的语法与反向引用略有不同,使用
${name}
,其中
name
是命名捕获
<name>
的组名。
以下是一个使用替换模式重新格式化日志数据的示例代码:
$MyString = @'
[2020-07-16T19:50:31] [PATCH] Service "xrdp" installed by "apt"
[2020-07-16T20:25:23] [INFO ] Service [2896] started
[2020-07-16T20:25:26] [DEBUG] Service [2896] ready
'@
$Patterns = @(
'(?m)^\[([^\]T]+)T([^\]]+)\] ' +
'\[(INFO|PATCH|DEBUG|WARN|ERROR|FATAL) ?\] (?<msg>.+)$',
'(?m)^(WARN|PATCH)(?= )',
'(?m)^INFO(?= )',
'(?m)^DEBUG(?= )'
)
$Replacements = @(
'$3 message at $2 on $1{0}${{msg}}{0}' -f [Environment]::NewLine,
'$1ING',
'INFORMATIONAL',
'DEBUGGING'
)
for ($i = 0; $i -lt $Patterns.Count; $i++) {
$MyString = $MyString -replace $Patterns[$i], $Replacements[$i]
}
$MyString
运行上述代码后,输出结果如下:
PATCHING message at 19:50:31 on 2020-07-16
Service "xrdp" installed by "apt"
INFORMATIONAL message at 20:25:23 on 2020-07-16
Service [2896] started
DEBUGGING message at 20:25:26 on 2020-07-16
Service [2896] ready
与正则表达式模式中的反向引用不同,不存在的捕获不会被解释为八进制字符代码,而是引擎会替换为该标记的字面文本,命名捕获也是如此。
以下是一个替换模式中不存在捕获的示例代码:
$MyString = '$5.23 (March)'
$NoCaptures = '\$\d+\.\d{2} \(\w+\)'
$Captures = '\$(\d+)\.(?<discount>\d{2}) \((\w+)\)'
$Replacement = '$$$1.00 ($$0.${discount} off until ${2})'
$MyString -replace $NoCaptures, $Replacement
$MyString -replace $Captures, $Replacement
运行上述代码后,输出结果如下:
$$1.00 ($0.${discount} off until ${2})
$5.00 ($0.23 off until March)
在这个示例中,
$$
标记插入一个字面美元符号,受保护的数字捕获引用
${NNN}
防止引擎将后续数字解释为组索引的一部分。例如,替换模式
$10
表示第十个捕获,而
${1}0
表示第一个捕获,后面跟着一个字面零。
3.2 整个匹配
可以使用美元符号后跟一个与号
$&
插入模式的整个匹配值,相当于捕获 0。
以下是一个替换整个匹配的示例代码:
'Nice!' -replace 'ice', 'oice $&'
运行上述代码后,输出结果如下:
Noice ice!
3.3 匹配跨度前缀和后缀
模式匹配前后的文本也会被引擎存储。要插入匹配的第一个字符之前的所有文本(前缀),使用美元符号后跟一个反引号
$``;要插入匹配的最后一个字符之后的所有文本(后缀),使用美元符号后跟一个单引号
$’`。
以下是一个替换匹配前后文本的示例代码:
'Nice!' -replace 'ice', '$`$`$`oice $&$''$''$''' # Literal
'Nice!' -replace 'ice', "$``$``$``oice $&$'$'$'" # Expandable
运行上述代码后,输出结果如下:
NNNNoice ice!!!!
NNNNoice ice!!!!
在可扩展字符串中需要转义反引号,在字面字符串中需要转义单引号,可以通过加倍相关字符来实现。
3.4 整个输入
可以使用
$_
替换替换操作前的整个输入字符串,这与 PowerShell 自动变量
$_
不同,必须传递美元符号后跟下划线。可以使用字面字符串
'$_'
,或者在可扩展字符串中转义美元符号
"
$_”
。这实际上相当于前缀、匹配和后缀标记的组合
$
$&$'
。
3.5 最后捕获
替换模式中可用的最后一个标记是替换最后捕获。这是匹配中编号最高的捕获。
以下是一个替换最后捕获的示例代码:
$MyPattern = '(?m)^(?<First>[\w-[\d]]+)((?: [\w.-[\d]]+)+)? ([\w--[\d]]+)$'
$MyReplacement = '"$2, $+$1"'
$Names = @'
John Smith
Jane Luisa Doe
Joe D. Bloggs
Joe Smith-Bloggs
'@
$Names -replace $MyPattern, $MyReplacement
运行上述代码后,输出结果如下:
"Smith, John"
"Doe, Jane Luisa"
"Bloggs, Joe D."
"Smith-Bloggs, Joe"
在这个示例中,注意姓氏是捕获 2,名字是捕获 3。这是因为 .NET 正则表达式引擎在所有未命名捕获之后为命名捕获分配索引。名字组是最后一个捕获,可以使用
${First}
、
$3
或
$+
访问。
4. 高级子表达式和反向引用
4.1 深入了解反向引用
之前已经提到过反向引用,在 .NET 正则表达式中还有一些额外的内容。可以使用反向引用通过编号和名称匹配正则表达式模式中较早的捕获。这与子例程不同,子例程是重新应用模式而不是捕获,.NET 正则表达式中不存在子例程。
对于数字反向引用,使用
\NNN
,其中
NNN
是从 1 开始的整数,表示模式中捕获从左到右的有序索引。对于命名反向引用,使用
\k<name>
或
\k'name'
,其中
name
是命名组
(?<name>...)
的名称。
引擎会为所有捕获分配一个数字索引,无论组是否命名,可以使用
\NNN
或
\k<...>
访问这些索引。当使用数字索引访问不存在的反向引用时,引擎会将其视为八进制字符代码。
以下是一个命名和数字反向引用的示例代码:
$Numbers = '12230'
# Matches repeating digits
$Numbers -match '(\d)\1'
# Tries to match capture 130, which doesn't exist,
# so matches octal 130 (capital "X")
$Numbers -match '(\d)\130'
# Use an index enclosed with triangular bracket or
# single quotation mark to prevent the above issue
$Numbers -match '(\d)\<1>30'
$Numbers -match "(\d)\'1'30"
# Passing numeric indexes to \k results in the indexed capture,
# unless a capturing group was explicitly named with that number
$Numbers -match '(\d)\k<1>30'
$Numbers -match "(\d)\k'1'30"
# You can use either enclosed form for both
# the capturing group and the backreference
$Numbers -match "(?'repeat'\d)\k<repeat>"
$Numbers -match "(?<repeat>\d)\k'repeat'"
运行上述代码后,输出结果如下:
True
False
True
True
True
True
True
True
如果为捕获组使用数字名称
(?<NNN>)
,会覆盖内部分配的索引,这样捕获名称将不再反映模式中捕获组的顺序,不建议这样做。
以下是一个命名数字捕获及其后果的示例代码:
'5:5:' -match "(?'2'\d)\1" # Capture 1 doesn't exist, throws error
'5:5:' -match "(?'2'\d)(:)\1\2" # Expected order of captures
'5:5:' -match "(?'2'\d)(:)\2\1" # Actual order of captures
运行上述代码后,会抛出错误:
OperationStopped: Invalid pattern '(?'2'\d)\1' at offset 10.
Reference to undefined group number 1.
False
True
捕获组名称始终区分大小写,无论
IgnoreCase
标志是否存在。
4.2 深入了解环视
环视可以在不成为匹配一部分的情况下确定匹配的成功或失败,即零宽度断言。环视也是原子的,一旦处理继续,引擎就不能回溯到其中。
以下是一个使用负环视隔离行注释的示例代码:
$MyPattern = '(?nm)^(?!\s*#).+(\r?\n)*'
$MyString = @'
# Create the RNG
$CryptoRandGen = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
# Buffer to store random bytes
[byte[]]$bufferByte = [byte[]]::new(1)
'@
$MyString -replace $MyPattern
运行上述代码后,输出结果如下:
# Create the RNG
# Buffer to store random bytes
这里负前瞻的作用是排除所有以零个或多个空格字符后跟哈希符号
#
开头的行,其他行匹配
.+
标记以及任何后续换行符,
-replace
运算符替换这些匹配项,只留下注释行和相邻换行符。
也可以在环视中使用锚点,因为它们本身就是零宽度的。例如,在多行模式中可以否定行开头锚点
(?!^)
。
以下是一个使用环视查找名词和冠词的示例代码:
$MyPattern = '(?im)(?<=(?<article>a|the) )(?>(?<noun>\w+))(?<!ing)(?!$)'
$MyString = @'
The cat sat on the mat eating a hat
The knitting needles fell on the floor again
'@
$MyMatches = [regex]::Matches($MyString, $MyPattern)
$MyMatches.ForEach{
'Match "{0}"' -f $_.Value
$_.Groups.Where{
$_.Name -ne '0' # Exclude the $0 'whole match' group
}.ForEach{
' ', $_.Name, '=', $_.Value -join ' '
}
}
运行上述代码后,输出结果如下:
Match "cat"
article = The
noun = cat
Match "mat"
article = the
noun = mat
Match "floor"
article = the
noun = floor
这个模式可以拆解如下:
-
(?im)
:选项修饰符,忽略大小写和多行模式。
-
(?<=(?<article>a|the) )
:包含命名捕获组
article
的正向后瞻,匹配
a
或
the
以及一个空格,意味着后续名词之前必须是
a
或
the
。
-
(?>(?<noun>\w+))
:包含命名捕获组
noun
的原子组,匹配一个或多个单词字符。
-
(?<!ing)
:负向后瞻,如果在当前点之前匹配到
ing
则导致匹配失败。
-
(?!$)
:负向前瞻,如果匹配到行尾(因为是多行模式)则导致匹配失败。
在字符串中的五个冠词 - 名词对中,有两个不匹配:
-
'a hat'
不匹配,因为
hat
中的
t
是行尾,负向前瞻
(?!$)
匹配到这一点导致失败。
-
'The knitting'
不匹配,因为
knitting
以
ing
结尾,负向后瞻
(?<!ing)
匹配到这一点导致失败。
综上所述,正则表达式在处理各种文本匹配和替换场景中有丰富的功能和高级特性,合理运用这些特性可以更高效地完成文本处理任务。例如在处理 IP 地址匹配时避免幻影匹配,在处理 Unicode 字符时考虑编码和类别匹配,以及在替换操作中灵活运用各种捕获和替换标记等。通过不断学习和实践这些高级用法,能够提升正则表达式的使用能力,更好地应对复杂的文本处理需求。
以下是一个总结正则表达式高级特性使用流程的 mermaid 流程图:
graph LR
A[开始] --> B[确定需求类型]
B --> |IP地址匹配| C[使用替代方案和前瞻断言避免幻影匹配]
B --> |Unicode处理| D[使用\u、\p匹配字符、块和类别,考虑UTF - 16编码]
B --> |字符类过滤| E[使用字符类减法过滤自定义字符类]
B --> |选项设置| F[使用内联选项或选项范围改变匹配行为]
B --> |注释添加| G[使用注释范围或行尾注释添加注释]
B --> |替换操作| H[使用各种替换标记进行文本替换]
B --> |反向引用和环视| I[使用反向引用匹配之前捕获,使用环视确定匹配条件]
C --> J[结束]
D --> J
E --> J
F --> J
G --> J
H --> J
I --> J
这个流程图展示了根据不同的正则表达式使用需求,选择相应的高级特性来完成任务的流程。通过这样的总结,可以更清晰地理解和运用正则表达式的高级功能。
超级会员免费看
71

被折叠的 条评论
为什么被折叠?



