(9)基础强化:元字符,正则表达式,匹配,提取组,Regex,Match与Matches

    
    
一、作业


    1、问:下面解压程序出错,什么原因?

        string src = @"E:\1.txt";
        string des = @"E:\2.txt";
        using (FileStream read = File.OpenRead(src))
        {
            using (GZipStream gzip = new GZipStream(read, CompressionMode.Decompress))
            {
                using (FileStream write = File.OpenWrite(des))
                {
                    byte[] bytes = new byte[read.Length];//a
                    int bytesRead=gzip.Read(bytes, 0, bytes.Length);
                    write.Write(bytes, 0, bytesRead);//b
                }
            }
        }


        
        假定原文件(2.txt)大小为100,而压缩后的1.txt为20。现在将1.txt解压还原成2.txt.
        
        上面定义的缓冲为20,还原时的大小也成了20,那么b处写入也是20,不会是100,解压
        后的文件肯定不对,不应该是20而是100.
        
        
    2、问:压缩流可以压缩多个文件吗?
        
        答:Gzip压缩算法主要适用于单个文件的压缩。它使用GZipStream类来实现压缩和解压
        缩操作,但只能处理单个文件流。如果你想要同时压缩多个文件,你需要分别对每个文
        件进行压缩,然后将它们合并到一个压缩文件中。

        private static void Main(string[] args)
        {
            string scr1 = @"E:\1.txt", scr2 = @"E:\2.txt";
            CompressFile(@"E:\3.txt", scr1, scr2);

            Console.ReadKey();
        }

        public static void CompressFile(string outpath, params string[] scrpath)
        {
            using (FileStream fsw = File.Create(outpath))
            {
                using (GZipStream Ugzip = new GZipStream(fsw, CompressionMode.Compress))
                {
                    foreach (string scr in scrpath)
                    {
                        using (FileStream fsr = File.OpenRead(scr))
                        {
                            byte[] buffer = new byte[1024 * 8];
                            int bytesRead;
                            while ((bytesRead = fsr.Read(buffer, 0, buffer.Length)) > 0)
                            {
                                Ugzip.Write(buffer, 0, bytesRead);
                            }
                        }
                    }
                }
            }
        }


        但是,悲哀的,这个可以解压,但不支持解压成各自单独的文件。除非人为在buffer添
         加分隔符,解压时再分隔区别。
        
        要做到多个文件的压缩与解压,可以用ZipFile类和ZipArchive类
        均来自using System.IO.Compression;
        
        下面直接使用Zip类将一个目录进行压缩和解压。

        string scrFolder = @"E:\1";
        string zipFile = @"E:\z.zip";
        string desFolder = @"E:\2";
        
        ZipFile.CreateFromDirectory(scrFolder, zipFile);//目录压缩
        ZipFile.ExtractToDirectory(zipFile, desFolder);//释放到目录        
        
        下面使用ZipFile与ZipArchive压缩多个文件并解压
        string[] files = { @"E:\1\1.txt", @"E:\1\2.txt" };
        string outFile = @"E:\zz.zip";

        using (ZipArchive za = ZipFile.Open(outFile, ZipArchiveMode.Create))//a
        {
            foreach (string file in files)
            {
                za.CreateEntryFromFile(file, Path.GetFileName(file));//b
            }
        }
        ZipFile.ExtractToDirectory(outFile, @"E:\3");//解压


        
        上面使用ZipFile.Open方法创建一个新的ZIP文件,并指定ZipArchiveMode.Create模式
        以创建新的 ZIP 文件。(主要是返回一个压缩流)
        
        通过 foreach 循环遍历源文件路径数组,并使用 archive.CreateEntryFromFile 方法
        将每个文件添加到 ZIP 文件中,并使用原始文件名作为 ZIP 文件中的条目名。
        
        b处第一参数是要参与压缩的文件,第二参数是压缩后显示的文件名称(条目名)。
        
        提示:ZipFile只能压缩目录,不能直接压缩多个文件。
            直接压缩多个文件,只能用ZipArchive。
    
    
    3、初始化器
        
        初始化器(Initializer)是一种用于初始化对象或集合的简便语法。通过使用初始化器,
        可以在创建对象或集合的同时为其属性或元素赋值,提供了一种便捷的方式来初始化数据。
        
        (1)对象初始化情况

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p1 = new Person();//1
                p1.Name = "First";
                p1.Age = 20;

                Person p2 = new Person("Second", 21);//2

                Person p3 = new Person() { Name = "Thirst", Age = 22 };//3

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }

            public Person()
            {
            }

            public Person(string name, int age)
            {
                this.Name = name;
                this.Age = age;
            }
        }     

   
        上面第一方式:

(9)基础强化:元字符,正则表达式,匹配,提取组,Regex,Match与Matches_第1张图片

 


        实际上就是先构造对象,再用set赋值各属性。
        
        上面第二方式:
      

(9)基础强化:元字符,正则表达式,匹配,提取组,Regex,Match与Matches_第2张图片

 


        这个直接是使用的是构造函数。
        
        上面第三种方式:
      

(9)基础强化:元字符,正则表达式,匹配,提取组,Regex,Match与Matches_第3张图片

 


        注意,它先构造了一个对象,命名随机,然后才将此对象赋值给p3.
        
        3处的括号可以省略Person p3 = new Person{ Name = "Thirst", Age = 22 }
        对于使用初始化器进行对象初始化,当使用默认的无参数构造函数创建对象时,可以省略().
        省略()是为了使代码更加简洁和易读。如果你想要明确指定使用哪个构造函数进行初始化,
        或者提供参数进行初始化,那么不应该省略 ()。
        
        总结:初始化器,是创建一个对象,对象名随机,赋值后,再提供给原对象名。
        
        (2)集合初始化器:

        List list1 = new List();//1
        list1.Add(1); list1.Add(2); list1.Add(3);

        List list2 = new List() { 1, 2, 3 };//2

        List list3 = new List(3) { 1, 2, 3 };//3


        
        上面的1和前面的1相似,都是构造一个集合后,加入三个值。
        
        后面的2和3一样,构造一集合后,命名随机,再加入三个值,再赋值给原集合名。
        
        注意:(1)初始器的值都是在{}中;
              (2)初始化器在对象创建时立即执行,而构造函数在对象创建后的初始化阶段执行。
              换句话说,初始化器是在对象创建过程中为属性赋值的一部分,而构造函数是对象
              的初始化逻辑的一部分,可以包含更复杂的初始化操作。
              
        简单地说:初始化器在构造函数之后

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p = new Person(19) { Age = 21 };
                Console.WriteLine(p.Age);//21
                Console.ReadKey();
            }
        }

        internal class Person
        {
            public int Age { get; set; }

            public Person(int age)
            {
                this.Age = age;
            }
        }


        上面结果是21,因为最后是初始化器执行,它覆盖了前面的构造函数的值。
        
        另外,也可以直接写{},因为对象后面必须有(),{},[]之一。

        Person p1 = new Person();
        Person p2 = new Person() { };
        Person p3 = new Person { };
        Person p4 = new Person;//错,必须有(),{},[]之一    

    
        
        
    4、什么是匿名类?
    
        匿名类是一种临时创建的类,用于在声明时定义其属性并初始化这些属性的值。匿名类的定
        义方式是通过使用关键字new和var并使用对象初始化器来创建。
        
        匿名类通常用于临时存储一组属性,而不需要定义专门的自定义类。它是一种方便的方式来
        创建临时的、只用于特定目的的数据结构。

        var person = new
        {
            Name = "John",
            Age = 30,
            IsStudent = true
        };
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}, IsStudent: {person.IsStudent}");


        
        上面创建了一个匿名类person,有Name、Age和IsStudent三个属性,并为它们分别赋予
        了"John"、30和true作为初始值。输出时访问其属性。
        
        注意:匿名类是只读的,无法对其属性进行修改。匿名类的属性和初始值在创建时确定,
                之后无法更改。此外,匿名类的类型名由编译器自动分配,可以通过var关键字
                隐式类型推断来声明匿名类的实例。
                
        匿名类主要用什么什么场景?
        
            匿名类在以下场景中非常有用:
        
        (1). 数据传递:
        
            匿名类可以用于在方法之间传递临时的数据结构。当你需要传递一组相关属性的数
            据,但又不想为这些属性创建专门的类时,匿名类可以提供一种简洁的解决方案。

        var pp = new { Name = "匿名", Age = 19 };
        ShowPerson(pp);
        
        private static void ShowPerson(object p)
        {
            Console.WriteLine($"{((dynamic)p).Name},{((dynamic)p).Age}");
        }


            注意:
            (a).号的运算优先级高于(),所以(dynamic)p.Name是先运算.后(),也就是先p.Name
                然后才是(dynamic)(p.Name)。因此这里要把p的转换后才取属性。
                
            (b)dynamic是C#中的一个类型,它表示一种动态类型。与其他类型(如int、string等)
                不同,dynamic类型的变量在编译时不进行静态类型检查,而在运行时进行类型检查
                和解析。
                
                因此使用dynamic关键字,可以在编译时不指定变量的具体类型,而是延迟类型的
                确定,直到运行时才确定其类型。
                
                上面使用了dynamic。因为匿名类的类型是在编译时未知的,需要使用dynamic来进
                行运行时类型推断和访问。
                
                例如,((dynamic)person).Name 表示我们在运行时以动态方式访问person对象的 
                Name 属性。由于编译器无法在编译时确定person的具体类型,因此使用dynamic 
                进行类型推断,使得编译器知道在运行时调用适当的属性。
                
                注意:使用dynamic会带来一些性能上的开销,因为类型检查是在运行时进行的。
                        此外,由于缺乏编译时的类型检查,使用dynamic也增加了代码的不确定
                        性和潜在的运行时错误的风险。因此,在使用dynamic时需要谨慎,确保
                        在运行时操作正确的类型,并在必要时进行适当的类型转换和异常处理。
            
            
        (2). LINQ查询:
        
            在LINQ查询中,匿名类可以用于创建临时的投影查询结果。通过匿名类,你可以定
            义仅包含你需要的属性的结果集,而不需要创建一个新的自定义类。

        var students = new[]
        {
            new { Name = "John", Age = 20, Grade = "A" },
            new { Name = "Sarah", Age = 22, Grade = "B" },
            new { Name = "Emily", Age = 19, Grade = "A" }
        };

        var query = from student in students
                    where student.Age > 20
                    select new { student.Name, student.Grade };

        foreach (var result in query)
        {
            Console.WriteLine($"Name: {result.Name}, Grade: {result.Grade}");
        }


      
        
        通过LINQ查询创建了一个包含部分属性的匿名类结果集,该结果集表示年龄大于20的学生的姓名和成绩。
        
        注意:匿名类是临时的,其生命周期只存在于创建它的方法或语句块内部。在方法调用或
                语句块结束后,匿名类的实例将被销毁。因此,匿名类适用于临时性的数据存
                储和传递。
        
        
    5、GZipStream压缩与解压过程。
        
        这个代码不必死记,记住过程:
        相同的:都得用FileStream打开读取流和写入流;
        不同的:
        压缩时,读取正常流fsr,然后写入正常流fsw,但在写入时得变换下,即压缩流gz写
                入到磁盘。
        解压时,读取正常流fsr,但这个流是被压缩的,因此这个需要解压出来gz,然后再按
                正常流fsw进行写入。
        //压缩

        using (FileStream fsr = File.OpenRead(@"E:\2.jpg"))//1
        {
            using (FileStream fsw = File.OpenWrite(@"E:\2.rar"))//2
            {
                using (GZipStream gz = new GZipStream(fsw, CompressionMode.Compress))//3
                {
                    byte[] buffer = new byte[1024 * 8];
                    int bytesRead;
                    while ((bytesRead = fsr.Read(buffer, 0, buffer.Length)) > 0)//4
                    {
                        gz.Write(buffer, 0, bytesRead);//5
                    }
                }
            }
        }
        //解压
        using (FileStream fsr = File.OpenRead(@"E:\2.rar"))//6
        {
            using (FileStream fsw = File.OpenWrite(@"E:\3.jpg"))//7
            {
                using (GZipStream gz = new GZipStream(fsr, CompressionMode.Decompress))//8
                {
                    byte[] buffer = new byte[1024 * 8];
                    int bytesRead;
                    while ((bytesRead = gz.Read(buffer, 0, buffer.Length)) > 0)//9
                    {
                        fsw.Write(buffer, 0, bytesRead);//10
                    }
                }
            }
        }


            上面压缩时,1打开源文件,2指定目标文件,最终需要利用2压缩即3处,在4处读取
            是源文件fsr,最终是要压缩写入文件,所以5用gz写入。
            
            下面解压时,同样6打开源文件,7指定解压的文件。由于filestream是正常流,若是
            压缩流,需要解压释放出来成为正常的流,因此8处的解压应该是从源文件即6处的fsr
            读取而来,这样源文件经解压后,就是正常的流了。因此9处用gz解压后的流进行读取,
            后面都是正常流了,直接用直接写入流fsw进行写入即可。
            
            上面区别的是:压缩在写入流后,用压缩流进行写入。
                          解压在读取流,最后正常流写入。
                          


二、元字符


    1、正则表达式前奏: 地狱
    
        有时提取或替换字符串的某些有规律的字符或字符串,会感觉复杂或难以下手:
        
        需求1:
        "192.168.10.5[port=8080]"这个字符串表示IP地址为192.168.10.5的服务器的8080端口
        是打开的,请用程序解析此字符串,然后打印出"IP地址为***的服务器的***端口是打开
        的"。
    
        需求2:
        "192.168.10.5[port=21,type=ftp]",这个字符串表示IP地址为192.168.10.5的服务器
        的21端口提供的是ftp服务,其中如果",type=ftp"部分被省略,则默认为http服务。请
        用程序解析此字符串,然后打印出“IP地址为***的服务器的**端口提供的服务为***”
    
        需求3: 
        判断一个字符串是否是Email? 必须含有@和.、不能以@或者.开始或者结束、@要在最后
        一个.之前
    
        需求4: 
        从一个文本中提取出所有的Email: 我有全部333M的照片,要的给我发email:[email protected]
        我也要[email protected],[email protected],楼主好人: [email protected].
    
    
    2、正则表达式入门: 天堂
        
        正则表达式是用来进行文本处理的技术,是语言无关的,在几乎所有语言中都有实现。
        javascript中还会用到。[正则表达式是对文本、对字符串操作的。]
    
        一个正则表达式就是由普通字符以及特殊字符(称为元字符)组成的文字模式。该模式描
        述在查找文字主体时待匹配的一个或多个字符串。正则表达式作为一个模板,将某个字
        符模式与所搜索的字符串进行匹配。正则表达式用来描述字符串的特征。
    
        就像通配符“*.jpg”、“%ab%”,它是对字符串进行匹配的特殊字符串
        
        正则表达式是非常复杂的,不要希望一次都掌握,理解正则表达式能做什么 (字符串的
        匹配、字符串的提取、字符串的替换),掌握常用的正则表达式用法,以后用到再查就行。
        
        找工作的亮点。后面项目中的采集器、敏感词过滤、URLRewiteValidator也会涉及到正
        则表达式
        
        正则表达式是对字符串操作的。
    
    
    3、元字符1
    
        要想学会正则表达式,理解元字符是一个必须攻克的难关。不用刻意记.
        
        问:什么是元字符?
        答:元字符是具有特殊含义的字符。它们不仅仅匹配字面值,而是用于表示某种模式或
            字符类型。这些元字符可以组合使用或与其他字符一起使用,构成复杂的匹配模式。
            例如,\d+表示匹配一个或多个数字字符。使用元字符可以更灵活和准确地进行模式
            匹配。
    
        .:匹配除\n之外的任何单个字符。
            例如:正则表达式"b.g"能匹配如下字符串:
            "big"、"bug"、"b g",但是不匹配"buug","b..g"可以"buug”。
        
        []:字符组,匹配括号中的任何一个字符(范围,字符集合)。
                例如:正则表达式"b[aui]g”匹配bug、big和bag,但是不匹配beg、baug。
            可以在括号中使用连字符-”来指定字符的区间来简化表示,
                例如正则表达式[0-9]可以匹配任何数字字符,这样正则表达式"a[0-9]c"等价
                于a[0123456789]c"就可以匹配"a0c”、“a1c”、a2c"等字符串;
            还可以制定多个区间,例如[A-Za-z]"可以匹配任何大小写字母,“[A-Za-z0-9]”可
                以匹配任何的大小写字母或者数字。
            
            思考:x[这里必须是元音]y,如何写正则?
            
            当.出现在[]中,则表示普通字符,而不作为元字符。
            -出现在[]中的非第一个字符时,认为是元字符 ,表示范围。
                a[-a-z]b第一个-是字符不是元字符,后面第二个-是元字符不是字符。
            |元字符在[]中也只表示一个普通的竖线,()在[]也表示普通的括号,类似的还有+,*,?等。
            提示:因为在[]里面用转义\或不用,都是可以的,若不明白,就傻瓜式地加上转义。
                例如:  a[.x]b这里的.是普通字符,若不明白加上转义a[\.x]b表示一样的意思。
                
            注意:在[]外的元字符要表示普通字符,需要转义。
                例如:a\.b这里的.黑底后表示匹配a.b字符串。
            
            问:[a-zA-Z]与[A-Za-z]谁正确?谁更节约资源?
            答:两者均正确。两者消耗资源相差微小。
                []里并不按照ASC顺序进行比对,而是按[]里的先后顺序进行比对。例如,对
                于字符类[aAbB],它会匹配小写字母’a’、大写字母’A’、小写字母’b’和大写
                字母’B’中的任意一个字符,顺序并不影响匹配的结果。
                
                至于哪个表达式更节约资源,实际上差异非常微小,可以忽略不计。在实际
                使用中,这两个表达式的性能基本相同,无论选择哪个都不会对性能产生显
                著影响。
                
            问:[A-z]表示[A-Za-z]吗?
            答:[A-z]与[A-Za-z]不是完全等价的。
            
                [A-z]表示从大写字母’A’到小写字母’z’之间的字符范围。这包括大写字母、
                小写字母以及一些其他特殊字符,如方括号与反斜杠。因此,使用[A-z]可
                能匹配到你不期望的字符。
                如果希望只匹配字母,应该使用[A-Za-z]而不是[A-z]。
                
            问:[b-d]与[d-b]一样吗?
            答:不一样.
                [b-d]表示’b’、‘c’和’d’。而[d-b]则是一个无效的字符范围,因为它是一个逆
                序的范围。
                
                在字符范围中,起始字符应该在结束字符之前。因此,[d-b]没有有效的匹配。
                
                无效字符范围,将无法匹配,或者正则表达式引擎报错。
            
        |:将两个匹配条件进行逻辑"或"运算。
            z|food能匹配z或food。(z|f)ood则配zood或food。 
            a(x|y)b只能匹配axb或ayb。
            
            问:正则表达式的优先级是怎样的? 
            答:在C#常见的正则表达式运算符和它们的优先级从高到低的顺序:
                
                1. `\`:转义符号。它用于转义特殊字符,使其失去其特殊含义。
                        例如,`\.`, `\\`。
                2. `[]`:字符类。它用于匹配一个字符范围内的任意字符。
                        例如,`[a-z]`, `[0-9]`。
                3. `()`:括号。它用于分组操作,控制运算符的作用范围并提供子表达式。
                        例如,`(ab)+`, `(abc|def)`。
                4. `*`、`+`、`?`、`{n}`、`{n,m}`:重复匹配。它们用于规定前面的表达式可
                    以重复出现的次数。
                        例如,`a*`, `b+`, `c?`, `d{3}`, `e{1,3}`。
                5. `^`、`$`:锚点。它们用于匹配字符串的开头和结尾位置。
                        例如,`^abc`, `xyz$`。
                6. `|`:或。它用于设置多个表达式的选项,匹配其中之一。
                        例如,`a|b|c`。
                7. `.`:通配符。它用于匹配除换行符外的任意一个字符。
                        例如,`a.b`。
                8. `\b`、`\B`:单词边界。用于匹配单词的边界位置。
                        例如,`\bword\b`。
                
                注意:括号可以改变运算符的优先级。在括号中的表达式最先被计算。
                    例如,`(ab)+`表示`ab`可以重复出现一次或多次。
            
        ():将()之间括起来的表达式定义为"组”(group),并且将匹配这个表达式的字符保存到一
            个临时区域,这个元字符在字符串提取的时候非常有用。把一些字符表示为一个整体。
            改变优先级、定义提取组两个作用。
    
    
    4、元字符2(限定符)
        
        总结:    *{0,},+{1,},?{0,1}
        
        *:匹配0至多个在它之前的子表达式,和通配符*没关系。等价于{0,}。
            例如正则表达式"zo*”(等同于z(o))能匹配"z"、"zo"以及“oo”;
            因此".*"意味着能配任意字符串。
            "z(b|c)*"表示zb、zbc、zcb、zccc、zbbbccc。
            "z(ab)*"能匹配z、zab、zabab(用括号改变优先级)
        
        +:匹配前面的子表达式一次或多次,和*对比《0到多次)。等价于(1,}
            例如正则表达式9+匹配9、99、999等。
            “zo+"能队配zo"以及“zoo”,不能配"z"。
            
        ?:匹配前面的子表达式零次或一次。等价于: {0,1}
            例如,"do(es)?”可以配"do"或"does"。
            [colou?r、favou?r]一般用来配可选部分”。(终贪婪模式)
        
        限定符:限定前面的正则表达式出现的次数。
        
        {n} :匹配确定的 n 次。"zo{2)"表示zoo。
            例如,"e{2}”不能配bed中的e,但是能配seed中的两个ee。 //seeeed,不可以。
            
            问:[0-9]{3}表示任意的三个数字,若表示三个相同的数字呢?
            答:^(\d)\1{2}$
                ^ 表示匹配字符串的开头位置。
                (\d) 表示一个数字字符的捕获组。
                \1 是一个反向引用,引用第一个捕获组中匹配的内容。
                {2} 表示前一个表达式(即捕获组中的数字字符)匹配连续出现两次。
                $ 表示匹配字符串的结尾位置。
                
                可以匹配 “111”、“222”、“333” 等连续三个重复的数字字符,但无法匹配 
                “123”、“456” 等不连续或不重复的数字字符。
                
                
        {n,} :至少匹配n次。例如,e{2,}不能匹配bed中的e,但能匹配seeeeeeeed中的所有e。
        
        {n,m} :最少配 n 次最多配 m 次。e{1,3}将配seeeeeeeed中的前三个e。
            {2,5}//bed,seed,seeed; beeeeed。
        
        
    5、元字符3
        
        ^(shift+6) : 匹配一行的开始。
            例如正则表达式“regex”能够匹配字符串“regex我会用”的开始,但是不能匹
            配“我会用regex”。
            
        ^另外一种意思: 非! (^0-9)
            a[^zxy]b匹配aab,但不匹配azx,ab
            
            注意:[]匹配时必须要有一个字符出现,哪怕是用非^时,所以上面ab不匹配。
        
        $:匹配行结束符。
            例如正则表达式“浮云$”能够匹配字符串“一切都是浮云”的末尾,但是不能匹配字
            符串“浮云呀”
            
        ^abc,匹配一个正则表达式的开始,这样来理解: 一字母a开头,后紧跟一个b,然后又紧
            跟一个c
            abcjflkdsjfkds
            
        888$,匹配一个正则表达式的结束。
            积分多少快乐解放路口的手机费888
    
        问:正则中^是一行的开始,还是一句的开始?
        答:^是匹配一行的开始位置,而不是一句的开始。
            ^用于匹配字符串的开头位置。如果正则表达式以^开头,它会尝试匹配输入字符串的
            起始位置,而不是每行的起始位置。
            
            假设我们有一个多行的输入字符串,每行都以数字开头,我们可以使用^\d来匹配每
            行开头的数字。这里的^表示匹配每行的开头位置。
            
            注意:默认情况下,C#正则表达式的匹配是在整个输入字符串上进行的,而不是逐行
                匹配。如果你想要逐行匹配,可以使用RegexOptions.Multiline选项,这样^将
                匹配每行的开头位置。
                
            同理:$表示一行结束而不是一句结束。
            
        问:RegexOptions.Multiline有什么作用?
        答:RegexOptions.Multiline是一个标志,用于指定在匹配时是否将输入字符串视为多行。
            使用RegexOptions.Multiline选项时,注意:
            
            1. `^`和`$`的匹配行为:
            默认情况下,`^`匹配字符串的开头,`$`匹配字符串的结尾。但是,使用RegexOpti
            ons.Multiline`选项后,它们会匹配每行的开头和结尾(以换行符为界)。
            
            2. `\A`和`\Z`的匹配行为:
            `\A`匹配字符串的开头,`\Z`匹配字符串的结尾。如果使用`RegexOptions.Multil
            ine`选项,它们将仍然匹配整个字符串的开头和结尾。
            
            3. `.`匹配任意字符时是否包括换行符:
            默认情况下,`.`匹配除换行符外的任意字符。然而,如果使用`RegexOptions.Mult
            iline`选项,`.`将匹配包括换行符在内的任何字符。
            
            4. `\b`和`\B`的匹配行为:
            在默认情况下,`\b`匹配单词边界,`\B`匹配非单词边界。但是,如果使用`RegexO
            ptions.Multiline`选项,`\b`和`\B`的行为不会受到影响。
            
            通过使用`RegexOptions.Multiline`选项,你可以在匹配时将输入字符串视为多行
            文本,以便更灵活地处理行级别的匹配需求。
    
    
    6、简写表达式
    
        注意这些表达式是不考虑转义符的。
        
        正则表达式中的\表示是正则里生效的字符\,而不是C#字符串级别的\。因此正则表达式中
        如果含有\字符,在C#代码中会被误认为是C#中的转义符。
        
        因此,需要使用@或者\双重转义。先让C#识别有\字符,然后在正则表达式中\生效。
        
        例如:正则表达式a\db表示ab之间有一数字,若用在C#代码中,会把\当作转义符,但C#
        中没有\d的定义,就会报错。因此需要再保留\,可以在C#中用a\\db,或者用@"a\db"。
        
        在C#看来@"\-"就是\-这个普通的字符串,只不过在正则表达式分析引擎看来他有了特殊
        含义。
        
        思路就是:先正则,后C#
        例1:比如对于\d匹配数字,先正则就是\d,再C#中\需要转义,因此C#完整应该为\\d
        例2:比如对于\d两个是普通字符时,不能当作\d来匹配数字:
            先正则:\d中\要转义故写成\\d
            后C#:对于上面的\\d,C#对\要转义,因此每一个都要转一次,最终:\\\\d
            
        技巧:为了减少麻烦,对于第一步正则,可以按正常的写。
            但对第二步“后C#”,可以直接用@来减少再次转义。
            
            因此上面例1:\d先正则就是原样\d,第二步再C#,就直接用@"\d"
                上面例2:\d普通字符,先正则就是\\d,再第二步C#:@"\\d"
            
            这样就简单多了。因为@就是不需要转义。
        
        
            \d:代表一个数字,等同[0-9]。\\d是\d
            \D:代表非数字,等同[^0-9]
            \s:代表换行符、Tab制表符等空白字符(空格、回车、制表符)
            \S:代表非空白字符(a0%s@@)
            \w:匹配字符或数字或下划线或汉字,即能组成单词的字符。如果通过ECMAScript
                选项指定了符合ECMASctipt的行为,则\w等效于[a-zA-Z_0-9]
            \W:非\w,等同于[^\w]%
            \b: 单词的边界。一边是单词 (\w) ,一边不是单词(\W))。
                Hello nihao,are you kidding? D-Day
                
        d:digital; s: space、w: word。大写就是“非”
        
        C#中当在字符串前使用@符号时,字符串中可以用两个双引号表示一个双引号。
            例如:Console.WriteLine(@"aaaa""fdsafdsaf");
        
        
        问:如何匹配任意的单个字符?[\s\S]
        答:能用.吗?不能,因为.不包含\n
            可以用[\s\S],[\w\W],[\d\D]
        
        
        问:空白字符有哪些?
        答:\s是一个特殊的字符转义序列,用于匹配空白字符(whitespace characters)。
            空白字符包括下列字符:
                空格(ASCII码为32)
                制表符(ASCII码为9)
                换行符(ASCII码为10)
                回车符(ASCII码为13)
                垂直制表符(ASCII码为11)
                换页符(ASCII码为12)
    
        
        问:如果对字符串的"\"进行匹配,正则表达式应该怎么写?
        答:步骤:先正则后,再考虑C#转义写法。
            1.正则:\需要转义,故应写成"\\"
            2.C#:因为C#把\当作转义符,需要两次\\才是真正的\,因此上面有两个\\
                第一个\写成\\,第二个\\还得写成\\,组合起来为\\\\
            
            结果为:"\\\\"
            如果用@替换,则写成@"\\",它分别消除每个转义,故@"\\\"是错误的。
        


三、Net中的正则表达式


    1、正则表达式在.Net就是用字符串表示这个字符串格式比较特殊,无论具体什么含义由Regex
        类多么特殊,在C#语言看来都是普通的字符串,具体什么含义由Regex类内部进行语法分
        析。
    
        如何匹配大于10小于20的字符串? (正则表达式是对字符串的操作。)
        ^[1][1-9]$,[11,12,13,14,15,16,17,18,19]观察字符串!自己写正则表达式之前先仔细
        观察字符串,找规律。写正则表达式前,首先要做的就是找规律,根据规律写出相应的正
        则表达式。
        
    2、正则表达式 (Regular Expression)的主要类: Regex
    
        常用的3种情况: (C#语法)
        
        (1)判断是否匹配: Regex.IsMatch("字符串","正则表达式")
        
        (2)字符串提取: Regex.Match("字符串","要提取的字符串的正则表达式");
            //只能提取一个(提取一次)
        
            字符串提取(循环提取所有): Regex.Matches(),(可以提取所有匹配的字符串。)
        
        (3)字符串替换所有: Regex.Replace("字符串","正则","替换内容");
        
        
    3、判断是否匹配: Regex.IsMatch("字符串","正则表达式")
    

        Console.WriteLine(Regex.IsMatch("123456", "[0-9]{6}"));
        Console.WriteLine(Regex.IsMatch("12345678", "[0-9]{6}"));
        Console.WriteLine(Regex.IsMatch("12345678", "^[0-9]{6}$"));
        Console.WriteLine(Regex.IsMatch("bg", "^b.*g$"));//true *0或多个


        
        注意:要完全匹配,需要加^与$
        
        问:Regex.IsMatch("123456", "[0-9]${6}") 结果是多少?
        答:True
            实际上Regex.IsMatch("123456", "[0-9]${1}")结果也为true.
            
            在C#正则表达式中,${6}是无效的表达式,它并不会被解释为后向引用或者重复次
            数。所以在表达式a${6}中,${6}只会当作$,{6}被忽略掉了。
            因此整个表达式变成了[0-9]$,因此匹配的是末尾6,显示为true.
            
        问:Regex.IsMatch("123456", "([0-9]$){6}")结果是多少?
        答:false
            此时([0-9]$){6}是正常表达式,没有匹配成功。看下面结果:

        string text = "12\n34\r\n5\n6";
        string pattern = @"[0-9]${2}";
        MatchCollection matches = Regex.Matches(text, pattern, RegexOptions.Multiline);

        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }


            结果为2,5,6。因为多行模式下,2,5,6成功匹配。
            
            提示:$与\Z的区别:
            $:一般是字符串结尾判断,若多行模式进,还会对每行末尾判断,但以\n为标准而不
            是以\r\n为标准,所以上面结果4并没有匹配成功。如果想以\r\n或\n来判断是否结
            尾,用\r?$
                特别提醒:^要么是开始,要么以行\r\n进行判断在开头。它不会认\n。
                        感觉这两口子的口味就是不一样。
                
            \Z:只以字符串结尾为判断,无论是否有多行。
            
        问:z|food 等效于 (z)|(food)还是(z|f)ood?
        答:(z)|(food)
            如果正则表达式中使用 | 而没有加括号,它会将两侧内容作为一个整体进行处理。
            
            所以z,food,zood,都可以匹配上面(zood中有z)
            
            注意:虽然二者等效,但捕获组会对匹配结果进行分组存储,相比于不使用捕获组的
                情况,它可能会更费资源。相比引起误会情况,建议还是用()表明顺序。
            
            |还可以多级套用,但只会选择其中之一进行匹配。
            例如a|bc|cd|efg,对于a,bc,cd,efg都是可以匹配的,但一次只能选择之一。但是,
            它都是从左向右依次匹配,谁先成功就马上返回,并且不会尝试继续匹配后续的子
            模式。
            
            匹配时注意优先级,例如z|food$,因为$优先级高于|,因此等效于(z)|(food$)。
            
        问:判断是否为身份证号码?规律如下:
            1.长度15或18,首位为不0;
            2.如果15位,全为数字;
            3.如果18位,前17为数字,末尾可能字母X
        答:^[1-9]\d{14}(\d{2}[0-9X])?$
        
        
        练习:判断字符串是否为正确的国内电话号码,不考虑分机。
            010-8888888或010-88888880或010xxxxxxx
            0335-8888888或0335-88888888(区号-电话号)03358888888
            10086、10010、95595、95599、95588 (5位)
            13888888888(11位都是数字)
        
            ^((\d{3,4}-?\d{7,8})|(\d{5})|(l\d{10})$
            或者^((\d{3,4}\-?\d{7,8})|(\d{5}))$
            
            注意:上面-可以不用转义
            
            
        问:正则中需要单独转义的字符有哪些?
        答:在 C# 的正则表达式中,以下符号需要进行转义:
            
            1. 句点(.):
                在正则表达式中,句点(.)表示匹配任意单个字符,为了匹配句点本身,需要
                使用反斜杠进行转义,即 `\.`。
                
            2. 反斜杠(\):
                反斜杠在正则表达式中起到转义字符的作用,因此,在匹配字面意义的反斜杠
                时,需要进行双重转义,即 `\\`。
                
            3. 方括号([]):
                方括号用于定义字符类,如果要匹配字面意义的方括号,需要进行转义,
                即 `\[` 和 `\]`。
                
            4. 连接符(-):
                连接符用于定义字符范围,在某些情况下可能需要进行转义,比如要匹配字面意
                义的连字符,可以使用 `\-` 或者将其放在字符类的开头或结尾避免被解释为范
                围定义。
                
            5. 问号(?):
                问号用于表示前面的元素是可选的,如果要匹配字面意义的问号,需要进行转
                义,即 `\?`。
                
            6. 星号(*):
                星号用于表示前面的元素可以出现零次或多次,如果要匹配字面意义的星号,
                需要进行转义,即 `\*`。
                
            7. 加号(+):
                加号用于表示前面的元素可以出现一次或多次,如果要匹配字面意义的加号,
                需要进行转义,即 `\+`。
                
            8. 左大括号({):
                左大括号用于表示重复次数,如果要匹配字面意义的左大括号,可以直接使
                用 `{` 或者进行转义,即 `\{`。
                
            9. 竖线(|):
                竖线用于表示模式选择,如果要匹配字面意义的竖线,可以直接使用 `|`或者
                进行转义,即 `\|`。
                
            10. 括号(()):
                括号用于分组和捕获,如果要匹配字面意义的括号,需要进行转义,即 
                `\(` 和 `\)。
        
        
        练习:判断一个字符串是否是合法的Email地址。一个Email地址的特征就是以一个字符序
        列开始,后面跟着@符号,后面又是一个字符序列,后面跟着符号.,最后是字符序列。
            
            ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$
            
            
        问:.net默认使用的是Unicode的匹配模式?
        答:是的。
            例如:\d可以匹配半角或全角的数字例如123456
            
            注意:[0-9]只能匹配半角的数字
        

            Regex.IsMatch("12378","^\d+$"):true
            Regex.IsMatch("12378","^[0-9]+$"):false


            
            若不想匹配Unicode,请设置RegexOptions.ECMAscript,如:

            Regex.IsMatch(("12378","^\d+$",RegexOptions.ECMAscript):false


            
            同理,\w也是用的Unicode,所以包括汉字.

            Regex.IsMatch(("123强国","^\w+$",RegexOptions.ECMAscript):false
            Regex.IsMatch(("123强国","^\w+$"):true


            
            同理,\s也能匹配全角的空格。
            
        
        问:\d或\w等半角时是用RegexOptions.ASCII还是RegexOptions.ECMAScript?
        答:用 RegexOptions.ECMAScript。
            虽然 RegexOptions.ASCII 可以限制匹配范围为 ASCII 字符,但它同时会限制了其
            他 Unicode 字符的匹配能力。这可能不符合你只想匹配半角模式的需求,因为半角
            字符可能包含在 Unicode 字符集中,并非全部被包括在 ASCII 字符集内。
            
            简言之:半角包括ASCII。因此RegexOptions.ECMAScript 选项更适合半角的需求。
            它可以模拟 ECMAScript(JavaScript)的正则表达式引擎,其中的 \d 和 \w 是按
            照半角模式进行匹配的。
        
        
        练习:
        1、匹配IP地址,4段用.分割的最多三位数字。

            192.168.54.77、333.333.333.333假设都是正确的。
            1.2.222.3、192.168.0.156
            
            [1-9][0-9]{0,2}([.][0-9]{1,3}){3}
            
            
        2、判断是否是合法的日期格式“2008-08-08”。四位数字-两位数字-两位数字。
            
            [0-9]{1,4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])
        
        3、判断是否是合法的url地址,
            http://www.test.com/a.htm?id=3&name=aaa、
            ftp://127.0.0.1/1.txt。字符串序列:
            //字符串序列。http,https,ftp,file,thunder,ed2k
            
            (http|https|ftp|file|thunder|ed2k)://([\w\-]+\.)+[\w\-]+(/[\w\-./?%&=]*)?
        
            注意:\w:只匹配字符[a-zA-Z]或数字[0-9]或下划线[_]或汉字,即能组成单词的
                字符注意它还支持全角的情况。
                因此,对于-,?,&,%,=,.,/等符号,需要单独列出。
        
        
        问:上面第三题[\w]是表示\w,还是表示:一个"\"和一个"w"?
        答:在正则表达式的字符类(方括号[])中,特殊字符(例如 \w、\d)仍然保持其特殊
        含义,而不是被解释为字面字符。所以,在 [\w] 中,\w 仍然表示匹配任意字母、数字
        或下划线字符。
            
            同样地,在 [\d] 中,\d仍然表示匹配任意数字字符。而不能拆分为\与d
                

     
四、字符串的提取与提取组


    1、热身:提取字符串中的所有数字。
        “大家好呀,hello,2010年10月10日是个好日子。恩,9494.吼吼!886.”
        使用\d+而不是\d,\d只能提取单个的数字字符。
        提示:一般IsMatch要使用^$,但若是提取一般不使用^$
        

        string s = "大家好呀,hello,2010年10月10日是个好日子。恩,9494.吼吼!886.";
        Match m = Regex.Match(s, @"[0-9]+");
        while (m.Success)
        {
            Console.Write(m.Value);
            Console.WriteLine($"\t新游标位置{m.Index}");
            m = m.NextMatch();
        }
        Console.WriteLine($"\t最后游标位置{m.Index}");//0


        
        重要:
            当定义Regex.Match(s,@"[0-9]+")时,就开始进行了第一次匹配。匹配成功与否由
            Success决定,一旦成功Index就变得有效,无论后面m=m.NextMatch()如何变化成
            新的match对象,但它的index结果,都是针对最原始的字符串s的位置。
            
            简言之:一旦正则没匹配成功,match.success为false,且match.index为0,为0表
            示没有找到,而不是索引位置为0.
            
            另外,匹配中有一个隐藏的游标,匹配成功游标就到达匹配成功的位置。例如在索
            引5处匹配成功,这个匹配字符长度是5,那么下一次匹配时,游标从5+5+1处进行
            下一次的匹配(也即m=m.NextMatch()时).
        
        
    2、Match类
        
        Match类是C#中的一个类,位于System.Text.RegularExpressions命名空间中,用于表示
        正则表达式的匹配结果。下面是它的常用属性和方法:

        Success:一个bool类型的属性,表示匹配是否成功。如果匹配成功,则为true,
                否则为false。
        Index:一个int类型的属性,表示匹配的起始索引,即匹配的文本在原始字符串
                中的起始位置。
        Length:一个int类型的属性,表示匹配的长度,即匹配的文本的字符数。
        Value:一个string类型的属性,表示匹配的文本值。
        ToString():一个方法,将Match对象转换为字符串表示。返回的字符串包含匹配的
                文本值以及其他相关信息。
        
        NextMatch():用于获取下一个匹配结果,尽管返回的是一个新的match对象,但它的index
                仍然是以原始的字符串位置为准。
        
        Groups:用于获取匹配结果的分组信息等。通过这些属性和方法,可以方便地处理和操作
                正则表达式的匹配结果。
        
        注意:index是匹配成功后在原字符串的位置。不能以index=0来判断匹配成功或失败。
                而是用success来判断,然后index才有效。
        
        
        问:上面Value与ToString()有什么区别?
        答:match.Value 和 match.ToString() 都用于获取匹配项的文本表示。
            match.Value 是 Match 对象的属性,用于获取当前匹配项的文本值。它返回的是一个
            字符串,表示在输入字符串中匹配到的具体内容。
            
            match.ToString() 是 Match 对象的方法,该方法返回一个字符串,表示匹配项的文
            本值。实际上,match.ToString() 方法内部调用的是 match.Value 属性来获取匹配
            项的文本值,然后将其转换为字符串。
            
            故Value 和 ToString() 的结果是一样的,它们都返回匹配项的文本值。你可以根据
            个人喜好选择使用其中的一种方式来获取匹配项的文本表示。
            
        
        分组介绍:
        Groups是Match对象的一个属性,它表示匹配结果中的分组信息。当使用正则表达式进行匹
        配时,可以使用括号来创建分组,以便在匹配成功后获取特定组的值。
        
        Groups属性返回一个GroupCollection对象,其中包含与匹配的分组相对应的Group对象。
        Group对象表示一个匹配的分组,它提供了获取和操作匹配分组的功能。
        
        与Groups属性和Group对象相关的常用属性和方法:
        
            Groups.Count:返回分组的数量。
            Groups[i]:通过索引访问指定的分组,其中i表示分组的索引(从1开始)。
            Groups["groupName"]:通过分组的名称访问指定的分组。分组的名称由正则表达式中的
                                    捕获组指定。
            Group.Value:返回分组的匹配值。
            Group.Index:返回分组的起始索引。
            Group.Length:返回分组的长度。
        
        例如,可以使用Groups[1].Value来获取第一个分组的匹配值,
        使用Groups["groupName"].Value来获取指定名称的分组匹配值。
        这种分组功能非常有用,可以在正则表达式匹配结果中提取和操作特定的子匹配结果。
        
        
    3、用Regex.Matches()提取字符串的所有匹配。
        
        前面match是逐个提取,现在用Regex.Matches()一次性全部提取所有匹配。
        因此前面的例子可以改写如下:

        string s = "“大家好呀,hello,2010年10月10日是个好日子。恩,9494.吼吼!886.”";
        MatchCollection mc = Regex.Matches(s, @"\d+");
        foreach (Match m in mc)
        {
            Console.WriteLine($"索引位置{m.Index}匹配值{m.Value}");
        }
        Console.WriteLine($"共计有{mc.Count}");
        Console.WriteLine($"第一个{mc[0]}");
        Console.ReadKey();


        
        问:Regex.Match()与Regex.Matches()有什么区别?
        答:当你只需要获取第一个匹配结果或简单判断是否匹配时,使用Regex.Match方法。
            当你需要查找和处理多个匹配结果时,使用Regex.Matches方法(返回集合);
            
            简言之:Matches着重多个匹配结果的获取。
                    Match着重单个匹配结果的具体处理。
            
        
    4、MatchCollection类
        
        MatchCollection 类在 C# 中用于存储一个或多个正则表达式匹配的结果。它
        由 Regex.Matches 方法返回。
        
        MatchCollection 类提供了一组方法和属性,用于访问和操作多个匹配结果。这
        使得您可以轻松地处理匹配结果并进行进一步的操作。常用的方法和属性:
        
        Count :获取匹配结果的数量。
        Item 索引器(即 `matches[index]`):通过索引访问匹配结果的特定项。
        GetEnumerator :返回一个枚举器(Enumerator),用于遍历 
                        MatchCollection 中的匹配结果。
        

        string input = "123 abc 456 def";
        string pattern = @"\d+";
        Regex regex = new Regex(pattern);
        MatchCollection matches = regex.Matches(input);
        Console.WriteLine("匹配的结果数量:" + matches.Count);
        
        foreach (Match match in matches)// 使用 foreach 遍历匹配结果
        {
            Console.WriteLine("匹配的值:" + match.Value);
            Console.WriteLine("匹配的索引位置:" + match.Index);
            Console.WriteLine();
        }
        
        if (matches.Count > 0)// 访问单个匹配结果
        {
            Match firstMatch = matches[0];
            Console.WriteLine("第一个匹配的值:" + firstMatch.Value);
            Console.WriteLine("第一个匹配的索引位置:" + firstMatch.Index);
        }


        
        
        问:GetEnumerator请举一个例子?
        答:

            string s = "“123 abc 456 def";
            MatchCollection mc = Regex.Matches(s, @"\d+");
            IEnumerator imc = mc.GetEnumerator();
            while (imc.MoveNext())
            {
                Match m = (Match)imc.Current;
                Console.WriteLine($"索引位置{m.Index}匹配字串为{m.Value}");
            }GetEnumerator


            
            上面实际是逐个在列举match对象.
            
            
        问:MatchCollection已经有了foreach,为什么还要用GetEnumerator方法?
        答:foreach遍历时,提供更简洁和易读的代码。GetEnumerator() 与其他集合类型保
            持一致。使MatchCollection 也能够作为一个可枚举的对象被使用。
            
            如果您希望手动控制遍历过程,可以使用 GetEnumerator() 方法和 while 循环,
            以便在每次迭代中执行更多的自定义逻辑。这种情况下,在遍历过程中需要更多
            的灵活性时,使用 GetEnumerator() 方法会更加有用。
            
            简言之,它类似match一样逐个控制输出每一个匹配元素,如索引,长度等。
            如果既要获取全部,又要精细控制每一个,可以用GetEnumerator()试试.
        
        
        问: Regex r=new Regex(pattern)与mc=Regex.Matches(s,pattern)的区别?
        答:前面是实例方法,后面是静态方法。都可以取得匹配。
            
            区别在于性能和重用性。
            如果您需要多次使用同一个正则表达式进行匹配,使用new Regex(pattern) 可以
            将正则表达式编译为可重用的Regex对象,并在每次匹配时直接使用该对象,可以
            提高性能。然而,如果只需要简单地执行一次匹配操作,而不需要重复使用该正则
            表达式,使用 Regex.Matches(input, pattern) 可以避免显式创建Regex对象,
            更加简洁。
            
            选择使用哪种方式主要取决于您的具体需求。
        
        
    4、练习:字符串提取:
        提取字符串中的Email。如何提取所有的Email地址?Matches(),
        返回值为MatchCollection,可以通过索引器访问到Match对象。

        string s = File.ReadAllText(@"E:\1.txt");//含html源码
        //如果上面有乱码,用File.ReadAllText(filePath, Encoding.GetEncoding("GB2312"))等
        MatchCollection mc = Regex.Matches(s, @"\b[\w\-.+]+@[-\w]+(\.[-\w]+)+\b", RegexOptions.ECMAScript);
        foreach (Match m in mc)
        {
            Console.WriteLine(m.Index);//也可列出匹配位置
        }
        for (int i = 0; i < mc.Count; i++)
        {
            Console.WriteLine(mc[i].Value);
        }    

    
        
        引申:上面的邮件出来后是一个整体,对[email protected]形式,想直接提取前面的a或b或c部分,
            怎么做呢?这里要使用“提取组”概念.
            
        
    5、提取组
        
        凡是正则表达式中用()的部分就是提取组
        ()既有改变优先级别的作用,又有分组进行提取组的功能。
        例如:\b([\w\-.+]+)@([-\w]+)((\.[-\w]+)+)\b表示有4个分组。
        
        对于分组嵌套的情况,从左向右数,以第一个左边括号为第一组,第二个左边括号为第二组
        ....依次数下去,是第几个左括号就是第几组。不用管右边的括号。例(()())()()有5组
        
        对于的就是Match的属性Groups,表示匹配结果中的组集合。
        返回值是一个GroupCollection对象,它包含了所有捕获的组。注意:它是只读的。
        
        重要:
            无论是否有捕获组,groups.Count始终为1,因为它包括整个正则表达式模式的匹配
            结果本身,它被视为一个默认的零号组。
            因此,对于匹配成功时groups[0]表示整个匹配成功的字符串.

        string s = "My email is [email protected] or [email protected]";
        MatchCollection mc = Regex.Matches(s, @"@(\w+)\.", RegexOptions.ECMAScript);
        foreach (Match m in mc)
        {
            Console.WriteLine($"{m.Value}里用户名为{m.Groups[0]},域名为{m.Groups[1]}");
            Console.WriteLine(m.Groups.Count);
        }


        结果:
            @163.里用户名为@163.,域名为163
            2
            @qq.里用户名为@qq.,域名为qq
            2
        
        
    6、自定义提取组
        
        自定义命名组为捕获组赋予了可读性强且有意义的名称,对于代码阅读和维护都非常
        有帮助。

        自定义命名组的语法为:
                (?pattern)
            其中`name`是组的名称,`pattern`是组的正则表达式模式。
            
        如何使用自定义命名组:

        string text = "Hello, my name is John Doe.";
        string pattern = @"Hello, my name is (?\w+ \w+).";
        Match match = Regex.Match(text, pattern);
        if (match.Success)
        {
            GroupCollection groups = match.Groups;
            Console.WriteLine("Match: " + match.Value);
            Console.WriteLine("Name: " + groups["name"].Value);
        }


        通过在`groups`属性中使用组名来访问捕获组的值,可以使用groups["name"].Value
        来获取名字的值。又如:

        string s = "My birthday is on 2023-07-12";
        MatchCollection mc = Regex.Matches(s, @"(?\d{4})-(?\d{2})-(?\d{2})");
        foreach (Match m in mc)
        {
            Console.WriteLine(m.Value);
            //注意gourps[0]表示m.value即当前整个匹配串
            Console.WriteLine($"\t{m.Groups[1]}年{m.Groups[2]}月{m.Groups[3]}日");
        }


        
        强调:Groups始终是1,哪怕没有匹配成功,它也是1,所以用groups时确保成功匹配。

        string s = "My birthday is on aaaa-aa-aa";
        Match mc = Regex.Match(s, @"(?\d{4})-(?\d{2})-(?\d{2})");

        Console.WriteLine(mc.Groups.Count);//1
        Console.WriteLine(mc.Groups[0]);//""
        上面没匹配成功时count也是1,groups[0]是null。所以应该用success来判断使用:
        string s = "My birthday is on 2023-07-12";
        Match m = Regex.Match(s, @"(?\d{4})-(?\d{2})-(?\d{2})");
        while (m.Success)
        {
            Console.WriteLine(m.Groups[0]);//2023-07-12
            Console.WriteLine($"\t{m.Groups[1]}年{m.Groups[2]}月{m.Groups[3]}日");//2023年07月12日
            m = m.NextMatch();
        }
        Console.WriteLine(m.Groups.Count);//1    

    
        
        或者用foreach来用,因为有集合就说明有匹配成功。

        string s = "My birthday is on 2023-07-12";
        MatchCollection mc = Regex.Matches(s, @"(?\d{4})-(?\d{2})-(?\d{2})");
        foreach (Match m in mc)
        {
            Console.WriteLine(m.Groups[0]);//2023-07-12
            Console.WriteLine($"\t{m.Groups[1]}年{m.Groups[2]}月{m.Groups[3]}日");//2023年07月12日
            Console.WriteLine(m.Groups.Count);//4   
        }
        //只有Match有Groups属性,所以mc是MatchCollection无Groups属性


        
        上面m.Groups.Count为4,因为匹配成功一次中:本身索引为0,year1,month2,day3.
        
    
    7、GroupCollection类
        
        GroupCollection是一个类,用于表示正则表达式匹配结果中的所有组。
        它是Match对象的一个属性,提供了对所有匹配组的访问。

        GroupCollection类提供了以下重要成员:
            Count:获取组的数量。
            IsReadOnly:获取一个值,指示集合是否为只读状态。
            Item[Int32]:通过组的索引获取指定的组。
            Item[String]:通过组的名称获取指定的组。

        string text = "Hello, my name is John Doe. I am 30 years old.";
        string pattern = @"my name is (?\w+ \w+). I am (?\d+) years old.";
        Match match = Regex.Match(text, pattern);
        if (match.Success)
        {
            GroupCollection groups = match.Groups;
            Console.WriteLine("Group count: " + groups.Count);//3
            
            for (int i = 0; i < groups.Count; i++)// 遍历所有组
            {
                Group group = groups[i];
                Console.WriteLine("Group index: " + i);
                Console.WriteLine("Group name: " + group.Name);
                Console.WriteLine("Group value: " + group.Value);
                Console.WriteLine();
            }
            
            Group nameGroup = groups["name"];// 通过名称获取组
            Console.WriteLine("Group by name \"name\": " + nameGroup.Value);
        }


        通过Regex.Match方法得到一个Match对象,并通过其Groups属性获取到匹配结果中的组。
        通过遍历GroupCollection中的每个Group对象,我们可以获取每个组的索引、名称和值。
        此外,我们还可以通过组的名称(如"name")来直接获取指定的组。
        
        
    8、Group类
        
        GroupCollection中的每个元素都是Group对象。
        Group类表示正则表达式匹配结果中的一个组。        Group类具有以下一些常用属性:
            Value:获取组的匹配结果字符串。string类型
            Index:获取组匹配的开始位置索引。
            Length:获取组匹配的长度。
            Captures:获取与组匹配的所有捕获的集合。

        string text = "Hello, my name is John Doe.";
        string pattern = @"Hello, my name is (?\w+ \w+).";
        Match match = Regex.Match(text, pattern);
        if (match.Success)
        {
            GroupCollection groups = match.Groups;

            foreach (Group group in groups)
            {
                Console.WriteLine("Group value: " + group.Value);
                Console.WriteLine("Group index: " + group.Index);
                Console.WriteLine("Group length: " + group.Length);
                Console.WriteLine();
            }
        }


        注意:每个匹配结果都作为Group对象存储在GroupCollection中,你可以通过foreach循
            环或使用索引来遍历它们。每个Group对象都提供了有关具体匹配组的信息,如值、
            索引和长度等。
        
        提示:因为match.Groups[i]或match.Groups["name"]的类型是Group类,因此不能用它
            和null或string.Empty直接进行比较。是否有值是用它的属性value来比较。
            if(m.Groups["Age"].Value==String.Empty)
        
        
    9、提取组的性能问题
        
        (1)提取组与捕获组是相同的一个概念。可以相互换称。
        
        (2)使用圆括号 () 可以创建捕获组。
            这意味着括号内的表达式将被视为一个组,并且被用于捕获匹配的子字符串。
            
            使用圆括号创建的捕获组可以提供以下功能:
                将组内的子字符串作为单独的项进行提取。
                使用组的索引或名称来访问匹配结果中的组。
            
        (3)捕获组在正则表达式匹配期间会使用一些额外的资源,因为它们需要在匹配过程中进
            行缓存。每个捕获组都会生成一个结果对象,用于存储匹配的子字符串。
            
            当使用捕获组时,正则表达式引擎会为每个捕获组创建一个额外的对象,并将匹配的
            子字符串存储在这些对象中。这些对象的创建和管理会占用一定的内存和处理时间。

            
            当需要进行大量匹配操作时,使用过多的捕获组可能会导致性能问题,尤其是在匹配
            大量文本时。因此,在设计正则表达式时要谨慎使用捕获组,只在需要提取和使用特
            定子字符串时才使用它们。
            
            如果你对某个组不感兴趣,或者不需要通过索引或名称来访问匹配结果中的组,可以
            使用非捕获组 (?: ) 来排除它们的缓存和处理开销。

        string s = "Hello, my name is John Doe. I am 30 years old.";
        Match m = Regex.Match(s, @"(?:\w+ \w+)\..+ (\d+)");
        if (m.Success)
        {
            Console.WriteLine(m.Groups.Count);//2
            foreach (Group m2 in m.Groups)
            {
                Console.WriteLine(m2.Value);//整体与30
            }
        }


            上面用了(?:)后,Count就为2了,一个是整体的匹配,另一个是年龄的匹配。
            而中的名字Jahn Doe因为用了非捕获组,所以Group中无法访问,它没有缓存。
            从而减少了开销。
        
        引申:?. 可空条件,可以为空,为空时不用执行后面的属性或方法,直接返回null
                    例p?.Age(),或a?[3]数组为空不用取第4元素直接返回null
              !   不可为空,告诉编译器确定不为null。若真的为null,直接抛异常。
              ?:三目运算,为真第一个,为假第二。
                    例 a>3?1:2   若a=4则返回1
              ??  null合并,不为null返回原值,为空返回后面的默认值。
                    例 a=b??3    若b不为null,则a=b;若b为null则用后面默认值a=3
        


五、提取组练习


    1、从文件路径中提取出文件名(包含后缀) @"^.+\(.+)$"。比如从c:\windows\testb.txt中
        提取出testb.txt这个文件名出来。项目中用Path.GetFileName更好。贪婪模式。注意:
        \在c#中与在正则表达式中的转义符问题。演示:见备注1.

        string p = @"c:\windows\testb.txt";
        Match m = Regex.Match(p, @".+\\(.+)");
        if (m.Success)
        {
            Console.WriteLine(m.Groups[1]);
        }    

    
        引申:贪婪模式
        
    
    2、从“June       26     ,       1951    ”中提取出月份June来。
        @"([a-zA-Z]+)\s*(\d{1,2})\s*,\s*\(d{4})\s*$"进行匹配。月份和日之间是必须要有
        空格分割的,所以使用空白符号“\s”匹配所有的空白字符,此处的空格是必须有的,所
        以使用“+”标识为匹配1至多个空格。之后的“,”与年份之间的空格是可有可无的,所以
        使用“*”表示为匹配0至多个.

        string p = @"June       26     ,       1951    ";
        Match m = Regex.Match(p, @"(?[a-zA-Z]+)\s+(?\d{1,2})\s*,\s*(?\d{4}\s*)");
        if (m.Success)
        {
            Console.WriteLine(m.Groups["Month"]);
        }


        
        
    3、从Email中提取出用户名和域名,比如从[email protected]中提取出test和163.com。

        string p = @"June       26     ,       1951    ";
        Match m = Regex.Match(p, @"(?[a-zA-Z]+)\s+(?\d{1,2})\s*,\s*(?\d{4}\s*)");
        if (m.Success)
        {
            Console.WriteLine(m.Groups["Month"]);
            if (m.Groups["month"].Value == string.Empty)
            {
                Console.WriteLine("为空串");
            }
        }


        注意:如果尝试访问一个名为"month"的组时,返回的组的Value属性将是一个空字符
            串,而不是null。因此,在使用match.Groups["month"].Value之前,建议检查
            组的值是否为空字符串。
    
    
    4、“192.168.10.5[port=21,type=ftp]”,这个字符串表示IP地址为192.168.10.5的服务器
        的21端口提供的是ftp服务,其中如果“,type=ftp”部分被省略,则默认为http服务。请
        用程序解析此字符串,然后打印出“IP地址为***的服务器的***端口提供的服务为***”

        string p = @"192.168.10.5[port=21,type=ftp]";
        Match m = Regex.Match(p, @"(?(\d{1,3}\.){1,3}\d{1,3})\[port=(?\d{1,5})(,type=(?[a-zA-Z]+))?\]");
        if (m.Success)
        {
            if (m.Groups["type"].Value == "")
            {
                Console.WriteLine($"IP地址为{m.Groups["ip"]}的服务器的{m.Groups["port"]}端口提供的服务为http");
            }
            else
            {
                Console.WriteLine($"IP地址为{m.Groups["ip"]}的服务器的{m.Groups["port"]}端口提供的服务为{m.Groups["type"]}");
            }
        }


        
        
 

你可能感兴趣的:(C#进阶,c#,正则表达式,提取组,Regex,Matches,Match)