目錄

NAME

perlfaq6 - 正則表示式

VERSION

版本 5.20210520

DESCRIPTION

此部分意外地小,因為 FAQ 的其他部分散布著許多與正則表示式相關的解答。例如,解碼 URL 和檢查某個東西是否為數字可以使用正則表示式處理,但這些解答可以在此文件中的其他地方找到(在 perlfaq9:「如何在網路上解碼或建立那些 %- 編碼」和 perlfaq4:「如何判斷一個純量是否為數字/整數/浮點數」,精確來說)。

如何在不建立難以辨識且難以維護的程式碼的情況下使用正則表示式?

有三個技巧可以讓正則表示式易於維護和理解。

正則表示式外的註解

使用一般的 Perl 註解說明您正在做什麼以及如何執行。

# turn the line into the first word, a colon, and the
# number of characters on the rest of the line
s/^(\w+)(.*)/ lc($1) . ":" . length($2) /meg;
正則表示式內的註解

/x 修飾詞會讓正則表示式模式中的空白被忽略(字元類別和少數其他地方除外),而且也允許您在那裡使用一般的註解。正如您所想像的,空白和註解非常有幫助。

/x 讓您可以將這個

s{<(?:[^>'"]*|".*?"|'.*?')+>}{}gs;

變成這個

s{ <                    # opening angle bracket
    (?:                 # Non-backreffing grouping paren
        [^>'"] *        # 0 or more things that are neither > nor ' nor "
            |           #    or else
        ".*?"           # a section between double quotes (stingy match)
            |           #    or else
        '.*?'           # a section between single quotes (stingy match)
    ) +                 #   all occurring one or more times
    >                   # closing angle bracket
}{}gsx;                 # replace with nothing, i.e. delete

它仍然不像散文那麼清楚,但對於描述模式中每個部分的意義非常有用。

不同的分隔符號

雖然我們通常認為模式會以 / 字元區分,但它們幾乎可以用任何字元區分。 perlre 有說明這一點。例如,上面的 s/// 使用大括號作為區分符號。選擇其他區分符號可以避免在模式中引用區分符號

s/\/usr\/local/\/usr\/share/g;    # bad delimiter choice
s#/usr/local#/usr/share#g;        # better

使用邏輯配對的區分符號甚至更具可讀性

s{/usr/local/}{/usr/share}g;      # better still

我在比一行還長的範圍內進行比對時遇到問題。出了什麼問題?

可能是你檢視的字串中沒有超過一行(很可能),或者你沒有在模式中使用正確的修飾詞(有可能)。

有許多方法可以將多行資料放入字串中。如果你希望在讀取輸入時自動執行此動作,你會想要設定 $/(可能是段落的 '' 或整個檔案的 undef),以便一次讀取超過一行。

請閱讀 perlre,以協助你決定要使用 /s/m 中的哪一個(或同時使用):/s 允許點包含換行符號,而 /m 允許插入符號和美元符號與換行符號相鄰,而不仅仅出現在字串的結尾。你確實需要確保字串中實際上有多行。

例如,這個程式會偵測重複的字詞,即使它們跨越換行符號(但不會跨越段落符號)。對於這個範例,我們不需要 /s,因為我們在正規表示式中沒有使用點,而我們希望它能跨越換行符號。我們也不需要 /m,因為我們不希望插入符號或美元符號在記錄中任何與換行符號相鄰的位置進行比對。但必須將 $/ 設定為預設值以外的值,否則我們實際上永遠不會讀取到多行記錄。

$/ = '';          # read in whole paragraph, not just one line
while ( <> ) {
    while ( /\b([\w'-]+)(\s+\g1)+\b/gi ) {     # word starts alpha
        print "Duplicate $1 at paragraph $.\n";
    }
}

以下是一些會找出以「From 」開頭的句子(許多郵件程式會破壞這些句子)的程式碼

$/ = '';          # read in whole paragraph, not just one line
while ( <> ) {
    while ( /^From /gm ) { # /m makes ^ match next to \n
    print "leading From in paragraph $.\n";
    }
}

以下是一些會找出段落中 START 和 END 之間所有內容的程式碼

undef $/;          # read in whole file, not just one line or paragraph
while ( <> ) {
    while ( /START(.*?)END/sgm ) { # /s makes . cross line boundaries
        print "$1\n";
    }
}

我如何找出兩個模式之間的行,而這兩個模式本身位於不同的行上?

你可以使用 Perl 有點特別的 .. 算子(在 perlop 中有說明)

perl -ne 'print if /START/ .. /END/' file1 file2 ...

如果你想要文字而不是行,你可以使用

perl -0777 -ne 'print "$1\n" while /START(.*?)END/gs' file1 file2 ...

但是如果您想要 STARTEND 的巢狀出現,您會遇到本節中關於匹配平衡文字問題一問中所描述的問題。

以下是使用 .. 的另一個範例

while (<>) {
    my $in_header =   1  .. /^$/;
    my $in_body   = /^$/ .. eof;
# now choose between them
} continue {
    $. = 0 if eof;    # fix $.
}

如何使用 regex 比對 XML、HTML 或其他令人討厭、醜陋的東西?

不要使用 regex。使用模組並忘記正規表示式。XML::LibXMLHTML::TokeParserHTML::TreeBuilder 模組是良好的起點,儘管每個命名空間都有其他針對特定任務和不同執行方式而特製的剖析模組。從 CPAN 搜尋 ( http://metacpan.org/ ) 開始,並驚嘆於人們已經為您完成的所有工作! :)

我將正規表示式放入 $/,但它不起作用。出了什麼問題?

$/ 必須是字串。如果您真的需要執行此操作,可以使用這些範例。

如果您有 File::Stream,這很容易。

use File::Stream;

my $stream = File::Stream->new(
    $filehandle,
    separator => qr/\s*,\s*/,
    );

print "$_\n" while <$stream>;

如果您沒有 File::Stream,您必須多做一點工作。

您可以使用 sysread 的四個引數形式持續新增至緩衝區。在將內容新增至緩衝區後,您檢查您是否有一個完整行(使用您的正規表示式)。

local $_ = "";
while( sysread FH, $_, 8192, length ) {
    while( s/^((?s).*?)your_pattern// ) {
        my $record = $1;
        # do stuff here.
    }
}

如果您不介意整個檔案最後都在記憶體中,您可以對 foreach 和使用 c 旗標和 \G錨定的比對執行相同操作。

local $_ = "";
while( sysread FH, $_, 8192, length ) {
    foreach my $record ( m/\G((?s).*?)your_pattern/gc ) {
        # do stuff here.
    }
    substr( $_, 0, pos ) = "" if pos;
}

如何在保留 RHS 大小的同時,對 LHS 進行不分大小寫的替換?

以下是 Larry Rosler 提供的可愛的 Perlish 解决方案。它利用了 ASCII 字串上按位元異或的特性。

$_= "this is a TEsT case";

$old = 'test';
$new = 'success';

s{(\Q$old\E)}
{ uc $new | (uc $1 ^ $1) .
    (uc(substr $1, -1) ^ substr $1, -1) x
    (length($new) - length $1)
}egi;

print;

以下是作為子常式,仿照上述範例

sub preserve_case {
    my ($old, $new) = @_;
    my $mask = uc $old ^ $old;

    uc $new | $mask .
        substr($mask, -1) x (length($new) - length($old))
}

$string = "this is a TEsT case";
$string =~ s/(test)/preserve_case($1, "success")/egi;
print "$string\n";

這會列印

this is a SUcCESS case

或者,要保留替換字詞的大小寫(如果它比原始字詞長),您可以使用 Jeff Pinyan 的這段程式碼

sub preserve_case {
    my ($from, $to) = @_;
    my ($lf, $lt) = map length, @_;

    if ($lt < $lf) { $from = substr $from, 0, $lt }
    else { $from .= substr $to, $lf }

    return uc $to | ($from ^ uc $from);
}

這會將句子變更為「this is a SUcCess case」。

只是為了顯示 C 程式設計師可以在任何程式語言中撰寫 C,如果您偏好更類似的 C 程式碼,下列指令碼讓替換與原來的字母大小寫相同,逐字相同。(它也比 Perlish 程式碼執行速度慢約 240%。)如果替換字元比要替換的字串多,最後一個字元的字母大小寫會用於替換的其餘部分。

# Original by Nathan Torkington, massaged by 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;
        }
    }
    # finish up with any remaining new (for when new is longer than 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;
}

如何讓 \w 符合國家字元集?

在您的指令碼中放入 use locale;。\w 字元類別取自目前的區域設定。

請參閱 perllocale 了解詳細資訊。

如何符合 /[a-zA-Z]/ 的區域設定智慧版本?

您可以使用 perlre 中記載的 POSIX 字元類別語法 /[[:alpha:]]/

不論您在什麼區域設定中,字母字元都是 \w 中的字元,不含數字和底線。作為正規表示式,它看起來像 /[^\W\d_]/。它的補數,非字母字元,就是 \W 中的所有內容,以及數字和底線,或 /[\W\d_]/

如何引用變數以在正規表示式中使用?

Perl 剖析器會在正規表示式中擴充 $variable 和 @variable 參照,除非分隔符號是單引號。也要記得,s/// 替換的右側被視為雙引號字串(請參閱 perlop 了解更詳細的資訊)。也要記得,任何正規表示式特殊字元都會被作用,除非您在替換前面加上 \Q。以下是一個範例

$string = "Placido P. Octopus";
$regex  = "P.";

$string =~ s/$regex/Polyp/;
# $string is now "Polypacido P. Octopus"

因為 . 在正規表示式中是特殊的,且可以符合任何單一字元,所以此處的正規表示式 P. 已經符合原始字串中的 <Pl>。

若要跳脫 . 的特殊意義,我們使用 \Q

$string = "Placido P. Octopus";
$regex  = "P.";

$string =~ s/\Q$regex/Polyp/;
# $string is now "Placido Polyp Octopus"

使用 \Q 會讓 regex 中的 . 被視為一般字元,所以 P. 會配對一個 P 後面接一個點。

/o 到底是用來做什麼的?

(由 brian d foy 提供)

正規表示式的 /o 選項(在 perlopperlreref 中有說明)是告訴 Perl 只編譯一次正規表示式。這只在樣式中包含變數時才有用。Perl 5.6 和更新版本如果樣式沒有變動,會自動處理這件事。

由於比對運算子 m//、替換運算子 s/// 和正規表示式引用運算子 qr// 都是雙引號建構,因此你可以將變數插入樣式中。請參閱「我如何引用變數以用在 regex 中?」的解答,以取得更多詳細資料。

這個範例從引數清單中取得一個正規表示式,並印出與之配對的輸入列

my $pattern = shift @ARGV;

while( <> ) {
    print if m/$pattern/;
}

在 Perl 5.6 之前的版本中,即使 $pattern 沒有變動,也會在每次反覆運算中重新編譯正規表示式。/o 會透過告訴 Perl 在第一次時編譯樣式,然後在後續反覆運算中重複使用它來防止這種情況發生

my $pattern = shift @ARGV;

while( <> ) {
    print if m/$pattern/o; # useful for Perl < 5.6
}

在 Perl 5.6 和更新版本中,如果變數沒有變動,Perl 就不會重新編譯正規表示式,因此你可能不需要 /o 選項。它沒有壞處,但也不會有幫助。如果你想要任何版本的 Perl 只編譯一次正規表示式,即使變數變動(因此只使用它的初始值),你仍然需要 /o

你可以觀察 Perl 的正規表示式引擎運作,以自行驗證 Perl 是否正在重新編譯正規表示式。use re 'debug' 實用模組(隨附在 Perl 5.005 和更新版本中)會顯示詳細資料。在 Perl 5.6 之前的版本中,你應該會看到 re 報告它在每次反覆運算中編譯正規表示式。在 Perl 5.6 或更新版本中,你應該只會看到 re 在第一次反覆運算時報告這件事。

use re 'debug';

my $regex = 'Perl';
foreach ( qw(Perl Java Ruby Python) ) {
    print STDERR "-" x 73, "\n";
    print STDERR "Trying $_...\n";
    print STDERR "\t$_ is good!\n" if m/$regex/;
}

如何使用正規表示法從檔案中移除 C 式註解?

雖然這的確可以做到,但比你想像的難得多。例如,這行單行指令

perl -0777 -pe 's{/\*.*?\*/}{}gs' foo.c

在許多情況下都能運作,但並非所有情況。你會發現,對於某些類型的 C 程式而言,它太過簡陋,特別是那些在引號字串中看似有註解的程式。對於這種情況,你需要使用 Jeffrey Friedl 建立、Fred Curtis 後來修改的類似程式。

$/ = undef;
$_ = <>;
s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $2 ? $2 : ""#gse;
print;

當然,這可以使用 /x 修飾詞寫得更易於閱讀,加上空白和註解。以下是 Fred Curtis 提供的擴充版本。

s{
   /\*         ##  Start of /* ... */ comment
   [^*]*\*+    ##  Non-* followed by 1-or-more *'s
   (
     [^/*][^*]*\*+
   )*          ##  0-or-more things which don't start with /
               ##    but do end with '*'
   /           ##  End of /* ... */ comment

 |         ##     OR  various things which aren't comments:

   (
     "           ##  Start of " ... " string
     (
       \\.           ##  Escaped char
     |               ##    OR
       [^"\\]        ##  Non "\
     )*
     "           ##  End of " ... " string

   |         ##     OR

     '           ##  Start of ' ... ' string
     (
       \\.           ##  Escaped char
     |               ##    OR
       [^'\\]        ##  Non '\
     )*
     '           ##  End of ' ... ' string

   |         ##     OR

     .           ##  Anything other char
     [^/"'\\]*   ##  Chars which doesn't start a comment, string or escape
   )
 }{defined $2 ? $2 : ""}gxse;

透過使用換行字元,略微修改後也能移除 C++ 註解,這些註解可能跨越多行

s#/\*[^*]*\*+([^/*][^*]*\*+)*/|//([^\\]|[^\n][\n]?)*?\n|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $3 ? $3 : ""#gse;

我可以使用 Perl 正規表示法來比對平衡文字嗎?

(由 brian d foy 提供)

你第一次嘗試時,可能會使用 Text::Balanced 模組,它自 Perl 5.8 起就包含在 Perl 標準函式庫中。它有許多函式可以處理棘手的文字。Regexp::Common 模組也可以提供你可以使用的罐頭模式,來提供協助。

自 Perl 5.10 起,你可以使用遞迴模式,以正規表示法比對平衡文字。在 Perl 5.10 之前,你必須使用各種技巧,例如在 (??{}) 序列中使用 Perl 程式碼。

以下是使用遞迴正規表示法的範例。目標是要擷取尖括號中的所有文字,包括巢狀尖括號中的文字。這個範例文字有兩個「主要」群組:一個群組有巢狀一層,另一個群組有巢狀兩層。尖括號中共有五個群組

I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.

用於比對平衡文字的正規表示法使用了兩個 Perl 5.10 的新正規表示法功能。這些功能在 perlre 中有說明,而這個範例是該文件中的範例的修改版本。

首先,將新的佔有 + 加到任何量詞中,會找到最長的比對,而且不會回溯。這很重要,因為你想要透過遞迴處理任何尖括號,而不是回溯。群組 [^<>]++ 會找到一個或多個非尖括號,而且不會回溯。

其次,新的 (?PARNO) 指的是 PARNO 給定的特定擷取群組中的子模式。在以下正規表示式中,第一個擷取群組會尋找(並記住)平衡的文字,而且您需要在第一個緩衝區中使用相同的模式才能通過巢狀文字。那是遞迴的部分。(?1) 將外部擷取群組中的模式用作正規表示式的獨立部分。

將所有內容組合在一起,您會得到

#!/usr/local/bin/perl5.10.0

my $string =<<"HERE";
I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.
HERE

my @groups = $string =~ m/
        (                   # start of capture group 1
        <                   # match an opening angle bracket
            (?:
                [^<>]++     # one or more non angle brackets, non backtracking
                  |
                (?1)        # found < or >, so recurse to capture group 1
            )*
        >                   # match a closing angle bracket
        )                   # end of capture group 1
        /xg;

$" = "\n\t";
print "Found:\n\t@groups\n";

輸出顯示 Perl 找到了兩個主要群組

Found:
    <brackets in <nested brackets> >
    <another group <nested once <nested twice> > >

只要多做一點工作,您就可以取得所有使用尖括號括起來的群組,即使它們也出現在其他尖括號中。每次取得平衡的配對時,移除其外部分隔符號(那是您剛剛配對到的,所以不要再次配對它),並將其加入要處理的字串佇列。持續執行此動作,直到沒有配對為止

#!/usr/local/bin/perl5.10.0

my @queue =<<"HERE";
I have some <brackets in <nested brackets> > and
<another group <nested once <nested twice> > >
and that's it.
HERE

my $regex = qr/
        (                   # start of bracket 1
        <                   # match an opening angle bracket
            (?:
                [^<>]++     # one or more non angle brackets, non backtracking
                  |
                (?1)        # recurse to bracket 1
            )*
        >                   # match a closing angle bracket
        )                   # end of bracket 1
        /x;

$" = "\n\t";

while( @queue ) {
    my $string = shift @queue;

    my @groups = $string =~ m/$regex/g;
    print "Found:\n\t@groups\n\n" if @groups;

    unshift @queue, map { s/^<//; s/>$//; $_ } @groups;
}

輸出顯示所有群組。最外層的配對會先顯示,而巢狀配對會稍後顯示

Found:
    <brackets in <nested brackets> >
    <another group <nested once <nested twice> > >

Found:
    <nested brackets>

Found:
    <nested once <nested twice> >

Found:
    <nested twice>

正規表示式貪婪是什麼意思?我該如何解決?

大多數人認為貪婪的正規表示式會盡可能配對。技術上來說,實際上是量詞(?*+{})貪婪,而不是整個模式;Perl 偏好局部貪婪和立即滿足,而不是整體貪婪。若要取得相同量詞的非貪婪版本,請使用(??*?+?{}?)。

範例

my $s1 = my $s2 = "I am very very cold";
$s1 =~ s/ve.*y //;      # I am cold
$s2 =~ s/ve.*?y //;     # I am very cold

請注意,第二個取代在遇到「y 」時便停止配對。*? 量詞有效地告訴正規表示式引擎盡可能快地找到配對,並將控制權傳遞給下一行,就像您在玩丟手帕遊戲一樣。

我該如何處理每一行中的每個字詞?

使用 split 函數

while (<>) {
    foreach my $word ( split ) {
        # do something with $word here
    }
}

請注意,這在英文意義上並非真正的字詞;它只是連續非空白字元的區塊。

若要只使用字母數字順序(包括底線),您可以考慮

while (<>) {
    foreach $word (m/(\w+)/g) {
        # do something with $word here
    }
}

我該如何列印出字詞頻率或行頻率摘要?

為執行此操作,您必須分析輸入串流中的每個字詞。我們假設您所謂的字詞是指字母、連字號或撇號的區塊,而非前一個問題中所述的非空白區塊字詞概念

my (%seen);
while (<>) {
    while ( /(\b[^\W_\d][\w'-]+\b)/g ) {   # misses "`sheep'"
        $seen{$1}++;
    }
}

while ( my ($word, $count) = each %seen ) {
    print "$count $word\n";
}

如果您想要對行數執行相同操作,則不需要正規表示式

my (%seen);

while (<>) {
    $seen{$_}++;
}

while ( my ($line, $count) = each %seen ) {
    print "$count $line";
}

如果您希望這些輸出按已排序順序顯示,請參閱 perlfaq4:「如何排序雜湊(選擇依據值而非鍵值排序)?」。

如何執行近似比對?

請參閱可從 CPAN 取得的模組 String::Approx

如何有效率地同時比對多個正規表示式?

(由 brian d foy 提供)

您應該避免在每次想要比對時編譯正規表示式。在此範例中,由於 $pattern 可能會變更,因此 Perl 必須在 foreach 迴圈的每次反覆運算中重新編譯正規表示式

my @patterns = qw( fo+ ba[rz] );

LINE: while( my $line = <> ) {
    foreach my $pattern ( @patterns ) {
        if( $line =~ m/\b$pattern\b/i ) {
            print $line;
            next LINE;
        }
    }
}

qr// 算子會編譯正規表示式,但不會套用。當您使用正規表示式的預編譯版本時,Perl 執行的動作會減少。在此範例中,我插入一個 map 將每個模式轉換為其預編譯形式。腳本的其餘部分相同,但速度更快

my @patterns = map { qr/\b$_\b/i } qw( fo+ ba[rz] );

LINE: while( my $line = <> ) {
    foreach my $pattern ( @patterns ) {
        if( $line =~ m/$pattern/ ) {
            print $line;
            next LINE;
        }
    }
}

在某些情況下,您可能可以將多個模式轉換為單一正規表示式。但請注意需要回溯的情況。在此範例中,正規表示式只編譯一次,因為 $regex 在反覆運算之間不會變更

my $regex = join '|', qw( fo+ ba[rz] );

while( my $line = <> ) {
    print if $line =~ m/\b(?:$regex)\b/i;
}

CPAN 中 Data::Munge 中的函數「list2re」也可拿來形成單一正規表示式,用以比對字串清單(而非正規表示式)。

有關正規表示式效率的更多詳細資料,請參閱 Jeffrey Friedl 所著的《Mastering Regular Expressions》。他說明正規表示式引擎如何運作,以及為什麼某些模式會意外地沒有效率。一旦您了解 Perl 如何套用正規表示式,您就可以針對個別情況調整正規表示式。

為什麼使用 \b 的字詞邊界搜尋對我無效?

(由 brian d foy 提供)

請確定您知道 \b 的實際作用:它是字元字元 \w 和非字元字元的邊界。這個非字元字元可能是 \W,但也可以是字串的開頭或結尾。

它(不是!)空白字元和非空白字元的邊界,也不是我們用來建構句子字詞之間的內容。

在正規表示式中,字詞邊界 (\b) 是「零寬度斷言」,表示它不代表字串中的字元,而是在特定位置的條件。

對於正規表示式 /\bPerl\b/,在「P」之前和「l」之後必須有字元邊界。只要在「P」之前和「l」之後有非字元字元,模式就會相符。這些字串相符 /\bPerl\b/。

"Perl"    # no word char before "P" or after "l"
"Perl "   # same as previous (space is not a word char)
"'Perl'"  # the "'" char is not a word char
"Perl's"  # no word char before "P", non-word char after "l"

這些字串不相符 /\bPerl\b/。

"Perl_"   # "_" is a word char!
"Perler"  # no word char before "P", but one after "l"

您不必使用 \b 來相符字詞。您可以尋找被字元字元包圍的非字元字元。這些字串相符模式 /\b'\b/。

"don't"   # the "'" char is surrounded by "n" and "t"
"qep'a'"  # the "'" char is surrounded by "p" and "a"

這些字串不相符 /\b'\b/。

"foo'"    # there is no word char after non-word "'"

您也可以使用 \b 的補數 \B,來指定不應該有字元邊界。

在模式 /\Bam\B/ 中,「a」之前和「m」之後必須有字元字元。這些模式相符 /\Bam\B/

"llama"   # "am" surrounded by word chars
"Samuel"  # same

這些字串不相符 /\Bam\B/

"Sam"      # no word boundary before "a", but one after "m"
"I am Sam" # "am" surrounded by non-word chars

為什麼使用 $&、$` 或 $' 會讓我的程式變慢?

(由 Anno Siegel 提供)

一旦 Perl 看到您在程式中的任何地方需要這些變數之一,它就會在每次模式相符時提供它們。這表示在每次模式相符時,整個字串都會被複製,一部分到 $`,一部分到 $&,一部分到 $'。因此,當字串很長且模式經常相符時,懲罰最為嚴重。如果您能的話,請避免使用 $&、$' 和 $`,但如果您不能,一旦您使用它們,就可以隨意使用,因為您已經付出了代價。請記住,有些演算法真的很感謝它們。從 5.005 版本開始,$& 變數不再像其他兩個變數那樣「昂貴」。

從 Perl 5.6.1 開始,特殊變數 @- 和 @+ 可以功能性地取代 $`、$& 和 $'。這些陣列包含指向每個相符項開始和結束的指標(有關完整說明,請參閱 perlvar),因此它們基本上提供相同的資訊,但沒有過度複製字串的風險。

Perl 5.10 新增了三個特殊變數,${^MATCH}${^PREMATCH}${^POSTMATCH},用來執行相同的任務,但沒有整體效能損失。Perl 5.10 只有在您使用 /p 修飾詞編譯或執行正規表示式時才會設定這些變數。

正規表示式中的 \G 有什麼好處?

您使用 \G錨點從上次比對結束的同一個字串開始下一次比對。正規表示式引擎無法跳過任何字元來尋找下一個比對,因此 \G 類似於字串開頭錨點 ^\G 錨點通常與 g 修飾詞一起使用。它使用 pos() 的值作為開始下一次比對的位置。當比對運算子進行連續比對時,它會使用最後一次比對後的下一個字元的位址來更新 pos()(或下一次比對的第一個字元,視您如何看待它)。每個字串都有自己的 pos() 值。

假設您想要比對像「1122a44」這樣的字串中所有連續的數字對,並在遇到非數字時停止比對。您想要比對 1122,但字母 a 出現在 2244 之間,您想要在 a 處停止。只比對數字對會跳過 a 並仍然比對 44

$_ = "1122a44";
my @pairs = m/(\d\d)/g;   # qw( 11 22 44 )

如果您使用 \G 錨點,您會強制 22 之後的比對從 a 開始。正規表示式無法在那裡比對,因為它找不到數字,因此下一次比對會失敗,而比對運算子會傳回它已經找到的對。

$_ = "1122a44";
my @pairs = m/\G(\d\d)/g; # qw( 11 22 )

您也可以在純量內容中使用 \G 錨點。您仍然需要 g 修飾詞。

$_ = "1122a44";
while( m/\G(\d\d)/g ) {
    print "Found $1\n";
}

在字母 a 處比對失敗後,perl 會重設 pos(),而同一個字串上的下一次比對會從開頭開始。

$_ = "1122a44";
while( m/\G(\d\d)/g ) {
    print "Found $1\n";
}

print "Found $1 after while" if m/(\d\d)/g; # finds "11"

您可以使用在 perlopperlreref 中記載的 c 修飾詞來停用 pos() 在失敗時重設。後續的比對會從上一個成功比對結束的地方 (pos() 的值) 開始,即使在同一個字串上進行的比對在此期間失敗。在此情況下,while() 迴圈後的比對會從 a (上一個比對停止的地方) 開始,而且由於它沒有使用任何錨定,因此它可以略過 a 來尋找 44

$_ = "1122a44";
while( m/\G(\d\d)/gc ) {
    print "Found $1\n";
}

print "Found $1 after while" if m/(\d\d)/g; # finds "44"

通常,當您想要在一個比對失敗時嘗試不同的比對時,例如在一個分詞器中,您會將 \G 錨定與 c 修飾詞搭配使用。Jeffrey Friedl 提供了這個範例,它可以在 5.004 或更新版本中運作。

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; };
    }
}

對於每一行,PARSER 迴圈會先嘗試比對一系列的數字,接著是一個字詞邊界。這個比對必須從上一個比對結束的地方 (或在第一次比對時從字串的開頭) 開始。由於 m/ \G( \d+\b )/gcx 使用了 c 修飾詞,如果字串不符合那個正規表示式,perl 就不會重設 pos(),而下一個比對會從同一個位置開始,嘗試不同的模式。

Perl 正規表示式是 DFA 還是 NFA?它們符合 POSIX 嗎?

雖然 Perl 的正規表示式類似於 egrep(1) 程式中的 DFA (確定性有限狀態自動機),但它們實際上是作為 NFA (非確定性有限狀態自動機) 來實作,以允許回溯和反向參照。而且它們也不是 POSIX 風格的,因為那些會保證在所有情況下都有最差情況的行為。(看來有些人比較喜歡一致性的保證,即使保證的是緩慢。)請參閱 Jeffrey Friedl 所著的「Mastering Regular Expressions」(來自 O'Reilly) 一書,以取得關於這些事項的所有您希望知道的詳細資訊 (完整的引文出現在 perlfaq2 中)。

在空語境中使用 grep 有什麼問題?

問題在於 grep 會建立一個回傳清單,不論語境為何。這表示您讓 Perl 費心建立一個清單,然後您就把它丟掉。如果清單很大,您會浪費時間和空間。如果您的目的是要迭代清單,那麼請使用 for 迴圈來執行這個目的。

在早於 5.8.1 的 perl 中,map 也會遇到這個問題。但是自 5.8.1 以後,這個問題已經修正,而 map 具有語境感知能力 - 在空語境中,不會建構任何清單。

如何比對包含多位元組字元的字串?

從 Perl 5.6 開始,Perl 已支援一定程度的多位元組字元。建議使用 Perl 5.8 或更新版本。支援的多位元組字元集包括 Unicode,以及透過 Encode 模組的舊式編碼。請參閱 perluniintroperlunicodeEncode

如果您受限於舊版 Perl,您可以使用 Unicode::String 模組來處理 Unicode,並使用 Unicode::Map8Unicode::Map 模組進行字元轉換。如果您使用日文編碼,可以嘗試使用 jperl 5.005_03。

最後,以下是 Jeffrey Friedl 提供的一組方法,他在 The Perl Journal 第 5 期中的一篇文章討論了這個問題。

假設您有一種奇怪的火星編碼,其中成對的 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」。這是一個大問題。

以下列出一些處理此問題的方法,但都很麻煩

# Make sure adjacent "martian" bytes are no longer adjacent.
$martian =~ s/([A-Z][A-Z])/ $1 /g;

print "found GX!\n" if $martian =~ /GX/;

或者像這樣

my @chars = $martian =~ m/([A-Z][A-Z]|[^A-Z])/g;
# above is conceptually similar to:     my @chars = $text =~ m/(.)/g;
#
foreach my $char (@chars) {
    print "found GX!\n", last if $char eq 'GX';
}

或者像這樣

while ($martian =~ m/\G([A-Z][A-Z]|.)/gs) {  # \G probably unneeded
    if ($1 eq 'GX') {
        print "found GX!\n";
        last;
    }
}

以下是 Benjamin Goldberg 提供的另一種稍微不那麼麻煩的方法,他使用零寬度否定後向斷言。

print "found GX!\n" if    $martian =~ m/
    (?<![A-Z])
    (?:[A-Z][A-Z])*?
    GX
    /x;

如果火星字元 GX 在字串中,此方法會成功,否則會失敗。如果您不喜歡使用 (?<!),即零寬度否定後向斷言,您可以將 (?<![A-Z]) 替換為 (?:^|[^A-Z])。

它的缺點是會在 $-[0] 和 $+[0] 中放入錯誤的內容,但通常可以解決這個問題。

如何比對儲存在變數中的正規表示式?

(由 brian d foy 提供)

我們不必將模式硬編碼到 match 運算子(或任何使用正規表示式的其他內容)中。我們可以將模式放入變數中以供後續使用。

match 運算子是雙引號內容,因此你可以像雙引號字串一樣內插你的變數。在此情況下,你將正規表示式讀取為使用者輸入並將其儲存在 $regex 中。一旦你在 $regex 中有模式,你就可以在 match 運算子中使用該變數。

chomp( my $regex = <STDIN> );

if( $string =~ m/$regex/ ) { ... }

$regex 中的任何正規表示式特殊字元仍然是特殊的,而且模式仍然必須有效,否則 Perl 會抱怨。例如,在此模式中有一個未配對的括號。

my $regex = "Unmatched ( paren";

"Two parens to bind them all" =~ m/$regex/;

當 Perl 編譯正規表示式時,它會將括號視為記憶體比對的開頭。當它找不到閉括號時,它會抱怨

Unmatched ( in regex; marked by <-- HERE in m/Unmatched ( <-- HERE  paren/ at script line 3.

你可以根據我們的狀況透過多種方式解決這個問題。首先,如果你不希望字串中的任何字元為特殊字元,你可以在使用字串之前使用 quotemeta 進行跳脫。

chomp( my $regex = <STDIN> );
$regex = quotemeta( $regex );

if( $string =~ m/$regex/ ) { ... }

你也可以使用 \Q\E 序列直接在 match 運算子中執行此操作。\Q 告訴 Perl 從哪裡開始跳脫特殊字元,而 \E 告訴它在哪裡停止(有關更多詳細資訊,請參閱 perlop)。

chomp( my $regex = <STDIN> );

if( $string =~ m/\Q$regex\E/ ) { ... }

或者,你可以使用 qr//,正規表示式引號運算子(有關更多詳細資訊,請參閱 perlop)。它會引號並可能編譯模式,而且你可以將正規表示式旗標套用於模式。

chomp( my $input = <STDIN> );

my $regex = qr/$input/is;

$string =~ m/$regex/  # same as m/$input/is;

你可能還想透過在整個內容周圍包裝一個 eval 區塊來捕捉任何錯誤。

chomp( my $input = <STDIN> );

eval {
    if( $string =~ m/\Q$input\E/ ) { ... }
};
warn $@ if $@;

或者...

my $regex = eval { qr/$input/is };
if( defined $regex ) {
    $string =~ m/$regex/;
}
else {
    warn $@;
}

作者和著作權

版權所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者,如註明。保留所有權利。

此文件是免費的;你可以在與 Perl 相同的條款下重新分發和/或修改它。

不論其散布方式,此檔案中的所有程式碼範例均在此置於公有領域。您被允許且鼓勵在您自己的程式中使用此程式碼以供娛樂或獲利之用,視您所見為合適。在程式碼中加上一個簡單的註解以示感謝會是禮貌的,但並非必要。