关于从普通文本提取正则表达式的再思考

rex按:写完上一篇文章之后,一直在考虑如何真正实现从普通文本中归纳正则表达式的实现。走了许多弯路,也学了不少知识。例如,perl黑豹书上复杂的数据结构、匿名散列和数组、refenrence;紫龙书上的状态机的构造,数据结构上图论的知识,都是很有用的。另外还新学了graphviz的用法。以前觉得很神秘,不过一用才发现很直观。本文的插图是使用online版本的graphviz画的。

除了本文的这种实现方法(基于图),我还使用另一种方式实现了,很简单:基于关键词。具体作法是,逐一读取每一行文本,使用\s+等将其split开,形成array;然后再对所有的array进行求交集的操作(使用hash),得到每一行都有的关键词;然后按从左到右的顺序建立这覆的正则式^(.*?)keyword1(.*?)keyword2….keywordN(.*?)$,再分别匹配每一行文本,得到hash的hash表,或者array的array,转置,并列输出,得到^(option1|option2…)keyword1(option..)…$这样的正则式。最后作为验证,再将所最终生成的正则与每一行匹配测试一下。

这样以词为单位做完之后,再逐个字母地分隔开来,递归地处理(option1|option2…)的部分。先是单词级,再是字母级,有利于先在最大程度上找出重复的内容;而且粗化和细化的处理过程,思路是一致的,粒度不同罢了。

新手请自重,高手请赐教,我的思路未必是正确或最优的。

问题                                                                       

                                                        

有文本文件text.txt,内容如下:
this is a red fox
this is a blue firefox
this is a pig
a red fox
请写一则程序,根据文本内容,自动构造(比较合理的)正则表达式,使之能够匹配文件中每一行文本。

标准正则                                                         

有两种极端的解法是不可取的:

  1. ^.*$
  2. ^(this is a red fox|this is a blue firefox|this is a pig|a red fox)$

第一种失之于太宽泛,第二种失之于太狭隘。太宽泛则泥沙俱下,无论什么文本都能匹配;太狭隘则僵化死板,缺乏灵活性。好的正则表达式源于例文本(从例文本中提取规律),又高于例文本(能匹配同规律的其它文本)。匹配什么,排除什么,都有定则,所谓“君子有所为而有所不为”,指的就是这种情况(貌似跑题了:))。

那么,如何是比较靠谱的正则表达式呢?以上文的例子而言,可以是:
^(this is )?a (red fox|blue firefox|pig)$

现在我们向着标准答案出发。

思路                                                                       

任何复杂的电路图,都可以拆分为三种简单的关系:串联,并联,短路。正则表达式也同理。

既然是一条正则匹配所有的文本,那么这条正则(记为$re)也应该匹配第一行文本。

第一行文本为this is a red fox。那么,从^this is a red fox$应该是$re的一个(真)子集。它的路径为:“^”->this->is->a->red->fox->”$”。全部节点之间,是串联关系,从左到右依次排列即可。

示意图如下(可以点击看全尺寸图,下同):

Photobucket 

同理,第二行文本也应该是$re的子集。不过,由于已经存在了由^->this->is->a的路径,到a时出现支路,a->blue->firefox->$

将此路径添加到示意图上,得到:

Photobucket 

显而易见,这两条并列的支路,始于a,终于$,可以使用|来并列之。

好了,我们总结一下规律:

并列:如果存在A->B->C,且同时存在A->D->C,则B与D之间是并联关系。即出发点相同,结束点相同,且出发点与结束点之间各有一个以上的节点。并列使用括号来表示,之间以|分隔。例如,对于A->B->C,A->D->C,则可以使用A(B|D)C来表示其正则关系。

为什么要强调是一个以上节点呢?这里先卖个关子。请继续阅读。

再往下,this is a pig,同理,只需要在原图基础上添加a->pig->$的支路即可。此时图示如下:

Photobucket

最后一条,a red fox。这条貌似复杂,但是只需在^->a之间新添加了一条路径而已;a->red->fox->$之间原有路径,可以继续使用。此时,得到完整的示意图如下:

Photobucket 
 此时,观察可知,一种新的情况出现了。同时存在^->a,和“^”->this->is->a两条路径。想一下初中物理电路图,我们可以将这种情况称为“短路”,即,“^”->this->is->a这个线路的^、a两个节点之间,添加了一条无障碍通道,它能无视this、is的存在,因此,让this->is这条路径成为可选项。再总结一下规律:

如果有A->B->…C->D的路径,且有A->D的路径,则称A->D之间存在短路,此时,B->…->C可以用(B->…->C)?来表示(就是用括号来表示被短路的部分,问号表示短路之)。

顶点A,D之间,最多存在一个短路关系。但是可以有1或更多条并列的关系存在。

好了,分析结束,得到这样的正则式:
^(this is )?a (red fox|blue firefox|pig)$

这也就是为什么上文要强调是一个节点的缘故。

如果我们再精益求精的话,可以对red fox|blue firefox|pig这部分递归地进行上述分析过程,进而得到 (red |blue fire)fox|pig这样的结果。

实现                                                                       

思路有了,编程就简单了。perl中,固然可以使用比较简洁的hash表来表示链表之间的关系:
例如:

my $hash;

$$hash{“^”}{“this”}{“is”}{“a”}{“red”}{“fox”}{“\$”}=”";
$$hash{“^”}{“this”}{“is”}{“a”}{“blue”}{“firefox”}{“\$”}=”";

但是,节点的增删修改都是麻烦事。(我在hash迷宫中lost了很久才爬出来)

抽空补了一下有向图的知识,觉得可以简化问题如下。

Photobucket 

上图其实是一个有向图,只需记录所有的顶点集合,路径集合,再来求各路径之间的关系;最后打印输出,即是所求。

顶点集合为:

(^, this, is, a, red, fox, blue, firefox, pig, $);

通路关系集合为:

(^->this, this->is,…)

这两个集合在读取文本文件行的时候可以一次性建立。不复杂。关键是关系的确立。

再次总结,如下:
  • 从一个顶点A出发的N条支路必定汇合(只是有时是同一个点,有时不在同一点而已。本文给出的例子是最简单的情况,这里可以假设为汇合到同一点)于M点。
  • 这N条路中,每一条路径的长短以经过的节点个数来计算。例如上图中,^到a有一条路,上面的路径为2,下面的路径为0。
  • 短的支路决定了这N条支路的关系。
  • 长度为任意两点之间,最多只可能有一条长度为0的边。
  • 如果存在长度为0的边,则其余的同级的支路被短路。
  • 长度不为0的N-1条支路之间是并列关系。
  • 整个图始于^,终于$。
这些条件、判断,均可以细化为函数。具体的程序从略。

个人应用之明文字串到正则

近来工作中需要将某种明文字串转为简单的正则式。手动做当然可以,但是大量重复性的劳动,自然是交给机器处理为好。昨晚写了一款这样的脚本,放在这里。因为是处理我自己的工作的脚本,贴在这里仅作记录和存档之用,可能对别人没什么实际作用。当然,从现有的明文字串到正则式的转换,应该是个不错的题目,有兴趣朋友的可以深究。

值得一提的是,代码中用了$&, (?{}) 这样的perl only的东东,明晰了思路,简化了代码。如果不使用这种特性的话,代码要长5倍。另外,据说从效率上来说,use English之后,使用$MATCH比直接使用$&快5倍。但是对于即输入即执行的命令行程序来说,$&已经足够好。

实际应用一例:

perl hash2re.pl H:aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA0.zip/H:aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0/aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0/aaa/Aaaaa/aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0.exe
RE 1:   ^[a-z]{3}-[A-Z][a-z]{3}-[A-Z][a-z]{3}[A-Z][a-z]{6}[A-Z][a-z]{7}-[A-Z]{3}[0-9]\.zip$
        Matches: "aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA0.zip"
 
RE 2:   ^[a-z]{3}-[A-Z][a-z]{3}-[A-Z][a-z]{3}[A-Z][a-z]{6}[A-Z][a-z]{7}-[A-Z]{3}-[0-9]$
        Matches: "aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0"
 
RE 3:   ^[a-z]{3}-[A-Z][a-z]{3}-[A-Z][a-z]{3}[A-Z][a-z]{6}[A-Z][a-z]{7}-[A-Z]{3}-[0-9]$
        Matches: "aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0"
 
RE 4:   ^[a-z]{3}$
        Matches: "aaa"
 
RE 5:   ^[A-Z][a-z]{4}$
        Matches: "Aaaaa"
 
RE 6:   ^[a-z]{3}-[A-Z][a-z]{3}-[A-Z][a-z]{3}[A-Z][a-z]{6}[A-Z][a-z]{7}-[A-Z]{3}-[0-9]\.exe$
        Matches: "aaa-Aaaa-AaaaAaaaaaaAaaaaaaa-AAA-0.exe"

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/perl
 
#   by rex zhang 
#   Feb 09 2010 in Shanghai
 
#   usage: split and regexize hashed filename
#
 
my $lines=$ARGV[0];
while($lines =~ m#(C:[^/]+)#)
{
    $c=$1;
    $lines =~ s/$c//;
    print "ClearText Filename Ignored:\t\"$c\"\n\n";
 
}
my @array=split(m!\s*(?:\/|H:)+\s*!, $lines); 
 
my $counter=0;
foreach $line (@array){
    next if not $line;
    my $re=$line;
    local $len;    
 
    $re =~ s/(?=[.\[\]()])/\\/g;
    $re =~ s/\?/./g;
    $re =~ s/0+(?{ $len=length($&)})/[0-9]\{$len\}/g; 
    $re =~ s/A+(?{ $len=length($&)})/[A-Z]\{$len\}/g;
    $re =~ s/a+(?{ $len=length($&)})/[a-z]\{$len\}/g;
    $re =~ s/(.)\1+(?{ $len=length($&)})/$1\{$len\}/g;
    $re =~ s/\{1\}//g;
    $re =  "\^$re\$";
 
    $counter++;
    if ($line =~ /$re/)
    {
        print "RE $counter:\t$re\n";
        print "\tMatches: \"$line\"\n";    
    }
    else
    {
        print "RE $counter:\t$re\n";
        print "\tFailed: \"$line\"\n";
    }
    print "\n";
}

统计重复文本行的两种方法

假设样本文件a.txt内容如下:

hello world!
hello world!
I love regex.
hello world!
I love regex.
hello world!

简单观察可知,hello world!共重复4行;I love regex.重复2行。如何使用正则表达式来写一个程序,统计这些数据呢?因为现实中需要统计的文件,绝非是只凭肉眼就能观察出来。我想到了两种方法,第一种方法,是依赖于正则表达式(否则这篇文章也不会贴在这里);第二种,hash表做主角,正则表达式作绿叶。

正则表达式的解法

思路是:对于任何一行文本,如果后面若干行[0~EOF)之后,如果存在相同的文本行,则记下该行内容,统计出现次数;然后删除这样的文本行,再进行下一行的统计。输出统计结果。

下面是相应的perl程序,附注释。

#!/usr/bin/perl 
#usage:  ./dup_re.pl <a.txt
 
undef $/;           # enable "slurp" mode
my $file = <STDIN>;          # whole file now here
 
while($file =~ m
    /                   #for each line;
        ^\s*            #ignore the whitespaces at both ends; 
        (\S.*?)         #get the line content, save to $1;
        \s*$            #ignore empty lines by using \S
        .*?             #check if there is the same pattern of $1
        ^\s*\1\s*$              #after 0 or more lines;
    /smx) 
{
    my $line=$1;                        
    my $count= $file =~ s/$line//g;     #delete the duplicated lines
                       #save the number to $count;
                                       #ignore empty lines
        print $count,"times:\t",$line,"\n";
}

Hash表解法

这种方法,受益于perl语言本身的强大的hash表功能。思路如下:

  • 建立空的hash表;
  • 逐行读取文件;
  • 以文本内容为key,插入到表中来。如果是首次出现,value为0,否则value++。
  • 输出hash表中value>=2的记录。

Perl程序:

#!/usr/bin/perl
#usage:  ./dup_hash.pl a.txt
 
my %hash=();
while(<>)
{
   if (/^\s*(\S.*?)\s*$/)           #ignore whitespaces at both ends; 
   {                                #ignore empty lines by using \S
        $hash{$1}++;                #save the line to $1, and count the time it appears
   }
}
 
#sort the hash by values; 
foreach $key (sort { $hash{$b} <=> $hash{$a} } keys %hash) { 
    if ($hash{$key}>=2)             #only print the lines that duplicates;
    {                               #for all results, just remove the 'if' line
       printf "%d times:\t%s\n", $hash{$key}, $key;
    }
}

结果

上面的程序分别保存为dup_re.pl,dup_hash.pl。由于程序对于外部文件的读取的方法不同,运行方式也有差别,详见下图:
我爱正则表达式|统计重复文本行的两种方法

Update

忽然想到,如果要让这脚本更有效,可以指定忽略大小写,忽略单词间多个空格的情况,使得Hello world!   hello   WORLd! 被视为重复行。测试了一下,正则式没让我失望。

[译]递归正则表达式

原文在此rex译于2009年12月15~17日,翻译过程中使用的是google docs@prism@firefox@ubuntu 9.10,很爽的体验。感谢余晟老师在正则和翻译方面的悉心指导。

递归正则表达式

平时我们用到的正则表达式,其实没那么“规则”。多数编程语言所支持的扩展的正则表达式,其运算能力比起形式语言理论所定义的“规则”正则表达式要强得多。

rex注:在这里其实可以看到,如果将正则表达式译为正规表达式,一切就都通顺了:
平时我们用到的正规表达式,其实没那么“正规”。多数编程语言所支持的扩展的正规表达式,其运算能力比起正式语言理论所定义的“正规”正规表达式要强得多。

regular expression的日文是“正规表现”,在鸟哥书中,好像也将其称为正规表达式。via

例如,经常用到的捕获缓存,就是用来帮助我们临时存储任意正则表达式模式,以便重复使用。 又如,“环视断言”能让正则表达式引擎在做决定之前先偷偷看看环视一下。这些扩展让正则表达式非常强大,足以描述一些“上下文无关语法”。

Perl语言的正则表达式引擎特性异常丰富,其特征之一是懒惰正则子表达式(Lazy regular subexpressions),格式为(??{code}),其中的“code”可以是任意一段perl程序,该子表达式可能匹配时,这段程序就会执行。

我们可以利用这一特征来编写出非常有趣的东西,即将正则表达式自身嵌在它的“code”部分,由此生成递归的正则表达式(a recursive regular expression)!

一直以来,正则表达式无法匹配0n1n这种表达式,也就是由若干个0以及同等数量的1所组成的字符串。如果使用懒惰正则子表达式,这一经典问题就迎刃而解。

下面是匹配0n1n字串的perl正则表达式代码。

$regex = qr/0(??{$regex})?1/;

rex注:紫龙书第四章有云:“文法是比正则表达式表达能力更强的表示方法。每个可以使用正则表达式描述的构造,都可以使用文法来描述,但是反之不成立。换句话说,每个正则语言都是一个上下文无关语言,但是反之不成立。”书中交待的正则,也应该是指的常规正则表达式,而非现代语言中的扩展的正则表达式。一般使用正则表达式来构造小部件,而使用文法来组建语言框架。

此正则表达式匹配一个字符0,之后是正则表达式自身0或1次,之后是字符1。如果正则表达式自身部分不能匹配,那么它只能匹配01;如果自身部分可以匹配,则正则表达式匹配的是00($regex)?11,此时若不能匹配自身则结果是0011,若可以匹配就是000($regex)?111,……依次顺延。

下面是匹配050000150000的Perl程序:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/perl                                                                                                                         
 
$str = "0"x50000 . "1"x50000;
$regex = qr/0(??{$regex})*1/;
 
if ($str =~ /^$regex$/) {
  print "yes, it matches";
}
else {
  print "no, it doesn't match";
}

现在来看题图所示的Yo Dawg正则表达式。你先猜猜它的作用?正确答案是,它匹配(foo(bar())baz)这样完全嵌套的括号表达式(fully parenthesized expression)或((()()())())这样的平衡括号表达式(balanced parentheses)。

1
2
3
4
5
6
7
8
9
$regex = qr/
  \(                 # (1) match an open paren (                               #(1),此处匹配开括号
    (                # followed by                                                      #(2),紧接着是
      [^()]+       #   (3) one or more non-paren character            #(3),1个或多个非括号字符
    |               # OR                                                                    #(4),或
      (??{$regex})   #   (5) the regex itself                                   #(5),正则式自身
    )*              # (6) repeated zero or more times                     #(6),重复0或多次
  \)                 # (7) followed by a close paren )                         #(7),紧接着是闭括号
/x;

rex注:关于Yo Dawg图片的含义,可以参考这里。基本上是全是“Yo dawg, I herd you like X, so we put a Y in your Y so you can Z while you Z”的结构的配图文字。

构造此正则表达式的思路是这样的。对于完全嵌套的括号表达式,它的开始字符是一个开括号。这是最简单的一步,我们直接写出(上面程序中的(1))。同理,它的结束字符是闭括号,于是得到(7)。现在该动脑筋了,括号中间是什么呢?对,它可以是既不是开括号又不是闭括号的任意字符(第(3)点),也可以是另一个完全嵌套的表达式(即第(5)点)。所有的这些,既可以只匹配0次(第(3)点),以便构造最小的完全嵌套括号表达式(),也可以匹配多次来匹配较复杂的表达式。

去掉 /x 选项(即,不再使用多行风格的注释模式),可以简记为:

$regex = qr/\(([^()]+|(??{$regex}))*\)/;

但是切勿在正式产品中使用这一特性,它太诡异,不好把握。建议使用较稳定成熟的Text::BalancedRegexp::Common 模块。

rex注:对于(??{code}),perl官方的提示是:此正则表达式仅作测试使用,可能有更新而不作提示。代码执行时产生的副作用,因版本而异,运行结果或有不同,取决于正则引擎的后期优化。

最后提醒大家,在Perl 5.10中已经可以使用递归捕获缓存来替代懒惰代码子表达式了,运行结果相同。

下面是匹配0n1n的递归捕获缓存语法(?N)的实现:

my $rx = qr/(0(?1)*1)/;

(?1)*的含义是“匹配第一组0或多次”,这里的第一组是指整个正则表达式。

请自行动手,重写平衡括号的正则表达式,当作练习。

祝玩得开心!

数字转美元程序

本程序将数字转换为英文的美元数,如: 输入

./num2eng.pl 1,100,834.10

则输出:

Total: Say US Dollars One Million One Hundred Thundsand Eight Hundred and Thirty-Four and Ten Cents Only.

注意事项:

  1. 整数部分可以使用半角的逗号、空格、单引号、下划线、中划线分隔。
  2. 分隔符的位置可以任意(每3位可,每4位也可),可以任意组合(可以混合使用上述的分隔符)。
  3. 如果使用单引号,请注意在最外边加上双引号以免转义。

完整程序:
Read the rest of this entry »

饭否消息析取之regex vs xml

页内导航:

批量导出饭否程序的方法很多,但是基本思路都是先将该网页保存到本地,然后将有用的饭否消息析取出来。本文不讨论如何下载饭否网页了(使用迅雷、wget、curl等),重点讨论对于下载到本地的网页,如何将有用的饭否消息析取出来。

Read the rest of this entry »

饭否私信格式分析

URL

饭否私信分为两种,一种是我收到的私信,一种是我发出的私信。

  • 我收到的私信:http://fanfou.com/privatemsg/p.(1-N)
  • 我发出的私信:http://fanfou.com/privatemsg/sent/p.(1-N)

上面的地址中不含饭否ID;需要cookie验证。

结束标志

通过cookie验证后,可以使用数字获得对应页码的私信内容。什么时候是结束呢?假如您的收件箱有1000条私信,每页显示20条,那么当你您输入http://fanfou.com/privatemsg/p.51时,就得不到任何有效的内容了。作为程序,它是寻找如下标志:

<ol class="wa">\s*</ol>

好友列表

在页面代码中,每页都有一个“向XXX发送私信”的combox列表,条目以昵称+ID组成。如果你的好友很多的话(500+),每条好友(昵称+ID)需要20字节(估算)的话,20*500=10K,大约需要多抓取10K的字节量。

收件箱饭否私信的结构

收到的私信分为两种,一种是有回复信息的(回复原文:…),一种是没有回复的。先从简单的入手,看没有回复的。
所有的私信都在<ol class=”wa”>…</ol>之内,以<li></li>分隔

例如下面这一条,就是一则很规范的私信(与发件人相关的信息都使以正则式表示):

<li>
	<a href="/[^"]+" title="[^"]+" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	来自<a href="[^"]+">[^"]+</a><span class="content">没法比较啊,你得说个具体的值,比如100条以下的算少,1000条以上的算多……</span>
	<span class="stamp time" title="2008-05-30 17:25">15 小时前</span>
	<span class="op">
		<a href="/privatemsg.reply/583520">回复</a>
		<a href="/privatemsg.del/583520" class="post_act">删除</a></span>
</li>

其中,需要记录的信息有:

  • 发件人名字;
  • 发件人ID;
  • 私信内容;
  • 时间;
  • 私信ID;(便于作删除、回复处理)。

根据以上需求,将上面的私信代码作处理:

<li>
	<a href="/([^"]+)" title="([^"]+)" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	来自<a href="[^"]+">[^"]+</a><span class="content">([^<]+)</span>
	<span class="stamp time" title="([^"]+)">[^<]+</span>
	<span class="op">
		<a href="/privatemsg.reply/(\d+)">回复</a>
		<a href="/privatemsg.del/\d+" class="post_act">删除</a>
	</span>
</li>

从而得到:

$1=fanfou ID;
$2=fanfou name;
$3=private msg;
$4=msg time;
$5=msg ID;

再看一下包含“回复原文”的私信的结构(部分内容已作正则处理):

<li>
	<a href="/([^"]+)" title="([^"]+)" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	来自<a href="[^"]+">[^"]+</a><span class="content">([^<]+)</span>
	<span class="stamp time" title="([^"]+)">[^<]+</span>
	<span class="op">
		<a href="/privatemsg.reply/(\d+)">回复</a>
		<a href="/privatemsg.del/\d+" class="post_act">删除</a>
	</span>
	<p class="pm-parent">回复原文: 有兴趣就有动力</p>
</li>

与前者相比,只是多了<p class=”pm-parent”>.*?</p>这一段。这是不足为虑的。只要整体加上?这个强有力的正则符号,就能与上面的代码片段归纳到一起。两者结合合的代码如下:

<li>
	<a href="/([^"]+)" title="([^"]+)" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	来自<a href="[^"]+">[^"]+</a><span class="content">([^<]+)</span>
	<span class="stamp time" title="([^"]+)">[^<]+</span>
	<span class="op">
		<a href="/privatemsg.reply/(\d+)">回复</a>
		<a href="/privatemsg.del/\d+" class="post_act">删除</a></span>
	(?:<p class="pm-parent">([^<]+)</p>)?
</li>

得到的变量为:

$1=fanfou ID;
$2=fanfou name;
$3=private msg;
$4=msg time;
$5=msg ID;
$6=parent msg;#回复原文。

发件箱饭否私信的结构

抄代码:

<li>
	<a href="/([^"]+)" title="([^"]+)" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	发给<a href="[^"]+">[^"]+</a><span class="content">海内的像片是真的。</span>
	<span class="stamp time" title="2008-05-28 20:24">2008-05-28 20:24</span>
	<span class="op"><a href="/privatemsg.del/576827" class="post_act">删除</a></span>
</li>

这与“我收到的私信”的结构完全一致,只是将原来的“来自”改为“发给”而已。

不出意外,带有“回复原文”的“我收到的私信”的结构是这样的:

<li>
	<a href="/([^"]+)" title="([^"]+)" class="avatar"><img src="[^"]+" alt="[^"]+" /></a>
	发给<a href="[^"]+">[^"]+</a><span class="content">在自述部分显示的那个网上。</span>
	<span class="stamp time" title="2008-05-29 17:00">2008-05-29 17:00</span>
	<span class="op"><a href="/privatemsg.del/579918" class="post_act">删除</a></span>
	<p class="pm-parent">回复原文: 我也要试一试。</p>
</li>

我们从饭否私信代码上得到的信息就这些。遗憾的是,饭否私信中,关于“回复原文”是以明文内容形式出现,而不是以原私信ID的形式出现。后期处理时通过搜索功能解决此问题并非不能,只是如果饭否官方能够再将此功能完善的话,会省整理者不少力气。

饭否的私信源码分析完毕。至于如何读写cookie,如何写代码,如何以数据库的形式来管理下载的数据,是第二阶段的事情了。待我一一实现。

windows下的正则式工具介绍之一:RegexBuddy

RegexBuddy

俗话说,工欲善其事,必先利其器。关于windows下的正则表达式工具,这里推荐的是:RegexBuddy和PowerGREP。在linux下,也有好用的正则表达式工具,例如grep的兄弟们,只不过是都是基于命令行的。而这两款windows下的小工具,其突出特点是可视化,允许尝试和预览,极大地方便了使用者。

RegexBuddy:网址是http://www.regexbuddy.com。在编写正则式时,它提供可视化的支持、提示、调试方面的便利;在使用正则表达式时,它无私地将正则式转换为多种语言的字串,还提供了代码输出功能。正则式助手,该称号名副其实。

基本界面

正则式的基本功能无外乎搜索和替换。在本文中,我们使用匹配Email的正则式,代码如下:

\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b

此时,RegexBuddy的界面如图:

regex001

在点击“Explain Token”时,能对当前的正则式片断的作用作出详细解释,例如当你把光标移动到\b上再点击Explain Token,就会激活帮助文档,自动定位到Word Boundaries这一段。

如果想对刚才编写的这条正则式进行测试和验证,可以点击“Test”进行测试。这时,在下边的文本框输入所需要匹配测试的文字,例如dog@animals.com,匹配结果就以黄色背景色标出。在本例中,你或许没有得到正确的匹配,呵呵,那是正常的。为什么?答案见文章结尾。

regex002

拷贝粘贴

RegexBuddy能把正则式以多种字符串格式拷贝出来。还是刚才那条正则式,根据需要,它可以被拷贝为:

'\\b[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b'
"\\b[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b"
'/\\b[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b/i'

具体的选项在copy菜单里,如下图所示。你不必为单双引号、正反斜线操心了。

regex004_menu

在paste菜单项中也有类似应用,不赘述。

正则式->代码

如果你想把刚才编写好的正则式应用在程序中,这里还有一个选项:Use,界面见下图:

regex004_use

在上面的match和replace之间点击,其代码也相应自动调整;当选取不同的language时,代码也会相应调整。它支持的语言格式为:

  • C#
  • Delphi(NET/Win32)
  • Java/JavaScript/ECMAScript
  • PCRE
  • PHP
  • Perl
  • Python
  • RealBasic
  • Ruby
  • VB

另外,它还有function选项,分别用以实现下述功能:

  • If/else branch whethe the regex matches (part of) a string. If/else验证正则式是否匹配字串(的一部分)。 最常用的功能。
  • If/else branch whethe the regex matches a string entirely. If/else验证正则式是否匹配整条字串。
  • Get the part of a string matched by the regex. 取得字串中与正则式匹配的部分。Get the part of a string matched by a capturing group. 取得字串中所匹配的捕获组。这一条我也是刚刚知道,很有用哟。
  • Get an array of all regex matches in a string.将字串中所有的匹配保存到数组中。
  • Iterate over all matches in a string。列出字串中所有的匹配项。(例如,在使用正则式’\w’来匹配字串’abc’时,本function列出的内容为’a',’b',’c’.)。单词iterate的含义是重复
  • Comment with RegexBuddy’s regex tree. RegexBuddy的正则树的注释。

文本分割split

如果需要处理的文本是以某种分隔符隔开的,而该种分隔符恰好又能使用正则式描述,(例如html标签),此时regexbuddy的split功能就可以大显身手了。我随便打开了一个饭否网页,对其源代码中的消息部分(<div id=“stream”>与</div>之内)的文本进行了处理,使用如下正则式删除了所有的尖括号内容,只留下普通文本。

使用的正则式为:

<[^>]+>

软件界面以及运行结果请见下图。



结尾:

关于本文开头提出的小问题,细心的你或许一下子就能看出答案了!见下图:

regex003

只要选中Case insensitive选项中OK啦!如果你没有找到,或许是因为该软件是英文的,一时间您没有注意到该选项;或者您对正则式还不太熟悉。

软件下载

上文已经提到,其官网为www.regexbuddy.com,可以去下载其最新版试用。该软件为商业软件

  • 如果你偶然路过,尝新而已,那只需下载试用版即可;
  • 如果你觉得好用、准备常用、手有余钱、非正版不用,不妨花美金购买
  • 如果你喜欢它,同时你认为优秀的网络资源是应该和朋友免费分享的,从而想获得该软件的全功能免费版,好吧,我也成全你,请在本文后留言(附邮箱),我会把这个小东西的链接发给你(最新版为3.1.1,我手头的全功能版为3.1.0,也足够用了)。

下篇文章将要介绍另一款regex工具:PowerGREP,敬请期待。

本文为rex.zhasm原创,原文地址在http://iregex.org/blog/y2008/m04/d30/regexbuddyregexbuddy.html,可以在遵循CC协议的条件下转载。

————————————-
2008.12.26 更新:
本文已经关闭评论,即将发布3.2.0完全版。不会晚于2008.12.28。敬请期待。
2008.12.28 更新:
请移步至此下载RegexBuddy 3.2.0版。

[老贴整理]如何使用正则式从英文句子里提取词根

以前在chinaunix回答过这样一个问题,用到了正则表达式(而且我认为正则式解决此类问题是最合适的。)

学英语的一些例句,每句都有若干词根相同的词,例如 She swears to wear the pearls that appear to be pears. 但是每句的词根都未必相同;我希望把这些包含词根的词都标记出来,请问如何写?

这里说的词根不是原本词根的定义,只是一组字母序列,比如

9. The dust in the industrial zone frustrated the industrious man.

词根是dust或ust

10. The just budget judge just justifies the adjustment of justice.

词根是dust

11. I used to abuse the unusual usage, but now I’m not used to doing so.

词根是use,有变形

12. The lace placed in the palace is replaced first, and displaced later.

词根是lace

13. I paced in the peaceful spacecraft.

词根是pace

14. Sir, your bird stirred my girlfriend’s birthday party.

词根是ir

如果您对此问题感兴趣,请独立思考后再继续阅读本站提供的解决方法。

Read the rest of this entry »