为了实现数据库与EXCEL导入导出功能,需要写一个EXCEL访问类,遇到单元范围地址处理的问题。考虑几天最终决定用正则表达式来判断单元格行列地址字符串。网上找了半天也没找到相关详细的描述,只能自己DIY。
这天书一样的规则实在令人却步,不过考虑到掌握之后前途无量,以前很多字符串解析都是用程序循环逻辑判断的方法实现,用正则表达式会变得很简单。花了2天时间研究正则表达式,终于实现了要求。在研究过程中还了解到小括号在正则表达式中的作用。把研究结果写出来的目的也是为了复习一遍以加深印象,同时也做个备忘。
先给出EXCEL单元及范围地址的正则表达式:
单地址:
(^(\$?)[a-zA-Z]+\2[0-9]*$)|(^\$?[0-9]+$)
范围地址:
((^(\$?)[a-zA-Z]+\3[0-9]*)|(^(\$?)[0-9]+)):(((\3)[a-zA-Z]+\3[0-9]*$)|(\5[0-9]+$))
并编写相应的方法,C#中要注意“\”符号的转义,要写成“\\”。
下面是C#实现方法。
using System.Text.RegularExpressions;
//其他代码略
//匹配单地址 public static bool IsXlsCell(string input) { return Regex.IsMatch(input, "(^(\\$?)[a-zA-Z]+\\2[0-9]*$)|(^\\$?[0-9]+$)"); } // 匹配范围地址 public static bool IsXlsRange(string input) { return Regex.IsMatch(input, "((^(\\$?)[a-zA-Z]+\\3[0-9]*)|(^(\\$?)[0-9]+)):(((\\3)[a-zA-Z]+\\3[0-9]*$)|(\\5[0-9]+$))"); }
分析并创建基本零件:
以单地址为例。在EXCEL中单个单元格地址的格式由行列参数组成,格式是“$列号$行号”,例如“$A$10”,也可以省略"$",如“A10”,该字符串表示1列10行的单元格。两种格式EXCEL都支持。然后分析单个单元格字符串的其他两种情况。EXCEL中另两种特殊情况是单列和单行,格式是“$列号”和“$行号”,比如“$A”和“$10”,或者“A”和“10”,分别表示列号为1的整个列和行号为10的整个行。
(EXCEL中的对象的方法参数顺序与之相反“CELL[int row,int col].Value”,是行在前列在后。而地址描述中字母总是表示列号,数字总是表示行号,有关字母转换成数值另外写文章说明。)
经过如此分析,可见单个单元地址就三种情况,外加“$”符号组合共6种组合。即“[$][COLUMN][$][ROW]”,这个就是模型,而且列只能是字母(不区分大小写),行只能是数字,方括号代表可省略。虽然这个写法不是很准确,但是基本已经能看出点正则表达式的影子了,也清楚地看出,除了"$"符号的修饰,单元地址的组成就是两部分,列项与行项。
下面一步步来组装基本的正则表达式,为了条理清晰,先给出每一项的结果,每一项你可以理解为一个零部件。
- “$”符号的正则表达式“\$?”
“$”符号可有可无,而为了完整性也不能丢下它不管,也为了字符串的美观,更是为了后面测试正则表达式中小括号的缓存作用,我做了这样的规则,一旦第一个匹配项带“$”,后面的匹配项必须有,否则视为无效,即不匹配(简直多此一举,哈哈,其实在实际应用时,匹配之前可以先Replace掉)。匹配单个可有可无的符号是“\$?”,由于“$”在正则表达式中式结束限定符,所以必须要转义,转义符和C-LIKE语言一样,都是“\”符,后面的“?”代表之前的匹配项匹配0个或1个,即可有可无啦。
- COLUMN列的正则表达式的基本格式“[a-zA-Z]”
“[a-zA-Z]”表示匹配一个大写或小写的字母,注意,只是匹配一个字母,方括号对里面是范围,这里限定了匹配字母范围,即小写的a到z和大写的A到Z,z和A之间不能有其他字符,包括空格。好了,现在进行扩展,列号可能是多个字母组成,因此在方括号后面加个限定符,如果需要匹配1个或连续多个,用“+”符号,即“[a-zA-Z]+”,如果要匹配0个或多个,用“*”符号,即“[a-zA-Z]*”。
- ROW行的正则表达式的基本格式“[0-9]”
这个就不用说了,和上面一样,0-9是限定范围,意思就是匹配单个数字符号,同理,要匹配1个或多个数字符号就是“[0-9]+”,要匹配0个或多个数字符号用“[0-9]*”。
小结一下,“+”和“*”都是用来限定前面的匹配项的,你可以理解“+”为至少有一个,同样可以理解“*”为可以是连续多个,也可以没有。
组装:
以上三项是单元地址匹配所需要的主要零件,为了描述方便,这里为这些零件按顺序分别称为X、Y、Z作为代号,下面开始组装。
从前面的分析看,单个单元格地址有三种情况,列行(单个单元格)、单列、单行,下面一个个来。
- 单个单元格的正则表达式“^\$?[a-zA-Z]+\$?[0-9]+$”
这种情况下,列字母之前只允许出现“$”或没有其他字符,因此需要用到“^”开始限定符,该符号代表右边的匹配项是字符串开始,不存在其他符号,即^X,也就是在第一个零件前面加个“^”,展开就是“^\$?”。注意这里的“?”的含义。接下来就是列字母序列,那么就是^XY,而且在单个单元格地址中列字母必须有一个且可能为多个字母,因此后面加个“+”,即^XY+,展开就是“^\$?[a-zA-Z]+”。再来组装行的表达式零件,代码是Z,好吧,那就是^XY+Z,等等,好像少了什么,对了,数字前面可能还有个“$”要匹配,插入之^XY+XZ,在这里行的数字符号也是必须有一个且可能为多个数字符号,因此后面也加个“+”,即^XY+XZ+,然后,对了,数字后面不能有其他非数字符号,加个结束限定符“$”(前面说过),加上后就是“^XY+XZ+$”,展开后就是“^\$?[a-zA-Z]+\$?[0-9]+$”。
- 单列的正则表达式“^\$?[a-zA-Z]+$”
这个就简单了,根据上面的说明,很容易组装起来,“^XY+$”,展开“^\$?[a-zA-Z]+$”,就这么简单。
- 单行的正则表达式“^\$?[0-9]+$”
这就不用说了,“^XZ+$”,展开“^\$?[0-9]+$”。
注意X,Y,Z都代表上面三个基本零件,像做代数一样要代入表达式。而“^”符号代表开始,“$”符号代表结束,如果不加,就无法屏蔽掉非法字符,像“12AB34”这样的字符串,就也会认为单元格地址匹配,所以是不允许的。
到此为止似乎是完成了,但是这样要写三个方法分别去匹配三种情况,而且判断时要分三次调用,这可还不够完美也很麻烦,需要进行整合优化。
优化:
为了方便理解,还是用代数式来描述,我们已经有了三个对应不同情况的表达式,他们是“^XY+XZ+$”、“^XY+$”、“^XZ+$”,可以看出,后两种分别是前一种的子集。
去掉头尾限定符,进行集合运算。
交集
“XY+XZ+” ∩ “XY+” =“XY+”。单列与单元格交集。
“XY+XZ+” ∩ “XZ+” =“XZ+”。单行与单元格交集。
补集(忘记什么符号了,不管了用减号代替着)
“XY+XZ+” - “XY+” =“XZ+”。单列与单元格补集。
“XY+XZ+” - “XZ+” =“XY+”。单行与单元格补集。
规则:左对齐,先看单列与单元格的关系。可以看出单列和单元格的交集的一部分“XY+”,补集是“XZ+”,不同之处是行部分,对于单列来说行号是缺失的,可以没有的。因此把“+”号换成“*”号,表示可以没有行。
加上头尾限定符,记为:“^XY+XZ*$“,这样即可以匹配单元格也可以匹配单列。
一下子解决了2种情况,还有一种单行情况只能单独列出,即"^XZ+$"。
然后组合“(^XY+XZ*$)|(^XZ+$)”,恩,不错,经过这样子优化,应该是最优了,好了可以做代数了,代入扩展开来:
"(^\$?[a-zA-Z]+\$?[0-9]*$)|(^\$?[0-9]+$)"
感觉差不多了,最后强制"$"符号一致性(发烧一下)。由于"?"代表可有可无,因此“$A10”的单个单元格地址也能匹配,但是为了测试小括号的缓存机制,要求统一使用“$”符号,要么都不带,要么都带。
把匹配“$”符号的匹配项加括号(这里不涉及单列和单行的情况),见标红色部分:
"(^(\$?)[a-zA-Z]+(\$?)[0-9]*$)|(^\$?[0-9]+$)"
无论前面一个匹配是否成功,要求后面一个要与之一致,根据正则表达式规则,小括号都会做匹配结果缓存,“\n”可以引用缓存中的结果,其中n代表缓存号,缓存号从1-99编号,按层次按先后编号,估计就是堆栈的原理。那么把括号分解看看第一个匹配项是几号。
( //缓存1# ^ (\$?) //缓存2# [a-zA-Z]+ (\$?) //缓存3# [0-9]*$ ) | (//缓存4# ^\$?[0-9]+$ )
这样清楚了,第一个“$”匹配项是2#缓存,OK,动个小手术,见红字,把“\$?”改成“\2”:
"(^(\$?)[a-zA-Z]+(\$?)[0-9]*$)|(^\$?[0-9]+$)"=>"(^(\$?)[a-zA-Z]+\2[0-9]*$)|(^\$?[0-9]+$)".
最后加上C#的转义符"\",这就是最终单地址的正则表达式。
"(^(\\$?)[a-zA-Z]+\\2[0-9]*$)|(^\\$?[0-9]+$)"
范围地址和此类似,就是结构上更复杂些。以上表达式在VS2008的C#中调试通过,无BUG。