\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位元組字母,混合使用。
譯者:陳彥銘
中譯版著作權所有:陳彥銘、蕭百齡及兩隻老虎工作室 。本中譯版遵守並使用與原文版相同的使用條款發行。