正则表达式使用TPerlRegEx完全符合PCRE标准(兼容Perl的正则表达式)

  • A+
所属分类:[开发技巧]

PerlRegEx底层是用C的类库,完全符合PCRE标准(兼容Perl的正则表达式)。使用方法:

解压了PerlRegEx包后,使用时只需要把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]+)+' 部分。整个(...)+结构需要在外部用开发语言的循环来逐次提取。先列出合法的不等符号:>=,>,=,< =,<,<>,==,!>,!< ,!>=,!< =,!=。因此不等符号部分应该是(!?>=?|!?< =?|!?==?|<>)。

因此,要分析篇正文,就需要:
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
也有可能不按主号码四字一断的做法,

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin
广告也精彩
avatar
广告也精彩

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: