C# 正则表达式的强大功能:如何有效地解析和转换字符串

C# 正则表达式的强大功能:如何有效地解析和转换字符串

编码文章call10242025-02-01 3:40:1321A+A-


假设您有一大块文本,并且想要解析它以获取一些信息。 或者您甚至想用另一种模式替换某些模式。 根据提取过程的复杂程度,整个“拆分、搜索和重组”工作流程可能会很乏味且未经优化。 有时,也很难知道在哪里剪切内容来解析您想要的数据。

相反,您可以使用正则表达式,也称为“regex”。

那么,什么是正则表达式?

正如维基百科中所解释的:

正则表达式(缩写为 regex 或 regexp;也称为有理表达式)是指定搜索模式的字符序列。

该工具来自理论计算机科学,更具体地说,来自形式语言的子领域。 大多数编程语言都有正则表达式; 当然,这些年来也出现了一些标准。

您可能已经遇到过正则表达式:它们通常写在斜杠 / 之间(或者作为 Python 中的原始字符串,使用 r'' 语法),并且看起来有点奇怪,如下所示:

/(#?\w{6})|(rgb\s?\(\s?(\d{1,3},\s?){2}\d{1,3}\s?\))/g

正如我们将在本文中看到的,该字符串中的所有奇怪字符实际上都是标记和特定模式,正则表达式评估过程将使用它们来测试您的输入字符串是否与该搜索模式相对应。 例如,上面的正则表达式检查您的文本是否包含一次或多次出现以十六进制或 255 — RGB 形式编写的颜色。

但如何呢? 这些奇怪的括号和大括号是什么意思? 为什么最后是g? 这段文字的颜色在哪里? 好吧 - 是时候深入研究正则表达式的逻辑了!

Matches? Groups?

将正则表达式应用于文本时,您会得到所谓的“匹配”。 这个想法是,正则表达式评估将遍历输入文本并尝试将您的搜索模式与内容相匹配:只要找到与查询相对应的文本位,它就会标记结果。 根据您的评估参数(正则表达式标志),您可以在第一个匹配后停止,查找所有匹配项,进行不区分大小写的搜索等。

但借助分组,您可以进行更“精确”的数据提取。 组是一种进一步指定要将输入切入的不同部分的方法:您可以“标记”搜索模式的位,以便在评估输入时返回带有此附加信息的匹配项。 我们说我们捕获了一组中的部分输入。 一个搜索模式可以包含多个捕获组,在这种情况下,一次匹配将返回组 0(即整个匹配),然后为每个捕获组返回一个子结果。

这些组还允许您进行反向引用,这意味着您可以告诉搜索模式查找之前匹配的组。 这种参考是动态的:您不必编写手动重复的模式,而是让它在评估过程中被填充。

我们很快就会看到如何做到这一切;)

当角色不仅仅是角色时……

因此,当您使用正则表达式时,您可以使用文本字符定义搜索模式,就像普通字符串一样; 但这些字符可以被赋予更“进化”的含义——它们实际上是元字符。 例如,点 . 字符是“您想要的任何字符”的占位符。

假设您有以下正则表达式搜索模式:/a./。 然后,它将匹配以下所有字符串:aa、ab、a1...任何包含 a 字符且其后跟随另一个 ASCII 字符的字符串都将匹配。

根据经验:普通的旧字母字符和数字只是原始字符,但几乎所有“特殊”字符都具有这种“扩展”含义(*、+、?、[、]、(、)...)。 换句话说,对于那些特殊字符,如果你想匹配原始字符而不是使用它的“元版本”,你需要在它前面添加一个反斜杠\来转义它:

/a./  => matches strings containing "a" then any character
/a\./ => matches only the string a.

正则表达式元字符或标记有多种类型。 如果您想浏览整个列表并更好地了解每个列表的作用,我建议您查看很棒的网站 regex101.com。 它告诉我们,我们可以将模式组织成很多类别。 最常见的是:

元序列:这些特定的标记是更复杂的“可能值的集合”的简写 - 就像代表“任何数字”的 \d 标记

字符类是相似的:通过在一堆字符周围放置一些方括号,您可以在搜索模式的此位置定义有效值列表; 例如,常见用法是允许模式中包含任何小写字母,可以写为:[a-z](“a 和 z 之间的任何字符”,默认区分大小写)

锚点:它们用于强制输入字符串中的搜索模式“位置”; 假设您只想匹配给定的模式(如果 位于字符串的开头),那么您可以在模式的开头使用 ^ 锚点来在搜索过程中强制执行此操作

量词让您选择模式必须出现多少次才能获得匹配:后跟星号 * 的模式如果出现零次或多次,将被视为匹配; 一个后跟一个加号 + 必须至少出现一次,等等。

如果您需要更精确地计算模式的出现次数,可以使用花括号,如下所示:

/^a{2}b{3,}\d{1,3}$/

此正则表达式将匹配恰好包含 2 个 as,然后包含 3 个或更多 b,然后包含任何 1 到 3 个数字的任何字符串。 它周围的 ^ 和 $ 锚告诉评估器忽略仅部分匹配该模式的任何字符串。 因此,例如,这些字符串将起作用:

aabbb1
aabbb12
aabbb138
aabbb111
aabbbbbb1

但这些不会:

abbb
aabbb
aabbb12
aabbb11111
bbbbb123
aabbb1?

但要小心:并非所有标记都在所有实现中定义! 根据所使用的标准,您的编程语言中的内置正则表达式评估器可能缺少某些功能......

注意:特别是在 Python 中,您很快就会脱离内置正则表达式功能的范围 - 来自 re 模块的功能。 您可能需要转向正则表达式模块!

一些基本的正则表达式示例

示例1:匹配各种可变大小写

您可能知道不同的编程语言有不同的命名约定。 例如,Python 用户是 Snake_Case 爱好者,而 JS 开发人员通常更喜欢 CamelCase 甚至 PascalCase。

现在,如果您使用一种约定编写了一个脚本,而您想将其切换为另一种约定,该怎么办? 如果脚本很长,手动执行会非常痛苦 - 并且简单的拆分不会对您有太大帮助。

另一方面,正则表达式可以轻松匹配这些不同的大小写,因此您可以根据自己的喜好替换名称! 以下是分别匹配 Snake_Case、CamelCase 和 PascalCase 的三种搜索模式:

/^[a-z][a-z0-9]+(_[a-z][a-z0-9]+)*$/
/^[a-z][a-z0-9]+(?:[A-Z][a-z0-9]+)*$/
/^[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)*$/

这些树正则表达式将分别匹配以下变量名称,例如:

obj1_main_id2
obj1MainId2
Obj1MainId2

正如您所看到的,这些搜索模式甚至允许变量名称中包含数字,只要它不是位于“单词”的开头。

示例 2:进行反向引用并使用多种模式

假设您正在使用一个非常基本的 HTML 解析器。 与所有其他标记语言一样,HTML 依赖于定义嵌套块的开始和结束标记。 那么如果您希望解析器能够匹配块怎么办? 您需要它能够获得与上次打开的标签相匹配的结束标签,对吧?

这可以通过对捕获的组使用回溯来实现。

在这种情况下,您需要处理两种情况:

自闭合标签(或“void 元素”):这些都是没有任何内部内容且仅具有属性的 HTML 元素,例如 ...

具有内部文本或 html 的常见标签,例如基本的

...
... 等。

好消息是我们可以仅使用一个正则表达式来匹配两种类型的标签! ;)

为此,我们将使用管道字符 |。 它可能会让您想起 C 族语言中的“或”条件(即使在这种情况下它是双倍的:if (a < 1 || a > 5) {...})。 这个管道让我们定义多个可能的模式来搜索,这样,如果这些子模式中有任何一个匹配,那么评估器就会将输入视为匹配。

第一种情况,自关闭标签,可以使用以下模式进行匹配:

<([^<]+) ?\/>

在这里,我使用了一些令牌类型和技巧:

分隔的 < 和 > 字符只是评估器“按原样”匹配的原始字符

我们还在模式的开头有一个捕获组,用于提取特定子结果中标签的内容(其名称和属性)

该组包含一个具有“例外模式”的字符类:[^<] 标记意味着求值器将匹配“所有不是 < 的字符”

然后,我们在标签结束标记之间可能有也可能没有空格(这是很好的做法,但不是强制性的......)所以我们使用 ? 量词

最后,我们得到了必须转义的结束斜杠 /

对于带有内容的完整标签,模式是类似的,只是我们在最后添加了一些反向引用来检查结束标签是否与开始标签匹配,因此我们必须将标签名称(组 1)与属性(组 2)分开 ):

<([^<]*) ?([^<]*)?>\s*(.*?)\s*<\/\1>

正如您所看到的,我们大多会重复使用相同的标记和一些新的标记,例如 \s 来匹配任何“非单词字符”(即任何可以“分隔单词”的东西)或匹配模式零或的 * 量词 更多次。

真正的大变化是在搜索模式的末尾,当我们使用 \1 时。 该标记是对我们之前捕获的组 1 的反向引用:它将尝试第二次重新匹配相同的文本位 - 在我们的例子中是标签名称。

我们现在可以将这两种搜索模式合并为一种,如下所示:

/<([^<]+) ?\/>|<([^<]*) ?([^<]*)?>\s*(.*?)\s*<\/\2>/

但请注意,由于模式的第一部分中有一个新的捕获组,因此组索引发生了移动,因此我们需要将反向引用更新为 \2!

示例 3:转义、锚点、标志、量词等等!

现在,假设您想要浏览图像文件名列表并获取所有 JPG 文件。 问题是扩展名尚未标准化,因此您可能拥有 .jpg、.jpeg、.JPG 或 .JPEG 文件。

为了快速匹配所有这些变体并且仅匹配那些变体,您可以使用以下正则表达式:

/[\w.]+\.jpe?g$/i

我们将分解此正则表达式的不同部分以使其更清晰:

第一部分将元序列 \w 嵌套在字符类内部。 \w 指“任何单词字符” - 因此它将匹配除空格、制表符或其他“分隔单词”的字符之外的任何文本字符。 然后,我们有一个点字符(实际的原始字符,因为字符类不考虑元字符,而只考虑原始字符)。 所有这些都归结为:“匹配任何单词字符或点字符”。

那么,+ 量词意味着我们在扩展名之前必须至少有一个字符:任何仅名为 .jpg 的文件都被视为无效

然后,点之前的反斜杠告诉正则表达式求值器我们不希望点字符用作元字符,在这里; 我们只想要它的原始值。 在字符串中完全匹配

那么,我们还有另一个量词:?; 这允许我们将 e 字符标记为可选:输入字符串无论出现零次还是一次都将被视为匹配,这同时为我们提供了 .jpg 和 .jpeg 扩展名

然后, $ 锚点确保我们只查看输入的末尾:如果有人错误地命名了文件,例如 my_image.jpg.png,那么我们会认为它是 PNG 并忽略它 - 因为 .jpg 不在输入字符串的末尾!

最后, i 标志(在正则表达式的尾部斜杠之后)指示搜索不区分大小写:这样,我们可以同时匹配 .jpg 和 .JPG 扩展名

我们甚至可以通过添加一些括号来使用捕获组将文件名与其扩展名分开:

/([\w.]+)\.(jpe?g)$/i

评估此正则表达式后,每个匹配项将由 3 组组成:默认组 0(整个匹配项)、包含文件名的组 1(最后一个点之前的所有内容)和包含扩展名的组 2(不包含点) 。

并且,根据您的正则表达式实现,您可以命名这些捕获组,这样您就可以拥有易于识别的字符串键,而不是整数索引:

/(?[\w.]+)\.(?jpe?g)$/i

当你有很多想要提取的组并且你觉得你可能会迷失在索引中时,这非常方便......:)

在 C# 中使用正则表达式

当然,C# 有一个内置的正则表达式实现! 要使用正则表达式,您只需导入
System.Text.RegularExpressions 包,然后您就可以轻松创建正则表达式变量并测试输入字符串:

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main(string[] args)
    {
        Regex rx = new Regex(@"[hj]ello\s+[a-z]orld$");

        string[] tests = new string[] {
            "hello world",
            "jello world",
            "Hello world",
            "hello   world",
            "hello   torld",
            "hello world!",
        };
        foreach (string test in tests) {
            Console.WriteLine("\"{0}\" matches? {1}", test, rx.IsMatch(test) ? "True" : "False");
        }
        // outputs:
        // "hello world" matches? True
        // "jello world" matches? True
        // "Hello world" matches? False
        // "hello   world" matches? True
        // "hello   torld" matches? True
        // "hello world!" matches? False
    }
}

您还可以在输入上执行正则表达式后检索单个匹配项。 这是我们用 C# 编写的 JPG 文件过滤示例:

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main(string[] args)
    {
        Regex rx = new Regex(@"([\w.]+)\.(jpe?g)$", RegexOptions.IgnoreCase);
        string[] tests = new string[] {
            "img1.jpg",
            "img2.JPEG",
            "img3.jpg.png",
            ".jpg",
        };
        foreach (string test in tests) {
            Match m = rx.Match(test);
            if (m.Success) {
                string name = m.Groups[1].Value;
                string ext = m.Groups[2].Value;
                Console.WriteLine("\"{0}\" is a JPG (name: {1}, extension: {2})", test, name, ext);
            }
            else {
                Console.WriteLine("\"{0}\" is not a valid JPG file!", test);
            }
        }
        // outputs:
        // "img1.jpg" is a JPG (name: img1, extension: jpg)
        // "img2.JPEG" is a JPG (name: img2, extension: JPEG)
        // "img3.jpg.png" is not a valid JPG file!
        // ".jpg" is not a valid JPG file!
    }
}

如果您使用全局正则表达式标志 g,它基本上告诉评估器即使在第一个匹配之后也继续进行,那么您可以通过将 Match() 函数更改为 Matches() 来迭代所有匹配,然后循环结果 存储在 MatchCollection 对象中。 这是相同的示例,但其中列表作为单个字符串提供:

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main(string[] args)
    {
        Regex rx = new Regex(
            @"([\w.]+)\.(jpe?g)$",
            RegexOptions.IgnoreCase | RegexOptions.Multiline
        );
        string filesList =
            "img1.jpg\n" +
            "img2.JPEG\n" +
            "img3.jpg.png\n" +
            ".jpg";
        MatchCollection matches = rx.Matches(filesList);
        Console.WriteLine("Found {0} valid files.", matches.Count);
        foreach (Match m in matches) {
            string name = m.Groups[1].Value;
            string ext = m.Groups[2].Value;
            Console.WriteLine("\"{0}\" is a JPG (name: {1}, extension: {2})", m.Value, name, ext);
        }
        // outputs:
        // Found 2 valid files.
        // "img1.jpg" is a JPG (name: img1, extension: jpg)
        // "img2.JPEG" is a JPG (name: img2, extension: JPEG)
    }
}

最后,提取命名组非常简单 - 您只需将 int 键替换为自定义字符串键即可:

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main(string[] args)
    {
        Regex rx = new Regex(
            @"(?[\w.]+)\.(?jpe?g)$",
            RegexOptions.IgnoreCase | RegexOptions.Multiline
        );
        string filesList =
            "img1.jpg\n" +
            "img2.JPEG\n" +
            "img3.jpg.png\n" +
            ".jpg";
        MatchCollection matches = rx.Matches(filesList);
        Console.WriteLine("Found {0} valid files.", matches.Count);
        foreach (Match m in matches) {
            string name = m.Groups["name"].Value;
            string ext = m.Groups["ext"].Value;
            Console.WriteLine("\"{0}\" is a JPG (name: {1}, extension: {2})", m.Value, name, ext);
        }
        // outputs:
        // Found 2 valid files.
        // "img1.jpg" is a JPG (name: img1, extension: jpg)
        // "img2.JPEG" is a JPG (name: img2, extension: JPEG)
    }
}

结论

正则表达式是一种非常强大的数据提取工具。 对于长文本,它们可以是搜索复杂模式、将其解析为子结果甚至直接用其他内容完全替换它们的优化方式。

即使它们乍一看有点奇怪,您最终也可以学会在大多数编程语言中使用它们并将它们应用于很多情况。

请注意,如果您希望正则表达式非常快(与基本字符串操作一样快甚至更快),您很可能需要预编译它们。 对于小输入,正则表达式可能比字符串操作慢。 但在复杂的情况下,仅通过字符串操作获得快速代码可能几乎不可能,因此每当事情变得非常困难时,正则表达式都是一个有趣的工具!

你呢:你是正则表达式的粉丝吗? 您在项目中经常使用它们吗? 欢迎在评论中做出反应! :)

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4