2012/12/14

关于在DELPHI6中使用正则表达式的一些心得

转载地址:
http://www.delphibbs.com/keylife/iblog_show.asp?xid=25712
http://blog.csdn.net/senfore/article/details/2154944

一. 工具选择

DELPHI6本身没有处理正则表达式的库,只能找第三方库。在选择方面,我的标准是:

1. 不必向开发环境注册控件
2. 接口简单
3. 符合主流的perl式正则语法
4. 专业,至少应该有个看上去专业的专门网站
5. 免费

目前符合以上标准, 比较好的RegEx类库有TRegExpr ( http://www.regexpstudio.com/ )与PerlRegEx ( http://www.regular-expressions.info/ )。

TRegExpr 是俄国人做的RE类库,应该说是目前国内最主流的免费RegEx类库了。纯DELPHI写成,支持中文,可以选择安装为开发环境控件,也可直接作为类库单 元使用(只有一个主类,一个pas文件)。美中不足是自从2004年后就没有更新了,版本一直是0.9xxx,就是不上1。而且不支持 Lookaround语法(前瞻与回溯功能)

PerlRegEx底层是用C的类库,完全符合PCRE标准(兼容Perl的正则表达式)。 文件结构比TRegExpr复杂一点,包括一个放底层obj文件的子目录和两个接口.pas文件,实际使用时只需要向项目中添加一个单元(当然也可以注册 成为控件)。之前的版本据说对中文支持不够,最新版本我在中文环境下用倒没遇到什么问题。说明文档也声称支持Unicode。

现在我是常 备两个这两个类库,但主要还是用PerlRegEx。除了迷信最后更新日期与C的执行效率外。还看中了RegexRubby这个基于同一套C类库的 RegEx编写工具,以及PerlRegEx提供了一个study方法,声称可以对正则式做点前期编译,提高执行效率。

=========================================================================
二 使用方法:

解 压了PerlRegEx包后,如果不想注册控件,除了PerlRegEx.pas、CHelpers.pas和PCRE目录外,其他的东西可以建个隐藏目 录搁置起来(没认真阅读用户协议,不知道能不能随便删。。。)。维持这两个文件与PCRE的目录结构不要变。使用时只需要把PerlRegEx.pas添 加进项目,在单元中uses PerlRegEx就可以了。

PerlRegEx提供了TPerlRegEx类。主要用法是:

RegEx : TPerlRegEx;
....
RegEx := TPerlRegEx;
try
  RegEx.Subject := '要匹配的正文';
  RegEx.RegEx := '正则表达式';
  if RegEx.Match then ....
finally
  RegEx.free
end;

如果要多次匹配并做一些处理,可以:
Matched : boolean;
....
RegEx.Match;
while RegEx.FoundMatch do
begin
  ....
  RegEx.MatchAgain;
end;

如果要替换匹配到的内容,可以
RegEx.Subject := '要匹配的正文';
RegEx.RegEx := '正则表达式';
RegEx.Replace := '替换的字符串'
if RegEx.Match then RegEx.ReplaceAll;  //结果在RegEx.subject
或者 if RegEx.Match then Result := RegEx.Replacement;

匹配到的字符串放在RegEx.MatchedExpression中,长度在RegEx.MatchedExpressionLength中,上一次匹配的结束位置在RegEx.Stop中

匹配到的子串放在RegEx.SubExpressions[i]中,子串个数在RegEx.SubExpressionCount中。

如果正则式很复杂而且常用,可创建一个生存期相对长的TPerlRegEx实例.设置好RegEx属性后,使用.Study方法对正则表达式进行预处理.据帮助文档说,文档资料会大大提高效率.

详情可参考文档。有一点文档上没有提到(又或者我看漏了),在第一次匹配之后,如果没有重新赋值subject,下一次匹配无论用Match或者MatchAgain,都是从上次的结束位置开始。所以如果要重新开始匹配,应先把RegEx.Start := 0;

==========================================================================
三. 使用和编写正则表达式要点

使用正则表达式,通常是用作三种用途:校验字符串,提取信息,处理字符串.

当 用作校验时,通常是对正文整体校验,例如通常是判断正文是否正确的邮件地址,而不是判断正文是否含有正确的邮件地址.因此应在正则表达式的两端加上行开始 锚点^与行结束锚点$.如果待校验的文字允许两端有空格,则应该在锚点前后用' *'或'/s*'(允许空格与TAB)匹配进去.

设计正则表达式的要点在于分段.对要匹配的内容分好段,就能够容易地各个击破.通常在要匹配的文字中会有一些分段的提示,例如逻辑上的单位、重复出现的模式或者不能连续重复出现的字符(串)。

以设计校验输入数字的正则条件为例,可以先列出符合条件的情况:
1234 / 12.34 / -12.34 / 12.3e4 / 12.3e-4 / .12E-34
可以看出,逻辑上的单位有:符号,整数部分,小数点,小数部分,e(或E),指数符号,指数部分
技术上的分段标志有:

符号:在开始与E后面各可能出现一次
小数点:只能出现一次,若出现,其后必须有小数部分。
e:只能出现一次,若出现,其后必须有指数部分。

所有“若出现,其后必须有。。。”的都可以考虑分为一组。可得初步方案: [+/-]?/d*(/./d+)?([Ee][+/-]*/d+)?

但 这个设计有问题,前半段的 /d*(/./d+)? 是可以匹配空串的,而需求是如果有整数部分,则小数部分可选。如果无整数部分,则必须有小数部分,直观的做法是改为(/d+(/./d+)?|/. /d+)。再认真观察一下,可以发现这个选择式无论任何情况,都是以/d+结尾,而我们实际上并不关心这个/d+是匹配到整数部分还是小数部分,至于前面 的小数点与整数部分都是可选的。因此,这部分可以改写为 /d*/.?/d+

所以最终的校验式是:^ *[+/-]?/d*/.?/d+([Ee][+/-]*/d+)? *$

使 用正则表达式提取信息是一个难点,但也是体现正则表达式强大实力的一个方面。提取信息的正则表达式必须要考虑四个方面:不误判(应该有一定语法检验能 力),不漏判,子串能匹配到正确位置。一些结构复杂或具有循环结构的正文,可能需要多次处理或使用开发语言的循环结构来辅助提取。具体技巧我现在还觉得比 较模糊,以下仅举几个例子:

1. 查找并分析 XX1>XX2,<,=,!,空格,TAB符号的任何字符串,式子两端与元素之间允许有空格或TAB

由 于这个不等式可能在上下文中,我们需要先在正文中把合语法的不等式隔离出来,否则下面的循环部分就会匹配到下一条不等式的部分。在这个例子中还算简单,找 到 '/b[^<>=!/s]+((不等符号)[^<>=!/s]+)+/b' 就可以了(其中不等符号在下面解释)。但这样只能匹配到整个式子而不能分别提取式子中的子串信息,当出现(...)+时,对应子串内容是只是最后一个匹配 到的串。

对每个匹配到的结果,因此这里需要分开两次提取,第二次需要使用循环来辅助。

先取最开始的子串,这个很容易,直接 '^/s*([^<>=/s]+)'就可以了,注意^ *是为了去掉开头多余的空格与TAB。真正的XX1在匹配到的子串1中。

接 着开始分析 '((不等符号)[^<>=!/s]+)+' 部分。整个(...)+结构需要在外部用开发语言的循环来逐次提取。先列出合法的不等符号:>=,>,=,<=,<,<& gt;,==,!>,!<,!>=,!<=,!=。因此不等符号部分应该是(!?>=?|!?& lt;=?|!?==?|<>)。

因此,要分析篇正文,就需要:
1)A匹配 '/b[^<>=!/s]+(/s*(!?>=?|!?<=?|!?==?|<>)/s*[^<>=!/s]+)+/b' 找出不等式。
2)    对每个A匹配结果, B匹配'^/s*([^<>=!/s])',提取子串1记录为变量名称。
3)    接着B匹配'/s*(!?>=?|!?<=?|!?==?|<>)/s*([^<>=!/s]+)',提取子串1记录为符号,子串2记录为变量名称。
4)    从3)开始循环直到找不到B匹配结果
5)从1)开始循环直到找不到A匹配结果

2. 提取电话号码

这 是不久前帮朋友做的一个小程序。事情是这样的:他的公司需要撒网式找澳大利亚酿酒公司合作伙伴,他的任务就是把网上查到的酿酒公司的联系方式记录入库。记 录联系方式的数据库是把公司名称,地址,电话号码,传真号码,邮箱等信息分开不同字段储存的。于是他必须用鼠标在网页准确选下各种信息然后粘贴到数据表 中,不但工作效率低,而且据说由于鼠标精确动作太多,手腕酸得不得了。于是找我帮忙写个小程序,需求是他可以把联系方式部分整个复制下来,我的程序自动提 取出有关信息。以下是提取电话号码信息功能的设计过程:

我看了一下联系方式的可能情况,发现有时里面会包括多个电话号码,而朋友的数据库只记录一个号码。因此我决定把他选定文本中所有电话号码都提取出来,列在一个ComboBox中让他选择。在正文中标记为“Phone”或"Ph"或"P"的优先列在前面。

电话号码的写法五花八门,先考虑电话的标记,一般有以下几种:
Phone, Phone:, (Phone), P, P-, PH 等等
因 此可把匹配标记的正则表达式设计为 '(?-i)(?:/(?Phone|Ph?)[-:]?/s*/)?)'。这种写法有个缺陷是无法保证两边括号能匹配(例如能匹配到 '(Phone:')。但在这里的目的并不是校验,括号不匹配并不影响我提取电话号码,只求简单写成这样就够了,否则就要写 成:'(?:Phone|Ph?)[-:]?|/((?:Phone|Ph?)[-:]?/s*/))' ,麻烦得多。

然后考虑电话号码本身,一个完整的固定电话号码可能是这样的:
+61 2 1234 5678 , (61 2) 1234 5678 , (61-)2-12345678, 61 (0) 2 12345678
而也可能省略国家代码(+61)或洲区号(02)简写成
02 1234 5678 , 1234 5678, 2-1234-5678
也有可能不按主号码四字一断的做法,写成
02 123 456 78 等等

构建正则表达式的过程如下:

匹 配国家代号:(?:[+( ]*61[-) ]*)?  可以看出,这个式子如果用来校验是不合格的,它能匹配到'(++61-)-'这样的正文。但我在这里是为了提取公司网页上的信息,公司不会在自己的联系 方法信息里放入这样的乱码。提取信息时,根据需求,在不误判的前提下可以假定输入信息不会出现太离谱的错误。

匹配区号:(?:/(? *0?/)? *(/d) *[-)]? *)?  同上,这个式子也可能匹配到不合法的正文。这里如果把区号与主体号码合并处理会简单很多。但我在这里想把区号提取出来,作为判断省份的一个依据。

匹 配电话号码主体:(?:/d{5,}(?:[ -]/d+)*|/d{1,4}(?:[ -]/d+)+) 如果仅考虑匹配电话号码,用/d*(?: [ -]/d+)*就行了。但澳大利亚的邮政编码刚好是四位数字,而且地址中的信箱号有可能是3到4位数字。因此这里用了个麻烦的写法,如果连续数字小于5 位,则后面必须跟一个连接号或空格,然后再跟数字,才能匹配到。

因此优先选取的号码的正则表达式是:(?-i) (?:/(?Phone|Ph?)[-:]?/s*/)?)((?:[+( ]*61[-) ]*)?(?:/(? *0?/)? *(/d) ?[-)]?/s*)?(?:/d{5,}(?:[ -]/d+)*|/d{1,4}(?:[ -]/d+)+))
电话号码在子串1,区号在子串2

第 一轮扫描正文,一边找到匹配,一边把其替换成''。完成后,就可以用((?:[+( ]*61[-) ]*)?(?:/(? *0?/)? *(/d) ?[-)]?/s*)?(?:/d{5,}(?:[ -]/d+)*|/d{1,4}(?:[ -]/d+)+)) (去掉了匹配电话标志部分)对替换后的正文,找出所有没有电话标志,但符合格式的子串。  

沒有留言:

張貼留言