C#开发训练营

第12课:正则表达式(1)

正则表达式(Regular Expressions)的功能,简单地理解就是通过模式(Pattern)来匹配(Match)文本内容,并对所匹配的内容进一步操作,如格式判断、替换、分割、删除等。

C#中的正则表达式

在.NET Framework类库中的System.Text.RegularExpressions命名空间中定义了一系列正则表达式操作资源,如Regex、Match、MatchCollection等类型。

进行简单测试时,可以使用Regex.IsMatch()方法,它需要两个参数,其中,参数一指定文本内容,参数二同样使用字符串类型指定匹配模式;当参数一的文本符合参数二的模式时,方法返回true值,否则返回false值。

下面的代码演示了Regex.IsMatch()方法的应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "abcdefg";
        bool result = Regex.IsMatch(s, pattern);
        tWeb.WriteLine(result);
    }
}

代码中,首先定义了一个简单的模式,只匹配abc;在字符串s中包含了abc,所以,Regex.IsMatch()方法会返回true。本例最终显示为True。

C#代码中定义模式字符串时,为更有效的定义模式内容,特别是需要通过\字符转义的内容,应使用逐字字符串,即使用@""定义模式字符串。

默认情况下,文本的匹配是区分字母大小写的,如果需要忽略字母大小写,可以在Regex.IsMatch()方法中使用第三个参数指定,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "ABCDEFG";
        bool result = Regex.IsMatch(s, pattern,RegexOptions.IgnoreCase);
        tWeb.WriteLine(result);
    }
}

本例,如果Regex.IsMatch()方法没有使用第三个参数会显示False;这里使用RegexOptions.IgnoreCase枚举值指定忽略大小写,运行结果为True。

RegexOptions枚举的常用成员包括:

  • IgnoreCase,不区分大小写。
  • Multiline,多行模式。改变^和$的含义,分别指定在任意一行的行首和行尾匹配。在单行模式下,分别匹配文本的开始和结尾。
  • Singleline,指定单行模式。更改圆点(.)的含义,它会匹配任何字符(包含\n换行符)。在多行模式下,圆点(.)匹配\n之外的所有字符。
  • RightToLeft,从右向左搜索。

需要获取第一个匹配内容时,可以使用Regex对象中的Match()方法,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "abcdefg";
        Match m = Regex.Match(s,pattern);
        tWeb.WriteLine(m.Value);
    }
}

本例,首先定义了模式pattern和文本内容s,然后调用Regex.Match()方法返回匹配的第一个结果,返回类型为Match类,使用其中的Value属性可以获取匹配的内容。代码执行结果会显示abc,即字符串s中的前三个字符。

如果文本中可能有多个匹配结果,可以使用Regex对象的Matches()方法返回全部匹配结果,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "abcdefgabcdefgabc";
        MatchCollection mc = Regex.Matches(s,pattern);
        foreach (Match m in mc)
        {
            tWeb.WriteLine(m.Value);
        }
    }
}

本例,字符串s中包含了三个abc,可以使用Regex.Matches()方法返回所有匹配的内容,返回结果为MatchCollection对象;代码最后使用foreach语句遍历所有结果,并通过Match对象的Value属性显示匹配内容。代码执行会显示三个abc。

Regex类中的Match()和Matches()方法还有一些重载版本,比如,可以指定开始匹配的字符索引位置,不指定时从第一个字符开始(索引0);还可以指定搜索的字符数,不指定时搜索从指定位置开始的所有内容。

Regex类中的Split()方法通过模式分隔文本内容,下面的代码演示了此方法的基本应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @",|\s";
        string s = "abc,def ghi\tjkl";
        string[] result = Regex.Split(s, pattern);
        foreach(string r in result)
            tWeb.WriteLine(r);
    }
}

本例中,会将字符串s通过逗号(,)或空白字符(空格、制表符等)进行分割;代码运行结果如图1。

图1

Regex类中的Replace()方法会将文本中的匹配模式的内容替换为指定的内容,如下面的代码演示了此方法的基本应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "abcdefgabc";
        string result = Regex.Replace(s, pattern,"***");
        tWeb.WriteLine(result);
    }
}

代码的功能是将字符串s中的abc替换为***,结果会显示***defg***。

此外,还可以在Regex.Replace()方法中使用委托指定替换规则,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"abc";
        string s = "abcdefgabc";
        string result = Regex.Replace(s, pattern,(Match m)=> {
            return "*" + m.Value + "*";
        });
        tWeb.WriteLine(result);
    }
}

代码的功能是将字符串s中的abc前后各加一个星号(*),执行结果如图2。

图2

Regex.Replace()方法中使用委托类型定义如下。

C#
[SerializableAttribute]
public delegate string MatchEvaluator(Match match)

其中,参数match会分别带入匹配结果,委托方法或Lambda表达式中需要返回匹配结果的操作结果,返回类型为string。如前面示例中就是在匹配内容和前面和后面各添加一个星号(*)。

本节主要讨论了C#代码中进行正则表达式操作的常用资源和基本应用方法,接下来会讨论更多关于模式的定义。

匹配字符

定义模式时,有一些字符会有特定的功能或含义,如图3。

图3

需要匹配文本中的单个字符应注意,对于特殊的字符,需要使用\字符进行转义,在C#中需要转义的字符包括 \ * + ? | { [ ( ) ^ $ . # 和空格。如果模式中包含了这些字符,可以先直接定义在字符串中,然后调用Regex.Escape()方法进行转义,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"*.*";
        tWeb.WriteLine(Regex.Escape(pattern));
    }
}

模式中定义的内容为*.*,通过Regex.Escape()方法转义后转换为\*\.\*,这也是本例的显示结果。

Regex.Escape()方法的反向操作可以使用Regex.Unescape()方法,如下面的代码会显示*.*。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"\*\.\*";
        tWeb.WriteLine(Regex.Unescape(pattern));
    }
}

对于一些特殊的字符可以使用如下转义:

  • \a,匹配响铃,字符编码\u0007。
  • \b,两种应用场景,[\b ]匹配退格,字符编码\u0008。中括号之外,\b是匹配字边界。
  • \t,匹配制表符,字符编码\u0009。
  • \r,匹配回车,字符编码\u000D。
  • \v,匹配垂直制表符,字符编码\u000B。
  • \f,匹配换页符,字符编码\u000C。
  • \n,匹配换行符,字符编码\u000A。
  • \e,匹配转义ESC(Escape)符,字符编码\u001B。
  • \nnn,匹配ASCII字符,nnn 包含表示八进制字符代码的两位数或三位数。
  • \xnn,匹配ASCII字符,nn表示两位十六进制字符代码。
  • \cX,匹配ASCII控制字符,其中X是控制字符的字母,如\cC表示Ctrl-C。
  • \unnnn ,匹配的UTF-16代码单元,单元值是nnnn十六进制。

使用\后接无法识别的转义字符时,将匹配此字符,如\-和直接使用-都表示匹配-符号。

对特殊字符转义需要特别注意,在不使用逐字字符串时,C#中的字符串本身就包含了转义,如"\."表示的是圆点,如果需要在模式中转义圆点就应该写成"\\.",所以建议在定义模式时总是使用逐字字符串,如@"\."。如下面的代码用于匹配*.xlsx文件名。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @".\.xlsx$";
        string filename = "demo1.xlsx";
        tWeb.WriteLine(Regex.IsMatch(filename,pattern));
    }
}

本例中定义的模式为.\.xlsx,其中,第一个圆点表示一个或多个字符,\.表示一个圆点,而\.xlsx则表示.xlsx扩展名,最后的$符号表示匹配的内容应该在文本的末尾,代码执行结果会显示True。这里,可以修改filename的内容来观察运行结果。

为避免转义带来的问题,也可以使用普通字符串定义模式,然后使用Regex.Escape()方法对需要转义的字符进行转换。

对于需要匹配的普通文本内容,可以直接使指定,匹配多个字符中的一个时,可以将这些字符定义在一对方括号中;匹配连续的数字或字母时,还可以在中括号中使用连接符(-)指定,如下面的代码。

C#
tWeb.WriteLine(Regex.IsMatch("aefg",@"[abcd]")); // True
tWeb.WriteLine(Regex.IsMatch("xyz", @"[abcd]")); // False
tWeb.WriteLine(Regex.IsMatch("aefg", @"[a-d]")); // True
tWeb.WriteLine(Regex.IsMatch("xyz", @"[a-d]")); // False

本例,前两个输出匹配a、b、c、d四个字母中的一个,后两个输出使用连接符,同样是匹配a到d的四个字母中的一个。

指定多组匹配规则时,使用竖线(|)分隔,如下面的代码。

C#
string p = @"[a-d|1-4]";
tWeb.WriteLine(Regex.IsMatch("aefg",p)); // True
tWeb.WriteLine(Regex.IsMatch("xyz", p)); // False
tWeb.WriteLine(Regex.IsMatch("1560", p)); // True

本例定义的模式为a到d的四个字母,或者是1到4的数字其中的一个字符。

在方括号定义的集合中,还可以使用^符号定义反向匹配,如判断文本中不应包含a和b时,可以使用如下代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"[^ab]";
        tWeb.WriteLine(Regex.IsMatch("a", pattern));  // False
        tWeb.WriteLine(Regex.IsMatch("x", pattern));  // True
    }
}

模式中,定义在一对中括号的集合最终会匹配一个字符,而定义在一对圆括号中的内容(子表达式)还可以定义更多的形式,如下面的代码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"(abc[0-9])(xyz[0-9])";
        tWeb.WriteLine(Regex.IsMatch("abc1xyz1", pattern));  // True
        tWeb.WriteLine(Regex.IsMatch("abcdxyz1", pattern));  // False
        tWeb.WriteLine(Regex.IsMatch("abc1xyz", pattern));  // False
    }
}

代码中定义的模式包含两个部分,前一部分由abc加一位数字组成,后一部分由xyz加一位数字组成。

匹配特定类型字符

匹配文本中某一类型的字符时,可以使用转义符或Unicode块。C#中可以使用如下限定符:

  • \d,数字0到9,相当于[0-9]。因为完整的数字还可能包含正号(+)、负号(-)、小数点(.)等字符,所以在开发过程中,判断文本内容是否为有效的数字时,应使用第7课讨论的相关方法。
  • \D,非数字字符,即0到9以外的字符。
  • \s,空白字符,如空格、制表符、回车符、换行符等。
  • \S,非空白字符,匹配与\s限定符相反的内容。
  • \w,字母、数字和下画线字符。ECMAScript标准中相当于[a-zA-Z0-9_]。
  • \W,字母、数字和下画线以外的字符。

下面的代码演示了\s限定符的应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"\s";
        tWeb.WriteLine(Regex.IsMatch("   ",pattern));
        tWeb.WriteLine(Regex.IsMatch("\t", pattern));
    }
}

执行代码会在页面中显示两个True,这里,\s限定符匹配了空格和水平制表符(\t)。

对于Unicode字符,可以按Unicode块进行匹配,格式为\p{<块名>}。Unicode字符集中,汉字的范围是4E00到9FFF,Unicode块名为IsCJKUnifiedIdeographs,代码中,判断字符是否为汉字可以使用如下模式。

C#
string pattern = @"\p{IsCJKUnifiedIdeographs}";

下面的代码,通过tStr.IsChinese()方法判断字符或字符串是否中文字符。

C#
// 是否为中文字符
public static bool IsChinese(this char ch)
{
    string pattern = @"\p{IsCJKUnifiedIdeographs}";
    return Regex.IsMatch(ch.ToString(), pattern);
}

// 字符串是否为中文
public static bool IsChinese(this string s)
{
    char[] chars = s.ToCharArray();
    for(int i = 0; i < chars.Length; i++)
    {
        if (chars[i].IsChinese() == false) return false;
    }
    return true;
}

代码中,tStr.IsChinese()定义为char和string类型的扩展方法,下面的代码演示了此方法的应用。

C#
using System;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        tWeb.WriteLine('A'.IsChinese());  // False
        tWeb.WriteLine('好'.IsChinese()); // True
        tWeb.WriteLine("Hello好".IsChinese());  // False
        tWeb.WriteLine("您好".IsChinese()); // True
    }
}

匹配数量

匹配文本内容时,还可以判断模式的匹配次数,相关的限定符如表。

贪婪限定符惰性限定符说明
**?匹配零次或多次。
++?匹配一次或多次。
???匹配零次或一次。
{n}{n}?匹配n次。
{n,}{n,}?匹配n次或更多次。
{n,m}{n,m}?匹配n到m次。

表中,数量n和m是正整数。使用贪婪限定符时,会尽可能多的特定模式实例。贪婪限定符后添加问号(?)时,匹配变为惰性的,会匹配尽可能少的实例。

下面的代码演示了+限定符的应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"a+";
        string s = "aaa";
        foreach(Match m in Regex.Matches(s,pattern))
        {
            tWeb.WriteLine(m.Value);
        }
    }
}

代码中,限定符+属于贪婪限定符,本例中会尽可能多地匹配字母a,这样,匹配结果就是aaa,显示结果如图4(a);如果将代码中的模式修改为@"a+?"就会使用惰性匹配,此时会分别匹配三个字母a,执行结果如图4(b)。

图4

位置和边界

前面已经讨论了如保匹配指定内容和数量,本节将讨论模式中如何定义匹配内容的位置及边界,相应的限定符包括:

  • ^,在方括号定义的集合中定义为反向匹配,在方括号外使用时,默认情况下(单行模式)匹配文本开始的内容;在多行模式中匹配在每行开始的内容。
  • $,默认情况下(单行模式)匹配文本结束位置的内容,或者是文本末尾的换行符(\n)之前;多行模式中必须出现在行的末尾,或在行末尾的 换行符(\n)之前。
  • \A,匹配必须出现在文本开始位置的内容,与单行模式下的^限定符功能相同。
  • \Z,匹配必须出现在文本末尾或末尾换行符(\n)之前的内容,与单行模式下的$限定符功能相同。在判断换行时,\Z与\n匹配,但与\r\n不匹配。
  • \z,匹配必须出现在文本末尾的内容,但不匹配换行符(\n)。
  • \G,从上一匹配结束位置开始匹配的内容。指定匹配必须出现在上一个匹配结束的位置。将此位置与Regex.Matches()或Match.NextMatch()方法配合使用时,它可确保所有匹配项是连续的。
  • \b,匹配字边界。
  • \B,匹配非字边界,与\b匹配规则相反。

下面的代码演示了如何匹配11位手机号码。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"^1\d{10}$";
        tWeb.WriteLine(Regex.IsMatch("12345678910", pattern));  // True
        tWeb.WriteLine(Regex.IsMatch("23456789101", pattern));  // False
        tWeb.WriteLine(Regex.IsMatch("1234567891", pattern));  // False
    }
}

模式中,^1表示要使用数字1开始,\d{10}$表示以10位数字结束。

限定符\b用于匹配\w和\W的边界;\B定义非边界,与\b的匹配规则相反。下面的代码演示了\b限定符的基本应用。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string pattern = @"\bgo\b";
        tWeb.WriteLine(Regex.IsMatch("go", pattern));  // True
        tWeb.WriteLine(Regex.IsMatch("gold", pattern));  // False
        tWeb.WriteLine(Regex.IsMatch("let's go", pattern));  // True
    }
}

代码的功能是检查文本中是否包含独立的单词go,而gold、good这样的单词是不会匹配的。

Web应用中,如果需要识别代码中的关键字时并使用特定的样式,使用\b限定符是很方便实现的,如下面的代码,会将关键字定义为span元素,并将元素的class属性设置为code_keyword。

C#
using System;
using System.Text.RegularExpressions;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string keyword = @"int|for";
        string pattern = @"\b" + keyword + @"\b";
        string source = @"int sum = 0;
for(int i=1;i<=100;i++) {
    sum+=i;
}
System.Console.WriteLine(sum);";
        //
        string result = Regex.Replace(source, pattern, (Match match) =>
        {
            return "<span class='code_keyword'>" + match.Value + "</span>";
        });

        tWeb.WriteLine("<pre>"+result+"</pre>");
    }
}

接下来,需要在/Test.aspx页面中定义code_keyword类的样式,如下面代码style元素中定义的内容。

HTML
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <style>
        .code_keyword{
            font-weight:bold;
            color:blue;
        }
    </style>
</head>

呈现的页面中,代码会显示在pre元素中,其中的int和for关键字会显示为蓝色并加粗,效果如图5。

图5