.NET 正则表达式的使用与特性
1. 正则替换与特殊序列
在 .NET 中,正则表达式的替换功能十分强大。例如,它可以将
some random spacing
这样的字符串进行处理。在搜索和替换时,会使用
LeadingWS
匹配的长度作为偏移量(即跳过的字符数)。这里利用了
Match
对象的一个便利特性,即使匹配失败,其
Length
属性仍可使用,失败时
Length
属性值为 0,这恰好满足对整个目标应用
AnyWS
的需求。
1.1 特殊替换序列
Regex.Replace
方法和
Match.Result
方法都接受一个特殊解释的“替换”字符串。其中的特殊序列会被匹配中的相应文本替换,具体如下表所示:
| 序列 | 替换内容 |
| ---- | ---- |
|
$&
| 正则表达式匹配的文本(也可表示为
$0
) |
|
$1
,
$2
, … | 相应捕获括号组匹配的文本 |
|
${name}
| 相应命名捕获匹配的文本 |
|
$‘
| 匹配位置之前的目标字符串文本 |
|
$’
| 匹配位置之后的目标字符串文本 |
|
$$
| 单个
$
字符 |
|
$R
| 整个原始目标字符串的副本 |
|
$+
| (见下文说明) |
$+
序列目前的实现用处不大。它源于 Perl 中有用的
$+
变量,该变量引用实际参与匹配的编号最高的捕获括号组。但在 .NET 替换字符串中,
$+
仅引用正则表达式中编号最高的捕获括号组,特别是在使用命名捕获时会自动重新编号,这使得它更显得无用。
除了表中描述的情况,替换字符串中其他对
$
的使用不会被修改。
1.2 使用替换委托
替换参数不限于简单字符串,还可以是委托(本质上是函数指针)。委托函数在每次匹配后被调用,以生成用于替换的文本。由于函数可以进行任意处理,因此这是一种非常强大的替换机制。
委托类型为
MatchEvaluator
,每次匹配调用一次。它引用的函数应接受匹配的
Match
对象,进行所需的处理,并返回用于替换的文本。
以下是两个产生相同结果的代码片段示例:
Target = R.Replace(Target, "<<$&>>")
Function MatchFunc(ByVal M as Match) as String
return M.Result("<<$&>>")
End Function
Dim Evaluator as MatchEvaluator = New MatchEvaluator(AddressOf MatchFunc)
Target = R.Replace(Target, Evaluator)
这两个代码片段都通过将匹配文本用
<<...>>
包裹来突出显示每个匹配。使用委托的优势在于,在计算替换内容时可以包含任意复杂的代码。例如,将摄氏度转换为华氏度的示例:
Function MatchFunc(ByVal M as Match) as String
'Get numeric temperature from $1, then convert to Fahrenheit
Dim Celsius as Double = Double.Parse(M.Groups(1).Value)
Dim Fahrenheit as Double = Celsius * 9 / 5 + 32
Return Fahrenheit & "F" 'Append an "F", and return
End Function
Dim Evaluator as MatchEvaluator = New MatchEvaluator(AddressOf MatchFunc)
Dim RRTemp as Regex = New Regex("(\d+)C\b", RegexOptions.IgnoreCase)
Target = RRTemp.Replace(Target, Evaluator)
若
Target
为
Temp is 37C.
,则会将其替换为
Temp is 98.6F.
。
2. 正则分割方法
2.1 基本分割
RegexObj.Split
方法将对象的正则表达式应用于目标字符串,返回由匹配项分隔的字符串数组。例如:
Dim R as New Regex("\.")
Dim Parts as String() = R.Split("209.204.146.22")
R.Split
返回由四个字符串
('209', '204', '146', '22')
组成的数组,这些字符串由文本中
\.
的三次匹配分隔。
2.2 带计数的分割
如果提供了
count
参数,则返回的字符串数量不超过
count
(除非使用了捕获括号,稍后会详细说明)。若未提供
count
,
Split
会返回由匹配分隔的尽可能多的字符串。提供
count
可能意味着正则表达式在最后一次匹配之前停止应用,此时最后一个字符串包含该行未分割的剩余部分。例如:
Dim R as New Regex("\.")
Dim Parts as String() = R.Split("209.204.146.22", 2)
此时,
Parts
接收两个字符串
'209'
和
'204.146.22'
。
2.3 带偏移量的分割
如果提供了偏移量(整数),在尝试应用正则表达式之前会跳过目标字符串中的相应字符数。跳过的文本成为返回的第一个字符串的一部分(除非指定了
RegexOptions.RightToLeft
,此时跳过的文本成为返回的最后一个字符串的一部分)。
2.4 使用捕获括号进行分割
如果使用了任何类型的捕获括号,通常会将捕获的文本的额外条目插入数组中。例如,要将
'2006-12-31'
或
'04/12/2007'
这样的字符串拆分为其组成部分,可以使用以下代码:
Dim R as New Regex("[-/]")
Dim Parts as String() = R.Split(MyDate)
这将返回三个数字(作为字符串)的列表。然而,添加捕获括号并使用
([-/,])
作为正则表达式会使
Split
返回五个字符串。如果
MyDate
包含
'2006-12-31'
,则这些字符串为
'2006'
,
'-'
,
'12'
,
'-'
,
'31'
。额外的
'-'
元素来自每次捕获的
$1
。
如果有多个捕获括号组,它们将按数字顺序插入(这意味着所有命名捕获都在所有未命名捕获之后)。
只要所有捕获括号组都实际参与匹配,
Split
与捕获括号的配合就能正常工作。但当前版本的 .NET 存在一个 bug,如果有一组捕获括号未参与匹配,则该组及其编号更高的组不会向返回列表中添加元素。
例如,想要按逗号及其周围的可选空格进行分割,并将空格添加到返回的元素列表中,可以使用
(\s+)?,(\s+)?
。当应用于
'this , that'
时,返回四个字符串
'this'
,
' '
,
' '
,
'that'
。但当应用于
'this, that'
时,第一组捕获括号无法匹配,会阻止为其(以及后续所有组)添加元素到列表中,因此只返回两个字符串
'this'
和
'that'
。目前的实现无法预先确切知道每次匹配将返回多少个字符串,这是一个主要缺点。在这个特定示例中,可以通过使用
(\s+),(\s+)
来解决问题(在这种情况下,两个组都保证参与任何整体匹配),但更复杂的表达式则不容易重写。
3. 查询正则表达式信息的方法
3.1 查询捕获组信息
以下方法允许查询正则表达式中捕获组的名称(包括数字名称和命名捕获的名称):
-
RegexObj.GetGroupNames()
-
RegexObj.GetGroupNumbers()
-
RegexObj.GroupNameFromNumber(number)
-
RegexObj.GroupNumberFromName(name)
这些方法不涉及任何特定匹配,仅涉及正则表达式中存在的组的名称和编号。
3.2 查询 Regex 对象本身的信息
以下方法允许查询
Regex
对象本身的信息(而不是将正则表达式对象应用于字符串):
-
RegexObj.ToString()
:返回最初传递给正则表达式构造函数的模式字符串。
-
RegexObj.RightToLeft
:返回一个布尔值,指示是否在正则表达式中指定了
RegexOptions.RightToLeft
。
-
RegexObj.Options
:返回与正则表达式关联的
RegexOptions
。各个选项的值如下表所示,报告时会将它们相加:
| 值 | 选项 |
| ---- | ---- |
| 0 | None |
| 16 | Singleline |
| 1 | IgnoreCase |
| 32 | IgnorePatternWhitespace |
| 2 | Multiline |
| 64 | RightToLeft |
| 4 | ExplicitCapture |
| 256 | ECMAScript |
| 8 | Compiled |
缺失的 128 值是用于 Microsoft 调试选项,在最终产品中不可用。
以下代码展示了如何显示
Regex
对象的相关信息:
'Display information known about the Regex object in the variable R
Console.WriteLine("Regex is: " & R.ToString())
Console.WriteLine("Options are: " & R.Options)
If R.RightToLeft
Console.WriteLine("Is Right-To-Left: True")
Else
Console.WriteLine("Is Right-To-Left: False")
End If
Dim S as String
For Each S in R.GetGroupNames()
Console.WriteLine("Name """ & S & """ is Num #" & R.GroupNumberFromName(S))
Next
Console.WriteLine("---")
Dim I as Integer
For Each I in R.GetGroupNumbers()
Console.WriteLine("Num #" & I & " is Name """ & R.GroupNameFromNumber(I) & """")
Next
运行两次,分别使用以下两个
Regex
对象:
New Regex("ˆ(\w+)://([ˆ/]+)(/\S+)")
New Regex("ˆ(?<proto>\w+)://(?<host>[ˆ/]+)(?<page>/\S+)", RegexOptions.Compiled)
会产生以下输出(为适应页面,其中一个正则表达式被截断):
Regex is: ˆ(\w+)://([ˆ/]+)(/\S+)
Option are: 0
Is Right-To-Left: False
Name "0" is Num #0
Name "1" is Num #1
Name "2" is Num #2
Name "3" is Num #3
---
Num #0 is Name "0"
Num #1 is Name "1"
Num #2 is Name "2"
Num #3 is Name "3"
Regex is: ˆ(?<proto>\w+)://(?<host> ⋅⋅⋅
Option are: 8
Is Right-To-Left: False
Name "0" is Num #0
Name "proto" is Num #1
Name "host" is Num #2
Name "page" is Num #3
---
Num #0 is Name "0"
Num #1 is Name "proto"
Num #2 is Name "host"
Num #3 is Name "page"
4. 使用 Match 对象
Match
对象由
Regex
的
Match
方法、
Regex.Match
静态函数(稍后讨论)以及
Match
对象自身的
NextMatch
方法创建。它封装了与正则表达式单次应用相关的所有信息,具有以下属性和方法:
4.1 基本属性
-
MatchObj.Success:返回一个布尔值,指示匹配是否成功。若不成功,对象是静态Match.Empty对象的副本。 -
MatchObj.Value和MatchObj.ToString():返回实际匹配的文本副本。 -
MatchObj.Length:返回实际匹配文本的长度。 -
MatchObj.Index:返回一个整数,指示在目标文本中找到匹配的位置。它是基于零的索引,即从字符串开头(左侧)到匹配文本开头(左侧)的字符数,即使使用RegexOptions.RightToLeft创建生成此Match对象的正则表达式,该规则仍然适用。
4.2 分组属性
MatchObj.Groups
属性是一个
GroupCollection
对象,其中封装了多个
Group
对象。它是一个普通的集合对象,具有
Count
和
Item
属性,但最常见的访问方式是通过索引获取单个
Group
对象。例如,
M.Groups(3)
是与第三组捕获括号相关的
Group
对象,
M.Groups("HostName")
是 “Hostname” 命名捕获的组对象(例如,在正则表达式中使用
(?<HostName>...)
之后)。需要注意的是,C# 中需要使用
M.Groups[3]
和
M.Groups["HostName"]
。第 0 组表示整个匹配本身,例如,
MatchObj.Groups(0).Value
与
MatchObj.Value
相同。
4.3 其他方法
-
MatchObj.NextMatch():重新调用原始正则表达式以在原始字符串中查找下一个匹配,返回一个新的Match对象。 -
MatchObj.Result(string):处理给定字符串中的特殊序列,返回处理后的文本。例如:
Dim M as Match = Regex.Match(SomeString, "\w+")
Console.WriteLine(M.Result("The first word is ’$&’"))
可以使用它获取匹配左右的文本副本:
M.Result("$‘") '这是匹配左侧的文本
M.Result("$’") '这是匹配右侧的文本
在调试时,显示类似
M.Result("[$‘<$&>$’]")
的内容可能会有帮助。例如,将
\d+
应用于字符串
'May 16, 1998'
创建的
Match
对象,会返回
'May <16>, 1998'
,清晰显示了确切的匹配。
-
MatchObj.Synchronized()
:返回一个新的
Match
对象,与当前对象相同,但可安全用于多线程。
-
MatchObj.Captures
属性不常使用,从第 437 页开始讨论。
5. 使用 Group 对象
Group
对象包含一组捕获括号的匹配信息(如果是第 0 组,则表示整个匹配)。它具有以下属性和方法:
5.1 基本属性
-
GroupObj.Success:返回一个布尔值,指示该组是否参与了匹配。并非所有组都一定会参与成功的整体匹配。例如,若(this)<(that)匹配成功,其中一组括号保证参与匹配,而另一组则保证未参与。 -
GroupObj.Value和GroupObj.ToString():返回该组捕获的文本副本。若匹配不成功,返回空字符串。 -
GroupObj.Length:返回该组捕获的文本长度。若匹配不成功,返回 0。 -
GroupObj.Index:返回一个整数,指示在目标文本中找到该组匹配的位置。返回值是基于零的索引,即从字符串开头(左侧)到捕获文本开头(左侧)的字符数,即使使用RegexOptions.RightToLeft创建生成此Match对象的正则表达式,该规则仍然适用。 -
GroupObj.Captures属性从第 437 页开始讨论。
6. 静态“便利”函数
在正则表达式的使用中,并不总是需要显式创建
Regex
对象。以下静态函数允许直接应用正则表达式:
-
Regex.IsMatch(target, pattern)
-
Regex.IsMatch(target, pattern, options)
-
Regex.Match(target, pattern)
-
Regex.Match(target, pattern, options)
-
Regex.Matches(target, pattern)
-
Regex.Matches(target, pattern, options)
-
Regex.Replace(target, pattern, replacement)
-
Regex.Replace(target, pattern, replacement, options)
-
Regex.Split(target, pattern)
-
Regex.Split(target, pattern, options)
这些函数本质上是对我们已经看到的核心
Regex
构造函数和方法的包装。它们为你构造一个临时的
Regex
对象,使用它调用你请求的方法,然后丢弃该对象(实际上并非真正丢弃,稍后会详细说明)。
例如:
If Regex.IsMatch(Line, "ˆ\s+$")
这与以下代码相同:
Dim TemporaryRegex = New Regex("ˆ\s+$")
If TemporaryRegex.IsMatch(Line)
或者更准确地说,与以下代码相同:
If New Regex("ˆ\s+$").IsMatch(Line)
使用这些便利函数的优点是,它们通常使简单任务更轻松、更便捷,使面向对象的包看起来像过程式的。缺点是每次都必须重新检查模式。
如果正则表达式在整个程序执行过程中只使用一次,从效率角度来看,使用便利函数与否无关紧要。但如果正则表达式多次使用(例如在循环中或常用函数中),每次准备正则表达式都会有一些开销。避免这种通常昂贵的开销是构建一次
Regex
对象,然后在实际检查文本时反复使用它的主要原因。不过,.NET 提供了一种两全其美的方法:兼具过程式的便利和面向对象的效率。
7. 正则表达式缓存
每次使用小的正则表达式都要构建和管理一个单独的
Regex
对象可能会很繁琐和不便,因此 .NET 正则表达式包提供了各种静态方法,这是非常好的。然而,静态方法的一个效率缺点是,理论上每次调用都会为你创建一个临时的
Regex
对象,应用它,然后丢弃它。在循环中多次对同一正则表达式执行此操作可能会有很多冗余工作。
为避免重复工作,.NET Framework 为通过静态方法创建的临时对象提供了缓存。一般来说,缓存的相关内容在第 6 章讨论,但简而言之,如果在静态方法中使用的正则表达式与“最近”使用的相同,静态方法将重用之前创建的正则表达式对象,而不是从头开始构建一个新的。
“最近”的默认含义是最近的 15 个正则表达式会被缓存。如果在循环中使用超过 15 个正则表达式,缓存的好处就会丧失,因为第 16 个正则表达式会挤出第一个,这样当你重新启动循环并重新应用时,第一个正则表达式已不在缓存中,必须重新生成。
如果默认的 15 个的大小对你来说太小,可以进行调整:
Regex.CacheSize = 123
如果你想禁用所有缓存,可以将值设置为 0。
8. 支持函数
除了前面讨论的便利函数外,还有一些其他的静态支持函数:
8.1 Regex.Escape
Regex.Escape(string)
函数接受一个字符串,返回一个将正则表达式元字符转义后的副本。这使得原始字符串适合作为文字字符串包含在正则表达式中。例如,如果用户输入存储在字符串变量
SearchTerm
中,可以使用以下代码构建正则表达式:
Dim UserRegex as Regex = New Regex("ˆ" & Regex.Escape(SearchTerm) & "$", RegexOptions.IgnoreCase)
这允许搜索词包含正则表达式元字符,而不会将它们视为元字符。如果不进行转义,例如
SearchTerm
的值为
':-)'
,会抛出
ArgumentException
。
8.2 Regex.Unescape
Regex.Unescape(string)
函数接受一个字符串,返回一个解释了某些正则表达式字符转义序列并移除其他反斜杠的副本。例如,如果传入
'\:\-\)'
,它将返回
':-)'
。字符简写也会被解码。如果原始字符串中有
'\n'
,在返回的字符串中实际上会被替换为换行符。或者如果有
'\u1234'
,相应的 Unicode 字符将被插入到字符串中。第 407 页顶部列出的所有字符简写都会被解释。虽然很难想象
Regex.Unescape
在正则表达式相关方面有很好的用途,但它可能作为一个通用工具,为 VB 字符串赋予一些转义知识。
8.3 Match.Empty
Match.Empty
函数返回一个表示匹配失败的
Match
对象。它可能用于初始化一个
Match
对象,该对象可能会也可能不会在稍后填充,但确实打算稍后查询。例如:
Dim SubMatch as Match = Match.Empty 'Initialize, in case it’s not set in the loop below
Dim Line as String
For Each Line in EmailHeaderLines
'If this is the subject, save the match info for later...
Dim ThisMatch as Match = Regex.Match(Line, "ˆSubject:\s+(.+)", RegexOptions.IgnoreCase)
If ThisMatch.Success
SubMatch = ThisMatch
End If
Next
If SubMatch.Success
Console.WriteLine(SubMatch.Result("The subject is: $1"))
Else
Console.WriteLine("No subject!")
End If
如果字符串数组
EmailHeaderLines
实际上没有行(或没有主题行),遍历它们的循环将永远不会设置
SubMatch
,因此如果没有以某种方式初始化,循环后对
SubMatch
的检查将导致空引用异常。所以,在这种情况下使用
Match.Empty
作为初始化器很方便。
8.4 Regex.CompileToAssembly
Regex.CompileToAssembly(...)
允许创建一个封装
Regex
对象的程序集,具体内容见下一节。
综上所述,.NET 中的正则表达式功能丰富且强大,通过合理使用这些方法和特性,可以高效地处理各种文本匹配和替换任务。同时,要注意一些细节,如捕获括号在分割时的使用以及缓存的设置,以避免潜在的问题和提高性能。
9. 总结与注意事项
9.1 功能总结
在 .NET 中使用正则表达式,我们拥有了丰富的工具和方法来处理文本匹配、替换和分割等任务。以下是对主要功能的总结:
-
替换功能
:通过
Regex.Replace
方法和
Match.Result
方法,可以实现强大的替换操作,支持特殊替换序列,还能使用替换委托进行复杂的替换处理。
-
分割方法
:
RegexObj.Split
方法可将目标字符串按正则匹配进行分割,支持带计数、偏移量以及捕获括号的分割。
-
信息查询
:提供了多种方法用于查询正则表达式中捕获组的信息以及
Regex
对象本身的信息。
-
对象使用
:
Match
对象和
Group
对象封装了匹配和分组的详细信息,方便我们获取和处理匹配结果。
-
静态便利函数
:一系列静态函数让简单的正则操作更加便捷,但多次使用同一正则表达式时可能存在效率问题。
-
缓存机制
:.NET 为静态方法创建的临时对象提供了缓存,可避免重复创建正则对象的开销。
-
支持函数
:包括
Regex.Escape
、
Regex.Unescape
、
Match.Empty
和
Regex.CompileToAssembly
等,为正则表达式的使用提供了更多便利和扩展。
9.2 注意事项
在使用这些功能时,需要注意以下几点:
-
捕获括号问题
:在使用
Split
方法时,若使用捕获括号,要注意当前版本 .NET 存在的 bug,即部分捕获括号未参与匹配时可能影响返回结果。对于复杂表达式,重写以解决此问题可能并不容易。
-
缓存设置
:正则表达式缓存的默认大小为 15 个,若在循环中使用的正则表达式数量超过此值,缓存将失效。可根据实际情况调整缓存大小或禁用缓存。
-
性能考虑
:对于多次使用的正则表达式,建议构建一次
Regex
对象并反复使用,以避免每次准备正则表达式的开销。
10. 实际应用示例
为了更好地理解和应用上述知识,下面给出一些实际应用示例。
10.1 验证邮箱地址
我们可以使用正则表达式来验证一个字符串是否为有效的邮箱地址。示例代码如下:
Dim emailPattern As String = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
Dim emailRegex As New Regex(emailPattern)
Dim email As String = "example@example.com"
If emailRegex.IsMatch(email)
Console.WriteLine("Valid email address.")
Else
Console.WriteLine("Invalid email address.")
End If
在这个示例中,我们定义了一个邮箱验证的正则表达式模式,创建了
Regex
对象,然后使用
IsMatch
方法验证邮箱地址是否有效。
10.2 提取 HTML 标签中的内容
假设我们有一个 HTML 字符串,想要提取其中所有
<p>
标签内的内容。示例代码如下:
Dim html As String = "<html><body><p>First paragraph</p><p>Second paragraph</p></body></html>"
Dim htmlPattern As String = "<p>(.*?)</p>"
Dim htmlRegex As New Regex(htmlPattern, RegexOptions.IgnoreCase)
Dim matches As MatchCollection = htmlRegex.Matches(html)
For Each match As Match In matches
Console.WriteLine(match.Groups(1).Value)
Next
在这个示例中,我们使用
Matches
方法查找所有匹配的
<p>
标签,并通过
Groups(1)
获取标签内的内容。
10.3 格式化电话号码
假设我们有一个包含电话号码的字符串,格式可能不统一,我们想要将其格式化为统一的
(XXX) XXX-XXXX
格式。示例代码如下:
Function FormatPhoneNumber(ByVal M As Match) As String
Dim phoneNumber As String = M.Value
phoneNumber = phoneNumber.Replace("-", "").Replace(" ", "").Replace("(", "").Replace(")", "")
Return String.Format("({0}) {1}-{2}", phoneNumber.Substring(0, 3), phoneNumber.Substring(3, 3), phoneNumber.Substring(6))
End Function
Dim phonePattern As String = "\d{3}[-\s]?\d{3}[-\s]?\d{4}"
Dim phoneRegex As New Regex(phonePattern)
Dim phoneText As String = "123-456-7890 123 456 7890 (123)4567890"
Dim evaluator As New MatchEvaluator(AddressOf FormatPhoneNumber)
Dim formattedText As String = phoneRegex.Replace(phoneText, evaluator)
Console.WriteLine(formattedText)
在这个示例中,我们使用替换委托
MatchEvaluator
来实现电话号码的格式化。
11. 流程图示例
下面是一个简单的流程图,展示了使用正则表达式进行文本匹配和替换的基本流程:
graph TD;
A[开始] --> B[定义正则表达式模式];
B --> C[创建 Regex 对象];
C --> D[选择操作类型];
D --> |匹配| E[使用 IsMatch 或 Match 方法];
D --> |替换| F[使用 Replace 方法];
D --> |分割| G[使用 Split 方法];
E --> H{匹配成功?};
H --> |是| I[处理匹配结果];
H --> |否| J[结束];
F --> K[获取替换结果];
G --> L[获取分割后的数组];
I --> J;
K --> J;
L --> J;
J[结束];
这个流程图清晰地展示了在 .NET 中使用正则表达式的基本步骤,从定义模式到选择操作类型,再到处理结果,最后结束流程。
12. 总结与展望
通过本文的介绍,我们深入了解了 .NET 中正则表达式的各种功能和使用方法。正则表达式在文本处理中具有重要的作用,能够帮助我们高效地完成各种匹配、替换和分割任务。在实际应用中,我们要根据具体需求选择合适的方法和工具,并注意一些细节和性能问题。
随着技术的不断发展,正则表达式的应用场景也在不断扩大。未来,我们可以期待 .NET 对正则表达式的支持更加完善,提供更多的功能和优化,以满足更复杂的文本处理需求。同时,开发者也可以结合其他技术和工具,进一步拓展正则表达式的应用范围,创造出更加高效和强大的文本处理程序。
希望本文能够帮助你更好地掌握 .NET 中正则表达式的使用,在实际开发中更加得心应手地处理文本任务。如果你有任何疑问或建议,欢迎留言讨论。
超级会员免费看

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



