\w
对应到附重音记号 (accented)的字元?
/[a-zA-Z]/
对应?
/o
到底是干麽用的?
\b
作字界搜寻时会失败呢?
\G
能给我什麽好处?
#把每行变成「第一个字、冒号,和剩馀的字元数」这样的格式。 s/^(\w+)(.*)/ lc($1) . ":" . length($2) /ge;
/x
修饰子会要直译器忽略正规表示式内的任意空白 (在特定字元类别 [character
class]中例外),同时也让你在式子中使用平常的注解方法。你应该能想像得到,
加上一些空白与注解帮助会有多大。
/x
让你把下面这行:
s{<(?:[^>'"]*|".*?"|'.*?')+>}{}gs;
变成:
s{ < #箭头括弧区起始 (?: #划分「勿追溯前段」(non-backreferencing)的括弧 [^>'"] * #有零个以上、不是 >、 ',或 "的字元 | #或者是 ".*?" #一段双引号圈起来的区域 (吝啬式对应) | #或者是 '.*?' #一段单引号圈起来的区域 (吝啬式对应) ) + #以上区域出现一次或多次 > #箭头括弧区结束 }{}gsx; #用空字串来替换;也就是杀掉
虽然它看来还是不够简明易懂,但至少大大有助於解释这个模式 (pattern)的意义。
/
字元来区隔,但实际上用几乎任何字元来作都行。perlre文件中有说明。例如,上面的 s///
便是用大括号来当区隔字元的。选择另一个区隔字元可以免除在模式中得避开
(quote)区隔字元的困扰。例如:
s/\/usr\/local/\/usr\/share/g; #选错区隔字元的後果【译注: #常被戏称为「搭牙签」症候群 ;-)】
s#/usr/local#/usr/share#g; #这样不是好多了?!
有很多方法将多行的资料结合成一个字串。如果你希望在读入输入资料时自动得到
这项功能,你得重新设定 $/变数 (若为段落,设成 '';若要将整个档案读进一字
串,设成 undef
),以容许你一次能读入一行以上的输入。
请参考 prelre,其中有选择 /s
或 /m
(或二者都用)的说明: /s
让万用字元 (``.'')能对应到换行字元【译注:通常换行字元不在 ``.''
的对应范围内】,
/m
则让 ``^''和 ``$''两个符号能够对应到任何换行字元的前後,而不只是像平常
那样只能对应到字串头尾。你所需要确定的是你的确有个多行的字串。
例如说,以下这个程式会侦测出同一段落里重覆的字,即使它们之间有换行符号相隔
(但是不能隔段)。在这个例子里,我们不需要用到 /s
,因为我们并未在任何要跨行对应的正规表示式中使用 ``.''。我们亦无需使用 /m
,因为我们不想让 ``^''或 ``$''去对应
到字串中每个换行字元前後的位置。但重点是,我们得把 $/
设成与内定值相异的值,否则我们实际上是无法读入一个多行的资料的。
$/ = ''; #读入一整段,而非仅是一行。 while ( <> ) { while ( /\b(\w\S+)(\s+\1)+\b/gi ) { print "在段落 $.找到重复的字 $1\n"; } }
以下的程式能找出开头为 ``From ''的句子 (许多邮件处理程式都会用到这个功能):
$/ = ''; #读入一整段,而非仅是一行。 while ( <> ) { while ( /^From /gm ) { # /m使得 ^也会对应到 \n之後 print "开头为 From的段落 $.\n"; } }
以下的程式会抓出在一个段落里所有夹在 START与 END之间的东西。
undef $/; #把整个档案读进来,而非只是一行或一段 while ( <> ) { while ( /START(.*?)END/sm ) { # /s使得 .能跨越行界 print "$1\n"; } }
..
运算元 (在 perlop文件中有说明):
perl -ne 'print if /START/ .. /END/' file1 file2 ...
如果你要的是整段文字而非各单行,你该使用:
perl -0777 -pe 'print "$1\n" while /START(.*?)END/gs' file1 file2 ...
但是当 START
和 END
之间的东西作巢状(内含)式分布 (nested occurrences)的时候
,你便得面对本篇中所提到的对称式文字对应的问题。
事实上,如果你不介意把整个档案读入记忆体的话,不妨试试看这个:
undef $/; @records = split /your_pattern/, <FH>;
Net::Telnet模组 (CPAN里有)具有一项功能,可监视着输入流 (input stream)、等待特定的模式出现,或是在规定时间到了还没等到时,送出逾时 (timeout)讯息。
##开一个有叁行的档案 open FH, ">file"; print FH "The first line\nThe second line\nThe third line\n"; close FH;
##取得一个可读/写的档案处理把手 $fh = new FileHandle "+<file";
##把它附着成一个 "stream"物件 use Net::Telnet; $file = new Net::Telnet (-fhopen => $fh);
##等到第二行出现了,就把第叁行印出来。 $file->waitfor('/second line\n/'); print $file->getline;
#原作者为 Nathan Torkington,经 Jeffrey Friedl调整 # sub preserve_case($$) { my ($old, $new) = @_; my ($state) = 0; # 0 = no change; 1 = lc; 2 = uc my ($i, $oldlen, $newlen, $c) = (0, length($old), length($new)); my ($len) = $oldlen < $newlen ? $oldlen : $newlen;
for ($i = 0; $i < $len; $i++) { if ($c = substr($old, $i, 1), $c =~ /[\W\d_]/) { $state = 0; } elsif (lc $c eq $c) { substr($new, $i, 1) = lc(substr($new, $i, 1)); $state = 1; } else { substr($new, $i, 1) = uc(substr($new, $i, 1)); $state = 2; } } #把剩下的 new部份作处理 (当 new比 old长时) if ($newlen > $oldlen) { if ($state == 1) { substr($new, $oldlen) = lc(substr($new, $oldlen)); } elsif ($state == 2) { substr($new, $oldlen) = uc(substr($new, $oldlen)); } } return $new; }
$a = "this is a TEsT case"; $a =~ s/(test)/preserve_case($1, "success")/gie; print "$a\n";
这会印出:
this is a SUcCESS case
\w
对应到附重音记号 (accented)的字元?
/[a-zA-Z]/
对应?
/[^\W\d_]/
表示,不论你的 locale为何。非字母则可用
/[\W\d_]/
表示 (假定你不把 ``_''当成字元)。
$variable
及 @variable
变数。同时也要记得,一个 s///
替换式右侧部份是当成双引号括起来处理的 (详情请参看 perlop说明文件)。更别忘记,任何一个正规表示式里的特殊字元都会先被解译、处理,
除非你在替换模式前加 \Q。以下即为一例。
$string = "to die?"; $lhs = "die?"; $rhs = "sleep no more";
$string =~ s/\Q$lhs/$rhs/; # $string现在成了 "to sleep no more"
少了 \Q,则这个正规表示式同时也会错误地对应到 ``di''。【译注:因为 /die?/ 这个式子表示 ``di''後头的 ``e''可有零个或一个】
/o
到底是干麽用的?
/o
会在第一次用到那个变数
时把它锁定。在一个无变数的正规表示式里面,此情形永远为真,而且事实上,当你整
个程式在被编译成内部(位元)码的同时,你所用的模式亦然。
除非在模式里有变数转译的情况发生,否则使用 /o
是无关痛痒的。在模式中有变数并且又有 /o
修饰子的情况下,正规表示式引擎则既不会知道也不会去管这个模式在
第一次评估之後其中变数是否又有所改变。
/o
常被用来额外提高执行效率。当重覆评估无关紧要 (因为事先知道该变数的
值不会改变);或是在有些罕见的情况下,故意不让正规表示式引擎察觉到变数值已改变
时,便可透过此一手段,避免持续评估,来达到提高效率的目的。
下面以一个 ``paragrep'' (「段落grep」)程式作范例:
$/ = ''; #使用段落模式 $pat = shift; while (<>) { print if /$pat/o; }
perl -0777 -pe 's{/\*.*?\*/}{}gs' foo.c
只能在大部分(但非全部)的情况下成功。你知道,这程式对某些种类的 C程式显得太 简陋、单纯了,尤其是那些被双引号括起来、看似注解的字串。针对它们,你需要像 这个 Jeffrey Friedl所写的这样的程式:
$/ = undef; $_ = <>; s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|\n+|.[^/"'\\]*)#$2#g; print;
当然,这程式可以用 /x
加上空白与注解使它更容易让人看懂。
\1
之类)这样方便的功能,但它们仍然不够强大。你依然得用非正规表示式
的技术去解析这类文字,譬如像两端用小括号或大括号包含起来的文字。
你可以在 http://www.perl.com/CPAN/authors/id/TOMC/scripts/pull_quotes.gz
找到一个精细复杂的副常式(给 7-bit ASCII专用),它可以抓出成对甚至於巢状分布
的单一字元,像 `
和 '
,{
和 }
,或 (
和 )
。
CPAN中的 C::Scan模组包含一个这样的副常式供内部使用,但无说明文件。
?
, *
, +
,{}
)而非整个模式;Perl较喜欢作区域性的贪婪以得
到立即的快感,而不是对整个式子的贪婪。如欲使用同样的量化子作非贪婪式对应的话
【译注:即所谓的吝啬(stingy)式对应】,用 (??
, *?
, +?
, {}?
)。例如:
$s1 = $s2 = "I am very very cold"; $s1 =~ s/ve.*y //; #贪婪式;结果为 I am cold $s2 =~ s/ve.*?y //; #吝啬式;结果为 I am very cold
注意到在第二个替换中一碰到 ``y''就停止整个对应了吗? *?
量化子有效率地告诉正
规表示式引擎,一但对应到一个模式,就马上把控制权移交下去,这行为就好比你手上有
个烫手山芋时所会采取的行动一样。
while (<>) { foreach $word ( split ) { #在此作你想对 $word作的动作 } }
请注意这里所谓的字和英文中对字的定义不同;它们可能只是一段连续的、非空白的 字元罢了。
若欲处理的是一连串纯字母的话,可以考虑用:
while (<>) { foreach $word (m/(\w+)/g) { #在此作你想对 $word作的动作 } }
while (<>) { while ( /(\b[^\W_\d][\w'-]+\b)/g ) { # "`sheep'"会漏失掉 $seen{$1}++; } } while ( ($word, $count) = each %seen ) { print "$count $word\n"; }
如果你要算行数,则用不着使用正规表示式:
while (<>) { $seen{$_}++; } while ( ($line, $count) = each %seen ) { print "$count $line"; }
如果你希望这些输出经过排列,请参看有关 Hashes的那部分。
while (<FH>) { foreach $pat (@patterns) { if ( /$pat/ ) { # do something } } }
要避免以上的方法,要不你就选用 CPAN 中几个实验性的正规表示式扩充模组其中一个 (对你的目的来说可能效率还是不够好),或是自己写个像下面这样的东西 (自 Jeffrey Friedl书中的一个函式所得到的灵感):
sub _bm_build { my $condition = shift; my @regexp = @_; #这里不可用 local(),得用 my() my $expr = join $condition => map { "m/\$regexp[$_]/o" } (0..$#regexp); my $match_func = eval "sub { $expr }"; die if $@; # $@【错误变数】里面有东西;这不该出现! return $match_func; }
sub bm_and { _bm_build('&&', @_) } sub bm_or { _bm_build('||', @_) }
$f1 = bm_and qw{ xterm (?i)window };
$f2 = bm_or qw{ \b[Ff]ree\b \bBSD\B (?i)sys(tem)?\s*[V5]\b };
# 我 /etc/termcap while ( <> ) { print "1: $_" if &$f1; print "2: $_" if &$f2; }
\b
作字界搜寻时会失败呢?
\b
做为 \s+
的同义词,还有把它当成界定空白及非空白字元间的边界。两者都不对。\b
是介於一个 \w
字元和 \W
字元之间的部分(亦即 \b
是一个「字」的边缘)。它是一个长度为 0的标的物,就像
^
、$
,以及所有其它的标示字元 (anchors)一样,在对应时并不消耗、占掉任何字元。perlre使用说明中对各正规表示式超字元 (metacharacters)的特性和使用都有做解释。
以下是错误使用 \b
的例子,并附上修正:
"two words" =~ /(\w+)\b(\w+)/; #错误! "two words" =~ /(\w+)\s+(\w+)/; #正确
" =matchless= text" =~ /\b=(\w+)=\b/; #错误! " =matchless= text" =~ /=(\w+)=/; #正确
虽然它们也许不能作到你以为它们能作的事,但 \b
及 \B
仍然相当有用。要看看正确使用 \b
的范例,请参考「如何於多行文字中抓出重复字」一问题内所附之范例。
\Bis\B
这个模式是使用 \B
的一个例子。它只会对应到出现在一个字内部的 ``is'',例如
``thistle'',但不会对应到 ``this''或 ``island''。
$2
等等的使用也是以同样的方式处理的。所以每当你的模式中含有捕捉用的小括号
(capturing parentheses)时,
你就得付出同样的代价。但若你从不在你的程式中用到 $&等这些东西,那麽
没有捕捉用小括号的正规表示式就不用付出额外的速度作代价。所以,请尽可能避免使用
$&, $'及 $`,但若真的无法避免 (有些演算法的确需要它们),就尽量用糸
吧,反正你已经付出代价了。
\G
能给我什麽好处?
\G
在一个对应式或替换式中要和 /g
修饰子合起来用 (若无 /g
它就会被忽眷 。它是用来标示上一个成功的模式对应完成後所停在的位置,亦即
pos()
点。
例如说,你有一行信件文字是按标准的 mail及 Usenet记法 (就是以 >
字元作开始)作成引言的,而你现在要把每个开头的 >
都换成 :
。那麽你可以用下面的方法来作:
s/^(>+)/':' x length($1)/gem;
或者使用 \G
,更简单也更快:
s/\G>/:/g;
更复杂的方法可能要用到记号赋予器 (tokenizer)。下面看来像 lex语法分析器程式
码的例子是 Jeffrey Friedl提供的。它在 5.003
版因为其版本中的臭虫而无法执行,但在 5.004或以上的版本的确可行。(请注意到 /c
的使用,它的存在是为了防止 /g
在对应失败时将搜寻位置归零到字串的开始。)
while (<>) { chomp; PARSER: { m/ \G( \d+\b )/gcx && do { print "number: $1\n"; redo; }; m/ \G( \w+ )/gcx && do { print "word: $1\n"; redo; }; m/ \G( \s+ )/gcx && do { print "space: $1\n"; redo; }; m/ \G( [^\w\d]+ )/gcx && do { print "other: $1\n"; redo; }; } }
当然,上面这个本来也可以写成像
while (<>) { chomp; PARSER: { if ( /\G( \d+\b )/gcx { print "number: $1\n"; redo PARSER; } if ( /\G( \w+ )/gcx { print "word: $1\n"; redo PARSER; } if ( /\G( \s+ )/gcx { print "space: $1\n"; redo PARSER; } if ( /\G( [^\w\d]+ )/gcx { print "other: $1\n"; redo PARSER; } } }
但是这麽作就不能让那些正规表示式的式子上下对齐一目了然了。
egrep(1)
程式的 DFAs (deterministic
finite
automata,决定式有限自动机)特性,但事实上为了具备「退回原路」(backtracking)
与「追溯前段」( backreferencing)的功能,它们实作时是用 NFAs
(non-deterministic finite automata,非决定式有限自动机)的。并且它们亦非
POSIX式的,因为那样会造成在所有情况下都有最差的表现。(似乎有些人较注重确
保一致性,即使那同时也确保了缓慢的速度)。你可以在 Jeffrey Friedl所着的
``精通正规表示式'' (Mastering Regular Expressions)一书中 (O'Reilly出版)
,获得所有你想知道关於这些事的所有细节(在 perlfaq2里面有该书的详细资料)。
grep()
在写法上不如 for
(嗯,技术上说是
foreach
啦)回圈。
假设有一种怪异的火星语编码协定,其中每两个大写的 ASCII字母代表一个火星 字母 (譬如 ``CV''这两个位元组代表一个火星字母,就像 ``SG''、``VS''、``XX'',等双字元组一样)。至於其它位元则和在 ASCII 里一样表示单一字元。
所以,像 ``I am CVSGXX!''这样的火星字串用了 12个位元去表示九个字母 'I',' ' ,'a','m',' ','CV','SG','XX','!'。
现在假设你要搜索这个字母:/GX/
。Perl并不懂火星语,所以它会找到 ``I am CVSGXX!''中 ``GX''
这两个位元,即使事实上这个字母并不在其中:它之所以看来像是在那儿是因为
``SG''和 ``XX''紧临在一起罢了,实际上并非真有 ``GX''存在。这是个大问题。
以下有些处理的方法,虽然都得付出不少代价:
$martian =~ s/([A-Z][A-Z])/ $1 /g; #让相邻的「火星」位元不再相邻 print "找到 GX了!\n" if $martian =~ /GX/;
或像这样:
@chars = $martian =~ m/([A-Z][A-Z]|[^A-Z])/g; #上面那行在理念上近似於: @chars = $text =~ m/(.)/g; # foreach $char (@chars) { print "找到 GX了!\n", last if $char eq 'GX'; }
这样也可以:
while ($martian =~ m/\G([A-Z][A-Z]|.)/gs) { #也许不需要 \G print "找到 GX了!\n", last if $1 eq 'GX'; }
不然乾脆这样:
die "对不起,Perl尚未支援火星文 )-:\n";
除此之外,CPAN里面有个范例程式能将半宽 (half-width)的片假名转成全宽 (full-width) [以 Shift-JIS或 EUC编码的],这是拜 Tom之赐才有的成果。
现在已有很多双 (和多)位元编码法被广泛的采用。这些方法中有些是采用 1-,2-, 3-,及 4位元组字母,混合使用。
译者:陈彦铭
中译版着作权所有:陈彦铭、萧百龄及两只老虎工作室 。本中译版遵守并使用与原文版相同的使用条款发行。