【C#】正则表达式总结 看不懂你打我~~
背景
正则老是忘记啊,不总结不行啊!这里我不做特别全面的介绍,但是会介绍比较有用的点。围绕一个例子来讲解!然后逐步优化。跟着例子尝试,你一定能看懂!
例子
原始字符串
string xml_str = @"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
xmlns:xsd=""http://www.w3.org/2001/XMLSchema""
xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
<soap:Body>
<UniRequest xmlns=""http://tempuri.org/"">
<rb>
<account>admin</account>
<optype>0</optype>
<param>{""moid"":""WO09280001"",""partID"":""3F258-A-MPWN"",""ppid"":""9908108383X2009280100000301
"",""testStation"":""T00001""}</param>
<password>123</password>
<sericeName>GET_PROCESS_STATUS</sericeName>
</rb>
</UniRequest>
</soap:Body>
</soap:Envelope>
";
最初的目标
我想把<param></param>标签中的字符串匹配出来!正则表达式重在匹配二字,后面多多体会。
给出程序:
string param1 = Regex.Match(xml_str, "<param>.*").Value;
- xml_str为原始字符串
- <param>.* 为正则表达式
- Value是匹配到的内容
看看能匹配得到什么?
<param>{"moid":"WO09280001","partID":"3F258-A-MPWN","ppid":"9908108383X2009280100000301
<param>.* 表示,我匹配的字符串是以<param>开头的,
其中的“.” 表示可以匹配任何单个字符,但是不包括换行
其中的“*” 表示 匹配前面的子表达式零次或多次。(要匹配 * 字符,请使用 \*)。
所以 .* 的组合,就能匹配任意多个任意字符!但是如果遇到换行,匹配就打住了。而在这个例子中</param>换行了,所以我们需要多行匹配!
什么是重复
重复这个观念,非常重要,贯穿整个正则的基础逻辑,这里解析一下:
代码/语法 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
点号本身只能匹配一个字符,但是加上*号之后,就能匹配无数个字符!
"重复" 他就像一个驱动器,驱动着匹配继续进行!
“问号”在正则中,扮演连个角色:
1 匹配前面的子表达式零次或一次,
2 指明一个非贪婪限定符。(懒惰限定符 中介绍)
ps(如要匹配 ? 字符,请使用 \?。)
升级:多行匹配
很简单,将 . 替换为 (.|\n) ,现在表达式变成了:
<param>(.|\n)*
string param1 = Regex.Match(xml_str, "<param>(.|\n)*").Value;
看看能匹配得到什么?
<param>{"moid":"WO09280001","partID":"3F258-A-MPWN","ppid":"9908108383X2009280100000301
","testStation":"T00001"}</param>
<password>123</password>
<sericeName>GET_PROCESS_STATUS</sericeName>
</rb>
</UniRequest>
</soap:Body>
</soap:Envelope>
(.|\n) 其实就是说,我要匹配任意字符,包括换行!这样就能一直匹配到字符串结束了!那这也不是我想要的
(.|\n) 有时会写成 (?:.|\n),表示不分组,因为带了括号就表示分组了,?:表示取消分组,这样能提供匹配效率。这个暂时不管啦!
添加结束匹配
<param>(.|\n)*</param>
这样很明显,我能就能得到:
<param>{"moid":"WO09280001","partID":"3F258-A-MPWN","ppid":"9908108383X2009280100000301
","testStation":"T00001"}</param>
以某字符开头和结尾但不包含本身的匹配
但是,我还是不满足,<param></param> 这对标签也不是我想要的!我只要中间的内容。
看下这张图里的红框部分的解释,它能满足我的需求。
于是,正则表达式最终进化为:
(?<=<param>)(.|\n)*(?=</param>)
string param1 = Regex.Match(xml_str, "(?<=<param>)(.|\n)*(?=</param>)").Value;
我们得到了标签里的json:
{"moid":"WO09280001","partID":"3F258-A-MPWN","ppid":"9908108383X2009280100000301
","testStation":"T00001"}
贪婪与懒惰
贪婪
新的需求,我需要取得json中,ppid的值,思路不变,以"ppid" 开头,以引号结束。试试:
string param1 = Regex.Match(xml_str, "\"ppid\"(.|\n)*\"").Value;
注意:引号本身需要转义。
我们得到的匹配内容是:
"ppid":"9908108383X2009280100000301
","testStation":"T00001"
我的说的是以引号结束,这里匹配到了字符串中最后一个引号,这就是贪婪。
匹配到第一个引号时候,此时并不会停止匹配,因为引号也是任意的字符!符合匹配规则。所以重复过程继续进行,后面还有引号所以不会停止!直到最后一个引号才会停止!
懒惰
string param1 = Regex.Match(xml_str, "\"ppid\"(.|\n)*?\"").Value;
不贪婪,就是在 * 的后面加个 ?,让你扪心自问,是不是太贪了?于是得到:
"ppid":"
懒惰:就是在能使整个匹配成功的前提下使用最少的重复。
这里先说明,有问号的情况就是懒惰,懒惰就是保证重复的次数最少。
懒惰限定符
这里我们对比常规限定符和懒惰限定符,限定符多了一个问号!问号是懒惰的标识!
把 *? 替换成 {2,}?
string param1 = Regex.Match(xml_str, "\"ppid\"(.|\n){2,}?\"").Value;
得到结果:
"ppid":"9908108383X2009280100000301
"
这里2的意思是,先重复两次再说!(重复就是驱动器)
从哪里开始重复?当然是"ppid"后面,重复的这两次相当于匹配了两个字符(冒号和引号)!
相当于把这两个字符跳过了!然后再执行懒惰!由于第二个数字为空({2,})
表示重复次数没有上限,就会一直往后重复!直到满足:
整个匹配成功的前提下使用最少的重复。
为了说明这点,我再举个例子!因为,这个次数问题以前都理解错误了:
如果,原始数据是:
"2113111311311111134444444444444"
我想匹配到第三个3结束也就是:
"21131113113"
哪一段代码可以实现要求?
A:
string param1 = Regex.Match("2113111311311111134444444444444", "2(.|\n){3,}?3").Value;
B:
string param1 = Regex.Match("2113111311311111134444444444444", "2(.|\n){7,}?3").Value;
如果选择对了,说明你理解对了这个重复次数的含义!
答案是B!
首先,我要匹配到第三个3,而不是最后一个,说明我需要用懒惰模式。
其次,因为是第三个3,所以我得强行跳过前面2个3,那我就先强行重复7次,跳过
前面两个三好了!
正则实现替换
替换,使用Regex.Replace,比Regex.Match 多一个替换参数。替换就是把匹配的内容,替换成新的内容:(这里的xxx就是要替换的内容,是个普通字符串)
//替换
string re = Regex.Replace(xml_str, "<param>(?:.|\n)*</param>", xxx);
正则实现计数
这里计算双引号的个数:注意使用的是 Matches
Regex.Matches(param, "\"").Count;
匹配判断
Regex.IsMatch(xml_str, "\"ppid\"")
判断正则表达式是否能匹配到内容,返回一个bool变量。
添加重复小技巧
如果掌握,这个小技巧,你将随心所欲的写出你想要的正则表达式。
需求说明
我有一个json数字,我想通过正则,告诉key就能取出对应的value。
首先,按思路写出:
Match match = Regex.Match(xml_str, $"(?<=\"{key_name}).*?:");
这样的话,匹配到key然后匹配到冒号,重复就结束了,没有动力了!
得到:
":
所以需要 添加重复过程,添加动力(这是个技巧)!继续往后匹配:
Match match = Regex.Match(xml_str, $"(?<=\"{key_name}).*?:.*?\"");
得到:
":"
再继续添加重复:
Regex.Match(xml_str, $"(?<=\"ppid).*?:.*?\"(.|\n)*?\"")
得到:
":"9908108383X2009280100000301
"
分组
如果,再弄懂分组,你无敌了。
那上面的结果,虽然是匹配到了,但是多了一些东西,我只想要这串码值。此时就可以借助分组来获取,分组就是括号,把自己想要的部分括起来。我加个括号看看。
Regex.Match(xml_str, $"(?<=\"ppid).*?:.*?\"((.|\n)*?)\"");
此时就多了个分组:
然后通过:
match.Groups.Values.ElementAtOrDefault(1)?.Value;
就能拿到一号分组的值!
目前测试来看,第一个分组(Groups[0])就是整个匹配的结果,整个分组是无法取消的。
取消分组
这种情况下,我有四个分组:
Regex.Match(xml_str, $"(?<=\"ppid)(.*?:.*?\")((.|\n)*?)\"");
通过 ?: 取消分组,提高效率:
Regex.Match(xml_str, $"(?<=\"ppid)(?:.*?:.*?\")((?:.|\n)*?)\"");
此时就只有两个分组了。
分组命名
var param1 = Regex.Match(xml_str, $"(?<=\"ppid)(.*?:.*?\")(?<rname>(?:.|\n)*?)\"", RegexOptions.ExplicitCapture);
?<rname>,给分组命名!
RegexOptions.ExplicitCapture,表示仅仅捕获带有显示命名的分组,其他分组相当于,通过 ?: 取消分组。
RegexOptions
还有其他的选项,如下:
RegexOptions.IgnorePatternWhitespace
这个就是增加可读性,你得留白会被忽略,而且可以添加注释。(但是,我测试时,这个选项影响了我的结果)
附送三个功能函数
函数体
//获取标签中的内容
string? GetP(string str, string p_name)
{
if (!str.Contains(p_name))
{
return null;
}
return Regex.Match(str, $"(?<=<{p_name}>)(.|\n)*(?=</{p_name}>)").Value;
}
//json根据key获取value的值
string? GetJValue(string str_json, string key_name)
{
if (!str_json.Contains(key_name))
{
return null;
}
Match match = Regex.Match(str_json, $"(?<=\"{key_name}\"\\s*:\\s*\")((.|\n)*?(?=\"))");
return match.Groups.Values.ElementAtOrDefault(1)?.Value;
}
//json替换key对应的value的值
string? ReplaceJValue(string str_json, string key_name, string replacement)
{
if (!str_json.Contains(key_name))
{
return null;
}
return Regex.Replace(str_json, $"(?<=\"{key_name}\"\\s*:\\s*\")((.|\n)*?(?=\"))", replacement);
}
测试过程
Console.WriteLine("-----------------------------------------------------");
Console.WriteLine($"{GetP(xml_str, "sericeName")}");
Console.WriteLine($"{GetJValue(xml_str, "moid")}");
Console.WriteLine($"{ReplaceJValue(xml_str, "ppid", "89898988989898989")}");
结果展示
常用符号
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
小结
本来没准备写这么详细的,结果写着写着就情不自禁的刨根问底,之前不懂的地方也彻底搞懂了,以后正则就是小case!希望也能帮到你!
参考资料
特别鸣谢这位作者,提供的全面资料。
作者:宋桓公
出处:http://www.cnblogs.com/douzi2/
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。