飞道的博客

C#中的正则匹配和文本处理

282人阅读  评论(0)

C#中的正则匹配和文本处理

1、简介

在博客之前上章讲了String类和StringBuilder类。尽管String类和StringBuilder类提供了一套方法用来处理基于字符串的数据, 但是正则表达式和支持它的类却为字符串处理任务提供了更强大的功能. 大多数字符串处理工作都需要在字符串中寻找特定排列规则的子串, 通过称为正则表达式的特殊语言就可以完成这个人无. 在本章大家会了解到创建正则表达式的方法以及如何利用它们解决常见的文本处理任务。

2、正则表达式介绍

所谓正则表达式是一种用于描述字符串中字符格式的语言, 正则表达式既可以用来执行字符串的搜索, 也可以用于字符串的替换。

正则表达式本身就是一个定义了用于其他字符串搜索模式的字符串. 通常情况下, 正则表达式中的字符与其自身匹配, 比如正则表达式"the"可以与字符串中任意位置找到的同样字符序列相匹配。

正则表达式还可以包含称之为元字符的特殊字符(meta characters). 元字符用于表示重复的, 可选的或分组的字符. 这 里将简要说明一下这些元字符的用法。

大多数有经验的计算机用户在工作中都会用到正则表达式, 即使那时他们并没有意识到正在这样做. 比如敲入下列命令提示符 :

                                 C:\>dir myfile.exe
那么"myfile.exe"就是正则表达式. 把正则表达式传递给dir(目录文件显示)命令, 然后在文件系统中任何与"myfile.exe"相匹配的文件都会显示在屏幕上。

许多用户还会在正则表达式中用到元字符. 当用户敲入下列内容时:

                                 C:\>dir *.cs 

这样就会用到含有元字符的正则表达式了. “.cs"是正则表达式. 而星号()是元字符,这意味着"匹配零个或更多个字符”. 然而, 表达式的剩余部分".cs"就只是在文件中找到的普通字符了. 这个正则表达式说明"匹配所有扩展名为‘cs’且文件名任意的文件". 此正则表达式传递给dir(目录文件显示)命令, 接着屏幕上就会显示出扩展名为.cs 的所有文件。

当然, 人们还可以构建并使用许多更为强大的正则表达式. 现在一起来看看如何在C#中使用正则表达式以及它们是多么的有用。

2.1、概述: 使用正则表达式

为 了 使 用 正 则 表 达 式 , 需要引入System.Text.RegularExpressions命名空间. 如果想要进行匹配, 就需要使用Match类. 如果打算做替换, 则不需要Match类了. 取而代之的是要用到Regex类的Replace方法。

首先来看看如何在字符串中进行单词匹配操作吧. 假设给定一个字符串"the quickbrown fox jumped over the lazy dog", 这里想要在字符串中找到单词"the". 下面的程 序完成了这项任务:

static void Main(string[] args)
{
   
    Regex reg = new Regex("the");
    string str1 = "the quick brown fox jumped over the lazy dog";
    Match matchSet;
    int matchPos;
    matchSet = reg.Match(str1);
    if (matchSet.Success) {
   
        matchPos = matchSet.Index;
        Console.WriteLine("在位置" + matchPos + "找到了指定字符串");
    }
    Console.ReadLine();
}

程序运行结果:

程序做的第一件事就是创建一个新的Regex 对象并且把要匹配的正则表达式传递给构造函数. str1字符串初始化之后, 程序声明了一个Match 对象matchSet. Match类提供与正则表达式进行匹配的方法。

if 语句使用了一种Match 类的属性Success 来确定是否是成功匹配. 如果值返回为True,那么正则表达式在字符串中至少匹配了一条子串. 否则的话, 存储在 Success中的值就是False。

程序还可以有另外一种方法来查看是否匹配成功. 通过把正则表达式和目标字符串传递给IsMatch方法的方式可以对正则表达式进行预测试. 如果与正则表达式产生了匹配, 那么这种方法就返回True, 否则返回False. 这种方法的操作如下所示 :

if (Regex.IsMatch(str1, "the")){
   
}

用Match 类的一个问题就是它只能存储一个匹配. 在前面的实例中, 针对子串"the"存在 两个匹配. 这里可以使用另外一种类Matches 类来存储与正则表达式的多个匹配. 为了处理所有找到的匹配可以把匹配存储到MatchCollection对象中. 这里有一个实例 :

static void Main()
{
   
    Regex reg = new Regex("the");
    string str1 = "the quick brown fox jumped over the lazy dog";
    Match matchSet;
    int matchPos;
    if (Regex.IsMatch(str1, "the")) {
   
        matchSet = reg.Match(str1);
        matchPos = matchSet.Index;
        Console.WriteLine("在位置" + matchPos + "找到了指定字符串");
    }
    Console.ReadLine();
}

接下来要讨论如何用Replace 方法把一个字符串用另一个字符串来替换. Replace方法可带有三个参数 : 一个目标字符串, 一个代表要替换的子串, 一个代表用于替换的新子串. 下面这段代码就用到了Replace方法 :

static void Main()
{
   
    Regex reg = new Regex("the");
    string str1 = "铁拳无敌俞大猷";
    str1 = Regex.Replace(str1, "铁拳无敌", "赤子之心");
    Console.WriteLine(str1);
    Console.ReadLine();
}

程序运行结果:

针对模式匹配和文本处理这里有许多RegEx和支持类的用法. 本章还将继续钻研讨论如何形成和使用更加复杂的正则表达式。

3、数量符

在编写正则表达式的时候, 经常会要想正则表达式添加数量型数据, 诸如"精确匹配两次"或者"匹配一次或多次". 利用数量符就可以把这些数据填加到正则表达式里面了。

这里要看到的第一个数量词就是加号(+). 这个数量符说明正则表达式应该匹配一个或多个该数量符前方的相邻字符. 下面的程序就举例说明了这个数量词的用法 : 数量符在编写正则表达式的时候, 经常会要想正则表达式添加数量型数据, 诸如"精确匹配两次"或者"匹配一次或多次". 利用数量符就可以把这些数据填加到正则表达式里面了。

这里要看到的第一个数量词就是加号(+). 这个数量符说明正则表达式应该匹配一个或多个该数量符前方的相邻字符. 下面的程序就举例说明了这个数量词的用法 :

static void Main()
{
   
    string[] words = new string[] {
    "bad", "boy", "baaad", "bear", "bend" };
    foreach (string word in words)
        if (Regex.IsMatch(word, "ba+"))
            Console.WriteLine($"以字母b开头的单词[{word}]中至少在开头后面出现了一次字母a");
    Console.ReadLine();
}

程序运行结果:

要匹配的单词是"bad"和"baaad". 正则表达式指明每一个以字母"b"开头并且包含一个或多个字母"a"的字符串都会产生匹配。

有较少限制的数量符就是星号(). 这个数量符说明其前方的相邻字符, 应该匹配零到多个. 但是在实践中这个数量符非常难用, 因为星号通常会导致匹配几乎所有内容. 例如,利用前面的代码, 如果把正则表达式变成读取"ba", 那么数组中的每个单词都会匹配。

问号(?)是一种精确匹配零次或一次的数量符. 如果把先前代码中的正则表达式变为"ba?d", 那么只有一个单词"bad"可以匹配。

通过在一对大括号内部放置一个数可以指定字符的匹配次数, 下面的程序说明了这个数量符的用法 :

static void Main()
{
   
    string[] words = new string[] {
    "bad", "boy", "baad", "baaad", "bear", "bend" };
    foreach (string word in words)
        if (Regex.IsMatch(word, "ba{2}d"))
            Console.WriteLine($"单词[{word}]以baad开头");
    Console.ReadLine();
}

程序运行结果:

“ba{2}d"正则表达式只能匹配字符串"baad”。

通过在大括号内提供两个数字可以说明匹配的最大值和最小值: {n,m}, 这里的n表示匹配的最小值而m则表示最大值. 在上述字符串中, 正则表达式"ba{1,3}d"将可以匹配"bad",“baad"以及"baaad”。

到目前为止已经讨论过的数量符展示的就是所谓的贪心(greedy)行为. 他们试图有尽可能多的匹配,而且这种行为经常会导致不预期的匹配. 下面是一个例子 :

static void Main() 
{
    
    string[] words= newstring[]{
   "Part", "of", "this","<b>string</b>", "is", "bold"}; 
    //这个".", 英文的句号, 在正则中代表除了换行符以外的任意单个字符
    string Regexp = "<.*>"; 
    MatchCollection aMatch; 
    foreach (string word in words) 
    {
    
        if (Regex.IsMatch(word, Regexp)) 
        {
    
            aMatch = Regex.Matches(word, Regexp); 
            for (int i = 0; i < aMatch.Count; i++) 
                Console.WriteLine(aMatch[i].Value); 
        } 
    } 
    Console.ReadLine();
} 

程序运行结果:

4、使用字符类

接下来这一小节会讨论如何用主要元素来构成正则表达式. 首先从字符类开始. 字符类描述字符串中出现字符的模式。

这里第一个要讨论的字符类就是句点(.). 这是一种非常非常容易使用的字符类. 它与字符串中任意字符匹配(除了换行符). 下面是一个实例:

static void Main()
{
   
    string str1 = "the quick brown fox jumped over the lazy dog";
    MatchCollection matchSet;
    matchSet = Regex.Matches(str1, ".");
    foreach (Match aMatch in matchSet)
        Console.WriteLine("matches at: " + aMatch.Index);
    Console.ReadLine();
}

程序运行结果(从这段程序的输出可以说明句点的工作原理):

句点可以匹配字符串中每一个单独字符。

较好利用句点的方法就是用它在字符串内部定义字符范围, 也就是用来限制字符串的开始或和结束字符. 比如我们可以使用正则表达式"t.e"来找到由t开头, 由e结尾, 并且中间只有一个任意字符的字符串 :

static void Main()
{
   
    string str1 = "the quick brown fox jumped over the lazy dog one time";
    MatchCollection matchSet;
    matchSet = Regex.Matches(str1, "t.e");
    foreach (Match aMatch in matchSet)
        Console.WriteLine("Matches at: " + aMatch.Index);
    Console.ReadLine();
}

程序运行结果:

在使用正则表达式的时候经常希望检查包含字符组的模式. 大家可以编写用一组闭合的方括号([ ])包裹着的正则表达式. 在方括号内的字符整体被作为正则表达式的一个字符类. 比如果想要编写的正则表 达 式 匹 配 任 何 小 写 的 字 母 字 符 , 可 以 写 成 如 下 这 样 的 表 达 式 : [abcdefghijklmnopqrstuvwxyz]. 但是这样是很难书写的, 所以通过连字号: [a-z]来表示字母范围的方式可以编写简写版本.

下面说明了如何利用这个正则表达式来匹配模式 :

static void Main()
{
   
    string str1 = "THE quick BROWN fox JUMPED over THE lazy DOG";
    MatchCollection matchSet;
    matchSet = Regex.Matches(str1, "[a-z]");
    foreach (Match aMatch in matchSet)
        Console.WriteLine("Matches at: " + aMatch.Index);
    Console.ReadLine();
}

上述程序中匹配的字母就是那些小写字母组成的单词"quick", “fox”, “over"和"lazy”。

字符类可以用多组字符构成. 如果想要既匹配小写字母也匹配大写字母, 那么可以把正则表达式写成这样: “[A-Za-z]”. 当然, 如果需要包括全部十个数字, 也可以编写像[0-9]这样由数字组成的字符类。

此外, 通过在字符类前面放置一个脱字符号(^)的方法人们还可以创建字符类的否定含义. 例如, 如果有字符类[aeiou]来表示元音类, 那么就可以编写[^aeiou]来表示辅音或非元音。

[A-Za-z0-9]形成了正则表达式用法中所谓的单词正则表达式, 你还可以用字符类: “\w"来表示同样的匹配规则. 而”\w"的否定含义, 即表示匹配所有非单词字符(比如标点符号)的字符类是"\W"。

此外, 还可以把数字字符类([0-9])写成\d(注意由于在C#中反斜杆后跟着其他字符很可能是表示转义字符, 所以如果你想表达的就是正则表达式的某种字符类, 应该写两根反斜杠, 比如\d在C#中定义时应该协作\d). 非数字字符类([^0-9])则可以写成\D 这样. 最后一点, 因为空格符在文本处理中扮演着非常重要的角色, 所以把\s 专门用来表示空格字符, 而把\S 用来表示非空格字符. 稍后在讨论分组构造时将会研究使用空白字符类。

5、用断言修改正则表达式

C#包含一系列可以添加给正则表达式的运算符. 这些运算符可以在不导致正则表达式引擎遍历字符串的情况下改变表达式的行为. 这些运算符被称为断言(assertion)。

第一个要研究的断言会导致正则表达式只能在字符串或行的开始处找到匹配. 这个断言由字符(^)产生. 在下面这段程序中, 正则表达式只与第一个字符为字母"h"的字符串相匹配, 而忽略掉字符串中其他位置上的"h". 代码如下所示 :

static void Main()
{
   
    string[] words = new string[] {
    "heal", "heel", "noah", "techno" };
    string Regexp = "^h";
    Match aMatch;
    foreach (string word in words)
        if (Regex.IsMatch(word, Regexp)) {
   
            aMatch = Regex.Match(word, Regexp);
            Console.WriteLine("Matched : " + word + " at position: " + aMatch.Index);
        }
    Console.ReadLine();
}

这段代码的输出就只有字符串"heal"和"heel"匹配。

还有一个断言会导致正则表达式只在行的末尾找到匹配. 这个断言就是美元符号($)。

如果把前一个正则表达式修改成如下形式 : string Regexp = “h$”; 那么"noah"就是唯一能找到的匹配。

此外, 另有一个断言可以在正则表达式中指定所有匹配只能发生在单词的边缘. 也就是说匹配只能发生在用空格分隔的单词的开始或结束处. 此断言用\b表示. 下面是此断言的工作过程 :

string words = "hark, what doth thou say, Harold? "; 
//表示特殊正则表达式的\b要写两根斜杠, 也就是告诉C#你要输入的是斜杠本身, 而不是要输入转义字符
string Regexp = "\\bh"; 

这个正则表达式与字符串中的单词"hark"和"Harold"相匹配。

在正则表达式中还可以使用其他一些断言, 但是上述三种是最普遍用到的断言。
使用分组结构
Regex 类有一套分组结构可以用来把成功的匹配进行分组, 从而更容易的使字符解析成相关的匹配. 例如, 给定了生日和年龄的字符串, 而用户只想确定日期的话. 通过把日期分组到一起,就可以确定它们作为一组, 而不再需要单独进行匹配了.

6、匿名组

这里可能用到几个不同的分组构造. 通过括号内围绕的正则表达式就可以组成一个分组。

正如不久要介绍的一样, 既然也可以命名组, 大家就可以考虑把这个构造作为匿名组. 作为一个实例, 请看看下列字符串:

“08/14/57 46 02/2/59 45 06/05/85 18 03/12/88 16 09/09/90 13”

这个字符串就是由生日和年龄组成的. 如果只需要匹配年龄而不要生日, 就可以把正则表达式作为一个匿名组来书写 : (\s\d{2}\s)

通过编写这种方式的正则表达式, 代表的匹配规则是, 寻找首位均是空格, 并且中间是两位数字的子串 :

static void Main()
{
   
    string words = "08/14/57 46 02/25/59 45 06/05/85 18" + "03/12/88 16 09/09/90 13";
    string Regexp1 = "(\\s\\d{2}\\s)";
    MatchCollection matchSet = Regex.Matches(words, Regexp1);
    foreach (Match aMatch in matchSet)
        Console.WriteLine(aMatch.Groups[0].Captures[0]);
    Console.ReadLine();
}

程序运行结果:

7、命名组

正则表达式组可以命名, 命名的组更容易使用, 这是因为可以通过引用组名来获得匹配结果. 组的名称由作为正则表达式前缀的问号和一对尖括号包裹的名字组成的. 例如, 为了在 先前的程序中将匿名组命名为"ages", 可以把正则表达式写成下列形式: (?\s\d{2}\s)

还可以用一对单引号来代替尖括号包裹名字。

现在要来修改一下这个程序, 使得此程序寻找日期而不是年龄, 而且用分组构造来组织日期。

下面是代码:

static void Main() 
{
    
    string words = "08/14/57 46 02/25/59 45 06/05/85 18 " + "03/12/88 16 09/09/90 13"; 
        string Regexp1 = "(?<dates>(\\d{2}/\\d{2}/\\d{2}))\\s"; 
    MatchCollection matchSet = Regex.Matches(words,Regexp1); 
    foreach (Match aMatch in matchSet) 
        Console.WriteLine("Date: {0}", aMatch.Groups["dates"]); 
} 

程序运行 结果:

让我们聚焦上述正则表达式中决定了匹配规则的部分 : (\d{2}/\d{2}/\d{2})\s)

它的含义就是, 寻找由2位数字开始,并紧随斜杠, 接着又是两个位数, 接着又是斜杠, 然后还是两位数字, 最后是个空格的子串。

并且我们还为该正则表达式设置了分组名称dates, 然后就可以通过Match类的Groups方法来获取指定的正则表达式分组所匹配到的内容 :

                  Console.WriteLine("Date: {0}", aMatch.Groups("dates")); 

8、零宽正向和反向断言

断言还可以用来确定正则表达式向前或向后额外的匹配规则. 这些断言可能是正的或负的, 也就是说可以表示指定字符也可以表示非指定字符. 观察一些例子会更容易理解其规则。

这些断言中第一个要讨论的就是正向断言(lookahead assertion). 此断言书写如下 :

                            (?= reg-exp-char)

这里的reg-exp-char是正则表达式或元字符. 此断言说明只要搜索到匹配的当前子表达式在指定位置的右侧, 那么匹配就继续. 下面这段代码说明了此断言的工作原理 :

string words = "lions lion tigers tiger bears,bear"; 
string Regexp1 = "\\w+(?=\\s)"; 

正则表达式对跟随空格的每个单词都做了匹配. 匹配的单词有"lions", “lion”, “tigers"和"tiger”. 正则表达式匹配单词, 但是不匹配空格. 记住这一点是非常重要的. (也就是说, 在寻找字符串时, 考虑了(?=regexp)中的正则要求, 但是最终不会吧(?=regexp)断言对应的字符串作为匹配到的字符串结果, 前提是正向断言在正则表达式最右侧, 否则正向断言所匹配的字符串一样会包含在结果中)

下一个断言是负的正向断言. 只要搜索到不匹配的当前子表达式在指定位置的右侧,那么此断言就继续匹配. 下面是代码段实例 :

string words = "subroutine routine subprocedure procedure"; 
string Regexp1 = "\\b(?!sub)\\w+\\b"; 

此正则表达式表明对每个单词所做的匹配不是以前缀"sub"开始的. 匹配的单词有"routine"和"procedure"

接下来的断言被称为是反向预搜索断言. 这些断言会向左搜索, 而不是向右. 下面的例子说明了如何编写一个正反向断言 :

static void Main()
{
   
    string words = "是不是真的 我看是真谛 什么是真滴 是什么就什么 是是非非由它去";
    string Regexp1 = "(?'group'((?<=\\w+)是\\w+))";
    MatchCollection matchSet = Regex.Matches(words, Regexp1);
    foreach (Match aMatch in matchSet)
        Console.WriteLine(aMatch.Groups["group"]);
    Console.ReadLine();
}

程序运行结果:

这个正则表达式的匹配规则是 : 出现在任意个非标点字符前的’是’字前面的任意个非标点字符. (与正向断言类似, 在正则表达式边缘的反向断言所匹配到的字符串不会作为匹配结果的一部分, 但是前提是处于正则表达式的最左侧)

现在我们再示范一个负反向断言, 它将要求一个处于任意非标点字符之间的’是’字之前不能是’不’字。

static void Main()
{
   
    string words = "是不是真的 我看是真谛 什么是真滴 是什么就什么 是是非非由它去";
    string Regexp1 = "(?'group'(\\w+(?<!不)是\\w+))";
    MatchCollection matchSet = Regex.Matches(words, Regexp1);
    foreach (Match aMatch in matchSet)
        Console.WriteLine("Date: {0}", aMatch.Groups["group"]);
    Console.ReadLine();
}

程序运行结果:(关于正向和反向断言, 我给大家推荐看看这篇文章, 写的很细了。大家有时间可以看看。)

9、CaptureCollection类

当正则表达式匹配子表达式的时候, 产生了一个被称为是Capture的对象, 而且会把此对 象添加到名为CaptureCollection的集合里面. 当在正则表达式中使用命名组的时候, 这个组就拥有自己的捕获集合.为了得到命名组正则表达式的捕获集合, 就要调用来自Match 对象Group属 性的Captures 属性. 结合例子会很容易理解. 利用前面小节的其中一个正则表达式,下列代码返回了在字符串中找到的所有日期和年龄, 而且日期和年龄是完全分组的:

static void Main()
{
   
    string dates = "08/14/57 46 02/25/59 45 06/05/85 18 " + "03/12/88 16 09/09/90  13";
    //这一段正则表达式中包含了dates和ages两个正则组
    string Regexp = "(?<dates>(\\d{2}/\\d{2}/\\d{2}))\\s(?<ages>(\\d{2}))\\s";
    MatchCollection matchSet;
    //通过Regex.Matches方法, 对dates字符串应用Regexp代表的正则表达式
    matchSet = Regex.Matches(dates, Regexp);
    Console.WriteLine();
    foreach (Match aMatch in matchSet) {
   
        //分别遍历两个正则组所代表的的捕获集合, 输出不同正则组的匹配结果字符串
        foreach (Capture aCapture in aMatch.Groups["dates"].Captures)
            Console.WriteLine("日期捕获: " + aCapture.ToString());
        foreach (Capture aCapture in aMatch.Groups["ages"].Captures)
            Console.WriteLine("年龄捕获: " + aCapture.ToString());
    }
    Console.ReadLine();
}

程序运行结果:

程序的外循坏遍历了每个匹配, 而两个内循环则遍历了不同的Capture集合, 一个是代表日期内容的dates组集合而另一个则是代表年龄内容的ages组集合。

10、正则表达式选项

在指定正则表达式的时候可以设置几个选项. 这些选项的范围从指定多行模式以便正则表达式可以在多行上正确工作, 到编译正则表达式以便能更快速执行. 下面这张表列出了可以设置的不同选项。

在查看此表之前, 需要注意这些选项的设置方式. 通常情况下, 对Regex 类的方法增加代表正则设置的第三个参数就可以, 比如Match方法, Matches方法. 例如,如果想要为正则表达式设置Multiline 选项, 代码行应像下面这样:

//第三个参数, 表示正则表达式要匹配多行内容
matchSet = Regex.Matches(dates, Regexp, RegexOptions.Multiline); 

下方我放的就是正则表达式的可选设置列表 ; (如果有不懂的我推荐下大家看下这个讲解

关注苏州程序大白,持续更新技术分享。谢谢大家支持


转载:https://blog.csdn.net/weixin_46931877/article/details/116830298
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场