C#正则表达式

正则表达式(Regular Expression)是处理文本信息的强大工具,其应用核心是根据模式(pattern)匹配文本内容,然后,可以获取匹配的内容,并进行替换、删除、分割等操作。本文将介绍C#与.NET Framework中的正则表达式应用,并介绍常用格式的判断和处理,如手机号码、18位身份证号码等。

正则表达式应用基础

正则表达式的应用基础是模式的定义,在C#中,模式使用字符串定义,由于模式中会使用转义,一般应使用逐字字符串定义模式。定义模式时会使用有着特殊含义的元字符,就像编程语言的关键字一样。下面是定义正则表达式模式时使用的元字符。

.\+*?[]^$(
){}=!<>|:-

在模式中需要匹配这些元字符时,需要使用\字符转义,如\\表示\字符、\.表示圆点等。此外,对于一些特殊的字符,也需要使用\字符转义,如:

转义字符说明
\a报警符
\t水平制表符
\n换行符
\v垂直制表符
\f进纸符
\r回车符
\NNNNNN为字符的三位八进制编码
\uNNNNNNNN为Unicode字符的4位十六进制编码
\xNNNN为ASC II字符的十六进制编码。
\字符显示指定的字符

模式的定义

首先了解模式的定义,包括匹配内容、匹配次数和匹配位置等。

模式中定义匹配的内容时,主要包括以下一些方法:

  • 圆点(.),匹配换行符(\n)以外的任意一个字符。
  • 匹配正则表达式元字符和特殊字符需要使用\符号转义,匹配其它字符直接书写即可。
  • [<字符集>],匹配<字符集>中的任意一个字符。<字符集>中可以是字符的列表,如"yn";也可以使用连接号指定范围,如a-z、0-9、A-Z等。比如,匹配一位十六进制数可以使用[a-f0-9]。
  • [^<字符集>],匹配<字符集>以外的任意一个字符。如[^abc]会匹配一个abc之外的字符。
  • \d,任意一个数字(0到9)字符。
  • \D,任意一个非数字字符。
  • \s,任意一个空白字符,包括空格、换页符(\f)、换行符(\n)、水平制表符(\t)、垂直制表符(\v)、回车符(\r)。
  • \S,任意一个非空白字符。
  • \w,任意一个单词字符,包括数字、大小写字母和下画线。
  • \W,任意一个非单词字符。
  • \p{<block>},<block>指定Unicode字符集中的块名称,可以匹配此块的字符。如"\p{IsCJKUnifiedIdeographs}"可以匹配汉字。
  • \P{<block>},匹配<block>块之外的Unicode字符。
  • 模式中,可以使用一对圆括号定义模式组。
  • 一个位置可以匹配多个模式时可以使用|符号分隔。

除了匹配内容,还可以指定匹配的次数,如:

  • *,匹配0次或多次。*?为非贪婪模式(匹配尽可能少的内容)。
  • +,匹配1次或多次。+?为非贪婪模式。
  • ?,匹配0次或1次。??为非贪婪模式。
  • {n},匹配n次。{n}?为非贪婪模式。
  • {n,},匹配最少n次。{n,}?为非贪婪模式。
  • {m,n},匹配m到n次。{m,n}?为非贪婪模式。

常用的位置匹配方法如下:

  • ^符号定义在模式第一个字符时,表示其后面的模式匹配字符串的开始部分。
  • $符号定义在模式最后一个字符时,表示其前面的模式匹配字符串的结束部分。
  • \b,表示单词的边界。
  • \B,表示非边界,与\b含义相反。

Regex类

Regex类定义在System.Text.RegularExpressions命名空间,首先来看IsMatch()静态方法的应用,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abc";
            string p = @"[yn]";
            Console.WriteLine(Regex.IsMatch(s, p));  // False
        }
    }
}

代码中,Regex.IsMatch()方法的第一个参数为字符串;第二个参数定义模式,本例定义为y或n字符,在s字符串中并不包含,方法返回false。可以在s字符串中添加y或n字符来观察执行结果。

默认情况下,模式匹配是区分字母大小写的,即代码中的[yn]并不会匹配Y和N字符,如果需要忽略字母大小写,可以使用Regex.IsMatch()方法的第三个参数,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abcXYZ";
            string p = @"[yn]";
            Console.WriteLine(Regex.IsMatch(s, p, RegexOptions.IgnoreCase));  // True
        }
    }
}

代码中,使用RegexOptions.IgnoreCase选项,[yn]可以匹配y、n、Y和N。Regex.IsMatch()方法还可以使用第四个参数指定匹配执行的超时时间,类型为TimeSpan。

RegexOptions枚举中包含的选项如下表。

枚举值数值说明
None0使用默认值。 规则为字母区分大小写, 空白符匹配字面量,捕获组可以隐式命名也可以显式命名。
IgnoreCase1忽略字母大小写。
Multiline2多行模式。^和$符号会匹配行首或行尾内容。
ExplicitCapture4显式捕获命名组。
Compiled8正则表达式编译为MSIL代码。编译后的代码可以提高运行效率,但会损失一些初始化时间。
Singleline16单行模式,圆点(.)匹配换行符(\n)之外的所有字符。
IgnorePatternWhitespace32忽略空白字符。
RightToLeft64从右到左匹配。适用于从右到左阅读的语言。
ECMAScript256兼容ECMAScript标准,只能与IgnoreCase、Multiline和Compiled值一起使用。
CultureInvariant512忽略语言中的文化差异。

同时使用多个选项时可以使用|运算符,具体的操作原理已在整数运算中讨论过。

获取匹配内容

Regex类可以使用模式作为构造函数的参数创建对象,然后使用Match()方法可以返回匹配的第一个文本内容,常用版本为,参数一指定字符串,参数二指定RegexOptions参数,不指定时使用默认操作。下面的代码演示了相关应用。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Regex re = new Regex(@"a+");
            Match m = re.Match("aaa");
            Console.WriteLine(m.Value);  // aaa
        }
    }
}

代码中使用了模式"a+",表示1个或多个a,默认使用贪婪模式,即匹配尽可能多的内容,执行代码会显示aaa。

获取全部匹配内容时使用Matches()方法,参数与Matches()方法相似,如下面的代码演示了非贪婪模式的应用。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Regex re = new Regex(@"a+?");
            foreach(Match m in re.Matches("aaa"))
                Console.WriteLine(m.Value);
        }
    }
}

本例使用非贪婪模式,当匹配到一个a时即停止本次匹配,并进行下一次匹配,匹配结果分别是三个a。

此外,Regex类的Match()和Matches()方法都有静态版本,常用版本的参数设置为:第一个参数设置字符串,第二个参数设置模式,第三个参数设置RegexOptions选项(可选参数)。

替换与删除匹配内容

Regex类的Replace()方法可通过模式匹配替换内容,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abcaaaxy";
            Console.WriteLine(Regex.Replace(s, @"a+", "*"));
        }
    }
}

代码中,会将s字符串中的连续的1个或多个a替换为一个*符号,执行结果会显示"*bc*xy"。

Replace()方法中,将替换内容设置为空字符串即可完成匹配内容的删除操作,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Regex re = new Regex(@"a+");
            string s = "abcaaabc";
            Console.WriteLine(re.Replace(s, ""));
        }
    }
}

执行代码会显示"bcbc"。

使用模式分割字符串

Regex类的Split()方法可以将字符串按模式分割为字符串数组,如下面的代码。

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

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "abcaaAxy";
            string[] arr = Regex.Split(s, @"a+", RegexOptions.IgnoreCase);
            foreach(string e in arr)
            {
                Console.WriteLine(e);
            }
        }
    }
}

代码中,通过一个或多个a分割字符串,并忽略字母大小写。分割结果为三个元素,第一个元素为第一个a前的空字符串,第二个元素为bc,第三个元素为xy,执行代码会分行显示这三个元素。

开发中,如果在分割结果中不包含空字符串元素,可以对操作进行封装,如下面的代码(cfx/Str.cs)。

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

namespace cfx
{
    public static class Str
    {
        // 其它代码
        // 符合指定的正则表达式模式
        public static bool ReMatch(this string s, string pattern, bool ignoreCase = false)
        {
            if (ignoreCase)
                return Regex.IsMatch(s, pattern, RegexOptions.IgnoreCase);
            else
                return Regex.IsMatch(s, pattern);
        }
        //
        public static List<string> ReSplit(this string s, string pattern, bool ignoreCase = false)
        {
            string[] arr = ignoreCase ?
                Regex.Split(s, pattern, RegexOptions.IgnoreCase) :
                Regex.Split(s, pattern);
            List<string> lst = new List<string>();
            foreach (string e in arr)
            {
                string a = e.Trim();
                if (a.Length > 0)
                    lst.Add(a);
            }
            return lst;
        }
    }
}

代码中,在Str类中定义了string类型的两个扩展方法,包括:

  • ReMatch()方法,判断字符串是否匹配指定的正则表达式模式,参数分别是字符串、模式、是否忽略大小写。
  • ReSplit()方法,根据正则表达式模式分割字符串,参数同样是字符串、模式、是否忽略大小写。请注意,此方法分割后的列表元素不包含纯空白字符和空字符串,并且会删除各个元素开始和结束位置的空白字符。

下面的代码,在Program.cs文件中测试这两个扩展方法的应用。

C#
using System;
using System.Collections.Generic;
using cfx;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string p = @"a+";
            string s = "abcaaAxy";
            //
            Console.WriteLine(s.ReMatch(p)); // True
            //
            List<string> lst = s.ReSplit(p,true);
            foreach (string e in lst)
                Console.WriteLine(e);
        }
    }
}

第一个输出,s字符串中匹配字母a,显示为True。接下来,对字符串使用一个或多个a分割,分割后列表元素为bc和xy。

手机号验证

下面的代码(cfx/Str.cs),会在Str类中添加IsMobilePhone()方法,判断字符串是否为11位手机号码。

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

namespace cfx
{
    public static class Str
    {
        // 其它代码
        // 判断是否为11位手机号
        public static bool IsMobilePhone(this string s)
        {
            return ReMatch(s, @"^1\d{10}$");
        }
    }
}

本例,定义的模式以数字1开始(^1),以10位数字结束(\d{10}$)。下面的代码,在Program.cs文件测试此方法的应用,可以修改s字符串的内容观察执行结果。

C#
using System;
using cfx;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "12345678910";
            //
            Console.WriteLine(s.IsMobilePhone()); // True
        }
    }
}

18位身份证号验证

18位身份证号码中,前六位是省、市、县三级编码各两位,然后是出生年(4位)、月(2位)、日(2位),接下来是三位序号(奇数为男,偶数为女),最后一位是通过前17位计算出的校验码(0到9的数字或大写字母X)。下面的代码(cfx/Str.cs),在Str类中封装IsIdCard()方法,其功能是判断字符串中的内容是否满足18位身份证号码的基本规则。

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

namespace cfx
{
    public static class Str
    {
        // 其它代码
        }
        // 是否满足18位身份证号码的基本规则
        public static bool IsIdCard(this string s)
        {
            // 判断基本模式
            string p = @"^[1-9][0-9]{5}[12][0-9]{3}[01][0-9][0-3][0-9]{4}[0-9X]$";
            if (ReMatch(s, p) == false) return false;
            // 计算校验码
            // 前17位对应的系数
            int[] factor = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
            // 
            int sum = 0;
            for(int i = 0; i < factor.Length; i++)
            {
                sum = sum + (ToInt(s.Substring(i, 1)) * factor[i]);
            }
            // 求除以11的余数
            int rem = sum % 11;
            string chkCode = "10X98765432";
            return s.Substring(17) == chkCode.Substring(rem, 1);
        }
    }
}

代码中,首先使用"^[1-9][0-9]{5}[12][0-9]{3}[01][0-9][0-3][0-9]{4}[0-9X]$"模式判断基本规则,具体含义如下:

  • "^[1-9][0-9]{5}",前六位,第一位是1到9开始,然后是5位数字。
  • "[12][0-9]{3}",四位年份,第一位是1或2,然后是3位数字。
  • "[01][0-9]",2位月份,第一位是0或1,然后是0到9。
  • "[0-3][0-9]{4}",包括5位数字,分别是2位日数据和3位序号,日子第一位为0到3,第二位是0到9,三位序号也是0到9的数字。
  • "[0-9X]$",第18位校验码,应该是0到9,或者是大写字母X。

然后,会通过前17位数字计算校验码,分别使用前17位各位数字乘以对应的系数,并求和。接下来,使用求得的和除以11,取余数,结果应在0到10之间;chkCode字符串中包含了各个余数值对应的正确的校验码。最后,读取第18个字符(索引17)与正确的校验码对比,相同时返回true,否则返回false。

下面的代码,在Program.cs文件中测试Str.IsIdCard()方法的应用,可以修改s字符串的内容观察执行结果。

C#
using System;
using cfx;

namespace csfx_demo
{
    class Program
    {
        static void Main(string[] args)
        {
            string s = "请输入身份证号码";
            //
            Console.WriteLine(s.IsIdCard()); 
        }
    }
}