【4】通过简化的正则表达式处理字符串

阅读目录

  • 常见字符串操作
  • 使用正则表达式处理字符串
  • “前后限定”查找目标
  • 自动处理转义字符
  • 界定串的通用化
  • 多个目标的匹配
  • 进一步扩展
  • 结论

在各类应用软件的开发中,字符串操作是最常见的操作之一。在各种不同的数据类型中,字符串类型是和现实世界关联最紧密的。对字符串的读入、比较、拼接、搜索、匹配、替换、拆分等操作,是每个程序员必须要掌握的基本功。而C#的字符串处理,在历经了微软的多种开发工具的多年的积累后,达到了一个新的高度,概念上既简单明了,功能上又强大易用。大多数的字符串操作,都可以轻松应对。

常见字符串操作

在基本的字符串应用之外,还有一些复杂性相对较高的字符串应用。其中的很多类型出现的概率较高。从本人的经验出发,常常遇到这样一些典型的应用:

1、在较复杂的文本中查找符合某种规律的部分。常见的比如对HTML代码的解析,如要在以下HTML代码中查找所有的厂商及其链接地址:

<div class='blank10'></div> 
<a href='/enews/all.html' target='_blank'>厂商列表</a>&nbsp; 
<div class='w688 bor'> 
  <div class='wzgjz'><span>关键字:</span> 
       <a href='/enews/atlist0_1.html' target='_blank'>联想</a>&nbsp; 
       <a href='/enews/atlist0_2.html' target='_blank'>戴尔</a>&nbsp; 
       <a href='/enews/atlist0_3.html' target='_blank'>第二大PC厂商</a>&nbsp; 
       <a href='/enews/atlist0_4.html' target='_blank'>惠普</a>&nbsp; 
       <a href='/enews/atlist0_5.html' target='_blank'>PC</a>&nbsp; 
  </div> 
</div>

2、解析URL地址、文件路径等,如:

http://www.cnblogs.com/jetz/p/3727697.html
E:\MyCode\CommonCode\CommonCode\bin\x86\Release\CommonCode.dll

3、配置文件的解析,如某个配置文件Test.ini的内容如下:

[Options] 
Language=2052 
BackupPrompt=0 
HideWarnings=1 
MSG_CONFIRMCLEAN=False 
WINDOW_MAX=1

4、Excel复制的纯文本解析,如以下的Excel:

5、某些通信协议的解析:

ST=32;CN=2071;PW=123456;MN=88888880000001;CP=&&DataTime=20040506010101;101-Ala=1.1&&

这些解析,从技术角度来说,都不是很难的问题。但每次遇到此类问题,都要花时间去研究,去具体思考一些算法方面的问题,即使解决了,时间一长,下次遇到,还得继续重头做起!因此,本文主要尝试按照正常的解决思路,逐步找到简化这类处理的方案。

使用正则表达式处理字符串

如果使用C#自身的字符串功能来进行处理,效率较为低下。要高效地处理字符串,正则表达式是首选。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串,它的特点是:

1. 灵活性、逻辑性和功能性非常的强;

2. 可以迅速地用极简单的方式达到字符串的复杂控制。

3. 对于刚接触的人来说,比较晦涩难懂。

关于正则表达式的语法,有很多的途径可以学习,在此不再赘述。比如,要完成HTML代码的匹配,可以通过下面的正则表达式来实现:

MatchCollection mas = Regex.Matches(s, "href='(?<link>.*?)' target='_blank'>(?<name>.*?)</a>"); 
for (int i = 0; i < mas.Count; i++) 
{ 
    Console.WriteLine(mas[i].Groups["name"]+"  -  "+mas[i].Groups["link"].Value); 
}

上述的包含很多复杂字符的表达式就是正则表达式,尽管其效率高,但着实太不直观了。即使是对正则表达式用得较多的人,也不能快速地写出这个表达式。一般都是设置断点,在即时窗口中慢慢尝试,直到找到满意的表达式为止。上例的输出结果如下:

厂商列表  -  /enews/all.html 
联想  -  /enews/atlist0_1.html 
戴尔  -  /enews/atlist0_2.html 
第二大PC厂商  -  /enews/atlist0_3.html 
惠普  -  /enews/atlist0_4.html 
PC  -  /enews/atlist0_5.html

对于上面的协议文本,假如要获取其中的PW参数,也可以采用如下的正则表达式来完成:

Match mat = Regex.Match(s, "PW=(?<pw>.*?);"); 
if (mat.Success) 
{ 
    string pw = mat.Groups["pw"].Value; 
}

“前后限定”查找目标

通过前面的例子,不难看出,很多的文本操作,都可以归纳到这种模式下:在文本中查找某个子串,需要满足的条件是,该子串的前后应该分别是某两个指定的字符串。

前例的协议文本解析中,要找指定的参数的值,前面的串应该是“PW=”,后面的串是“;”,通过正则表达式的模式串“PW=(?<pw>.*?);”就可以找到了。

1、简化

对于这种常见的情况,能否进行简化呢?对于正则表达式的使用,本人的经验是:正则表达式最容易忘记的,是它的规则,以及各种各样的语言的细节。而处理的基本过程,如Regex.Match、Match.Success、Groups、Value等操作和属性,往往不容易忘记。因此,我的简化的原则是:

1)保持正则表达式的基本处理流程

2)对正则表达式的模式串进行简化

因此,可以通过一个函数,通过给出前后的字符串来构造一个正则表达式的模式串。

//根据前后内容返回字符串 
public static string  GetPatternString(string strHead, string strTear) 
{ 
    return "(?<head>"+strHead +")"+ "(?<body>.*?)" +"(?<tear>"+ strTear+")"; 
} 

为了便于控制,分别对匹配后的三部分命名为head、body、tear,可以使用类似Group["body"].Value的方式来访问三个部分。

测试该函数的结果为:

Console.WriteLine(RegexUtil.GetPatternString("PW=",";"));

输出结果为:(?<head>PW=)(?<body>.*?)(?<tear>;)

转义字符的处理

上述模式串的生成中,还有一个较大的问题,如果传递的前后限定字符串中包含一些正则表达式的特殊符号的话,则会带来歧义。正则表达式中,以下符号都是有特定含义的:

\<>.^${}|)*+?

如果要当作普通字符的话,需要在前面加“\”进行转义。这个小小的细节却是我比较烦的一个来源,因为在写正则表达式的时候,往往很难准确的记得究竟是哪些字符需要转义,因此每次都要去查手册。

因此,我们对于这些特殊符号,自动进行转义,去掉特殊化。这样,在大多数的情况下,我们不用考虑这些特殊符号了,任何符号都可以直接使用。

public static string CharTransfer(string str)  //转换为模式串 
{ 
    return str 
        .Replace(@"\", "\\x5C")    //优先替换\ 
        .Replace("<", @"\<") 
        .Replace(">", @"\>") 
        .Replace(".", @"\.") 
        .Replace("$", @"\$") 
        .Replace("^", @"\^") 
        .Replace("{", @"\{") 
        .Replace("[", @"\[") 
        .Replace("(", @"\(") 
        .Replace("|", @"\|") 
        .Replace(")", @"\)") 
        .Replace("*", @"\*") 
        .Replace("+", @"\+") 
        .Replace("?", @"\?") 
        ; 
} 

构造模式串的函数也相应的变化:

public static string  GetPatternString(string strHead, string strTear) 
{ 
    return "(?<head>" + CharTransfer(strHead) + ")" + "(?<body>.*?)" + "(?<tear>" + CharTransfer(strTear) + ")"; 
} 

测试中包含特殊字符“.”,结果将其转换为“\.”:

Console.WriteLine(RegexUtil.GetPatternString("1.", ";"));

输出:(?<head>1\.)(?<body>.*?)(?<tear>;)

界定串的通用化

现在已经可以达到任意指定前后界定串的程度了,但是,在实际应用中,往往有这种情况:假如前后的定界串存在一些细节上的差异,该怎么描述?

借鉴DOS/Windows中广为接受的通配符的做法,我们也可以定义一个通配符*,用来匹配任意文本。为了和普通的*区分,设定为“(*)”。这种模式可以看作是一种自定义的转义字符。对于“(*)”,可以转换为正则表达式的“.*?”,?的作用是惰性匹配,只要能够匹配,就以第一次的匹配结果作为结果。惰性匹配的模式能够更好的满足我们的需求。

因此,对于CharTransfer函数,就需要加上一个自定义的转义。当然,还需要考虑到相互间的逻辑关系和转换的先后次序,处理的结果如下:

private static string CharTransfer(string str)  //转换为模式串 
{ 
    return str 
        .Replace("(*)", "A_n_y_C_h_a_r_s")      //(*)-->A_n_y_C_h_a_r_s-->.* ,如果先替换.*会会被当成普通的字符,如果后*也会被当成普通字符 
        .Replace(@"\", "\\x5C")    //优先替换\ 
        .Replace("<", @"\<") 
        .Replace(">", @"\>") 
        .Replace(".", @"\.") 
        .Replace("$", @"\$") 
        .Replace("^", @"\^") 
        .Replace("{", @"\{") 
        .Replace("[", @"\[") 
        .Replace("(", @"\(") 
        .Replace("|", @"\|") 
        .Replace(")", @"\)") 
        .Replace("*", @"\*") 
        .Replace("+", @"\+") 
        .Replace("?", @"\?") 
        .Replace("A_n_y_C_h_a_r_s", ".*?") 
        ; 
} 

构造出模式串后,就可以进行匹配了。正则表达式的匹配结果可以返回单个匹配和匹配集合。前者用Match方法,后者用Matches方法。本人在应用中,往往喜欢使用后者,因为后者是可以包含前者的,这种思路在JQuery中也得到了体现,默认情况下,返回的结果都是集合。

多个目标的匹配

前述的匹配中,每次匹配,目标往往只有一个。加入需要同时匹配多个目标呢?如Excel的文本的匹配,每个单元格都以\t分隔,行间以\r\n分隔。借鉴前面的通用化思路,也可以构造出一个串,直接进行匹配。因此,对GetPatternString进行了重构,如下:

public static string  GetPatternString(string strHead, string strTear) 
{ 
    return "(?<head>" + CharTransfer(strHead) + ")" + "(?<body>.*?)" + "(?<tear>" + CharTransfer(strTear) + ")"; 
} 

通过多项匹配,处理过程如下:

string s = @" 
学号    姓名    成绩    班号 
951001    李小明    76    11 
951002    赵    林    87    21 
951003    李小敏    85    11 
951004    陈维强    50    31 
951005    林玉美    53    11 
951006    陈    山    96    21 
951007    江苏明    85    11 
951008    罗晓南    67    31 
"; 
MatchCollection mas = Regex.Matches(s, RegexUtil.GetPatternString("(*)\t(*)\t(*)\t(*)\r\n")); 
for (int i = 0; i < mas.Count; i++) 
{ 
	Console.WriteLine(mas[i].Value); 
} 

结果输出为:

951001    李小明    76    11
951002    赵    林    87    21 
951003    李小敏    85    11 
951004    陈维强    50    31 
951005    林玉美    53    11 
951006    陈    山    96    21 
951007    江苏明    85    11 
951008    罗晓南    67    31 

上述例子中,为便于处理,对于每个节点最好也能命名。但由于数量不定,因此只能采用用户自行命名的方式。对此,我们设定规则如下:

(*name*):表示任意字符串,匹配后,其分组命名为name。

那么,对于GetPatternString中,需要对这种表示进行解析。

public static string GetPatternString(string str) 
{ 
	//先处理(*),防止干扰 
	string mypattern = str.Replace("(*)", "A_n_y_C_h_a_r_s"); 
	//先把(*name*)命名全部替换为一个不含特殊字符的串 L_e_f_t...R_i_g_h_t 
	string pat = GetPatternString("(*", "*)"); 
	mypattern = Regex.Replace(mypattern, pat, delegate(Match mat) { 
		return "L_e_f_t" + mat.Groups["body"].Value + "R_i_g_h_t"; } 
		);
	mypattern = mypattern.Replace("A_n_y_C_h_a_r_s", "(*)"); 
	//再把(*)替换回去,统一转正则表达式 
	mypattern = CharTransfer(mypattern); 
	//将特殊字符转换为正则表达式的转义字符 
	//将命名串换回符合正则表达式的样式 
	string pat2 = GetPatternString("L_e_f_t", "R_i_g_h_t"); 
	mypattern = Regex.Replace(mypattern, pat2, delegate(Match mat) { 
		return "(?<" + mat.Groups["body"].Value + ">.*?)"; } 
		); 
	return mypattern; 
}

调用命名模式处理如下:

MatchCollection mas = Regex.Matches(s, RegexUtil.GetPatternString("(*)\t(*xm*)\t(*cj*)\t(*)\r\n"));
for (int i = 0; i < mas.Count; i++) 
{ 
	Console.WriteLine(mas[i].Groups["xm"].Value+" - "+mas[i].Groups["cj"].Value); 
} 

结果输出为:

姓名 - 成绩 
李小明 - 76 
赵    林 - 87 
李小敏 - 85 
陈维强 - 50 
林玉美 - 53 
陈    山 - 96 
江苏明 - 85 
罗晓南 - 67 

进一步扩展

1、返回字符串数组。这个简化的意义有限。

2、重写一个Matches。意义也有限,因为核心在模式串。

3、构造串时,加入正则表达式的规则。在实际应用中,也有这样的需求,如无法定位结尾,命名的部分需要指定模式等。但是,综合考虑到设计初衷,还是放弃。

对于需要特定处理的,可以对返回的串进行进一步的修改加工。如,对于配置文件的读取:

[Options] 
Language=2052 
BackupPrompt=0 
HideWarnings=1 
MSG_CONFIRMCLEAN=False 
WINDOW_MAX=1 

采用以下代码处理:

string s = AccessFile.ReadFile(Application.StartupPath+"\\test.ini"); 
MatchCollection mas = Regex.Matches(s,RegexUtil.GetPatternString("(*name*)=(*value*)\r\n")); 
for (int i = 0; i < mas.Count; i++) 
{ 
	Console.WriteLine(mas[i].Groups["name"].Value+":"+mas[i].Groups["value"].Value); 
} 

输出结果为:

Language:2052 
BackupPrompt:0 
HideWarnings:1 
MSG_CONFIRMCLEAN:False 

可以看出,最后一行没有被解析出来。因为最后一行的结束标志不是回车。

如果对正则表达式比较熟悉的话,完全可以进行修改:

string s = AccessFile.ReadFile(Application.StartupPath+"\\test.ini");
string pat=RegexUtil.GetPatternString("(*name*)=(*value*)\r\n");
pat=pat.Replace("\r\n","(\r\n|$)");
MatchCollection mas = Regex.Matches(s,pat);
for (int i = 0; i < mas.Count; i++)
{
	Console.WriteLine(mas[i].Groups["name"].Value+":"+mas[i].Groups["value"].Value);
}

输出结果如下:

Language:2052 
BackupPrompt:0 
HideWarnings:1 
MSG_CONFIRMCLEAN:False 
WINDOW_MAX:1

结论

对于常见的字符串操作,正则表达式是一个强有力的工具。但由于其规则的复杂,不便于在常规情况下快速运用。本文提出了一套简化的规则,屏蔽了正则表达式的细节,降低了正则表达式的使用难度:

规则1:通过指定前后定界字符串,自动生成需要的正则表达式。函数:CommonCode.RegexUtil.GetPattern(s1,s2)

规则2:使用“(*)”代替任意字符,其他所有特殊字符都去特殊化,可以随意使用无需考虑转义

规则3:使用“(*name*)”来表示命名分组的任意串

规则4:通过CommonCode.RegexUtil.GetPattern(s),可以返回通用的模式串

规则5:GetPattern的两个重构函数中,参数中都可以使用(*)和(*name*)

规则6:考虑到复杂性,不支持更复杂的正则表达式,如有需要,可以获取模式串后,对其进行进一步的加工

demo页面

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jimoer

Java设计模式学习记录-模板方法模式

模板方法模式,定义一个操作中算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

1864
来自专栏進无尽的文章

设计模式| 行为型模式 (上)

行为型模式共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、解释器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式。 分两篇文...

1642
来自专栏腾讯技术工程官方号的专栏

你需要认真对待warning,不然......

作者介绍:olivialu,架构平台部质量组员工,测试妹子两年来摸爬滚打、踩坑无数,努力从细微处发现问题、提升测试思路和方法,通过构建测试工具,让每个BUG无所...

2358
来自专栏我是攻城师

玩转大数据系列之Apache Pig高级技能之函数编程(六)

3513
来自专栏java学习

Java每日一练(2017/6/22)

Java基础 | 数据库 | Android | 学习视频 | 学习资料下载 最新通知 ●回复"每日一练"获取以前的题目! ●【新】Ajax知识点视频更新了!(...

35412
来自专栏听雨堂

字符串处理总结(旧)

在各类应用软件的开发中,字符串操作是最常见的操作之一。在各种不同的数据类型中,字符串类型是和现实世界关联最紧密的。对字符串的读入、比较、拼接、搜索、匹配、替换、...

2588
来自专栏互扯程序

设计模式不止23种!

现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享。

1304
来自专栏喔家ArchiSelf

一个函数的自白

我是——编程世界的函数,不是数学中的幂,指,对和三角函数等等,但是和f(x)又有着千丝万缕的关系。

1415
来自专栏java思维导图

Getter & Setter:使用还是废弃

私有变量 为什么我们要使用私有的实例变量呢? 因为我们不希望其他类直接的依赖于这些变量。而且在心血来潮时,我们还可以灵活的修改变量类型和实现。 然而,为什么程序...

2926
来自专栏V站

php时间函数 time()和Date()详解

使用函式 date() 实现  <?php echo $showtime=date("Y-m-d H:i:s");?>  显示的格式: 年-月-日 小时:分...

8746

扫码关注云+社区

领取腾讯云代金券