內容

名稱

perlretut - Perl 正規表示式教學

說明

此頁面提供基礎教學,說明如何理解、建立和使用 Perl 中的正規表示式。它作為正規表示式參考頁面 perlre 的補充。正規表示式是 m//s///qr//split 算子的組成部分,因此本教學也涵蓋了 perlop 中的「Regexp Quote-Like Operators」perlfunc 中的「split」

Perl 以其在文字處理方面的卓越表現聞名,而正規表示式是這項盛名的幕後功臣之一。Perl 正規表示式展現了在其他大多數電腦語言中前所未見的效率和靈活性。即使只精通正規表示式的基礎知識,也能讓您輕鬆地處理文字。

什麼是正規表示式?正規表示式最基本的定義,就是用來判斷字串是否具備特定特徵的範本。字串通常是文字,例如一行、句子、網頁,甚至是一整本書,但它不一定是文字。例如,它可以是二進位資料。生物學家經常使用 Perl 來尋找長 DNA 序列中的模式。

假設我們想要確定變數 $var 中的文字是否包含字元序列 m u s h r o o m(為方便閱讀而加入空白)。我們可以用 Perl 寫成

$var =~ m/mushroom/

如果 $var 在其中任何地方包含該字元序列,此表達式的值將為 TRUE,否則為 FALSE。括在 '/' 字元中的部分表示我們正在尋找的特徵。我們稱之為樣式。尋找樣式是否出現在字串中的過程稱為比對,而 "=~" 算子與 m// 告訴 Perl 嘗試將樣式與字串比對。請注意,樣式也是字串,但正如我們將看到的,它是一種非常特殊的字串。樣式在當今社會中很常見;範例包括輸入搜尋引擎以尋找網頁的樣式,以及用於列出目錄中檔案的樣式,例如,"ls *.txt" 或 "dir *.*"。在 Perl 中,由正規表示式描述的樣式不僅用於搜尋字串,也用於擷取字串中所需的部份,以及執行搜尋和取代作業。

正規表示式有抽象且難以理解的無端名聲。這其實只是因為用於表達它們的符號傾向於簡潔且密集,而不是因為它們本身複雜。我們建議使用 /x 正規表示式修改器(如下所述)以及大量的空白,以減少它們的密度並讓它們更容易閱讀。正規表示式是使用條件式和迴圈等簡單概念建構的,而且它們並不比 Perl 語言本身對應的 if 條件式和 while 迴圈更難理解。

本教學課程透過逐一探討正規表示法概念及其符號,並提供許多範例,來縮短學習曲線。本教學課程的第一部分將由最簡單的字詞搜尋進展到基本的正規表示法概念。如果您能掌握第一部分,您將具備解決約 98% 需求所需的所有工具。本教學課程的第二部分適合已熟悉基礎知識且渴望獲得更多強大工具的人。它探討更進階的正規表示法運算子,並介紹最新的尖端創新。

註:為了節省時間,「正規表示法」通常縮寫為 regexp 或 regex。Regexp 是比 regex 更自然的縮寫,但較難發音。Perl pod 文件在 regexp 和 regex 之間平均分配;在 Perl 中,有多種方法可以縮寫它。我們將在本教學課程中使用 regexp。

v5.22 的新增功能 use re 'strict' 在編譯正規表示法模式時,會套用比其他情況更嚴格的規則。它可以找出一些合法的東西,但可能不是您想要的。

第 1 部分:基礎知識

簡單的字詞配對

最簡單的 regexp 僅是一個字詞,或更普遍地說,一個字元串。僅由一個字詞組成的 regexp 會配對包含該字詞的任何字串

"Hello World" =~ /World/;  # matches

這個 Perl 語句到底是什麼?"Hello World" 是個簡單的雙引號字串。World 是正規表示式,而包圍 /World/// 告訴 Perl 在字串中搜尋比對。運算子 =~ 將字串與正規表示式比對關聯起來,如果正規表示式比對成功,則產生真值,如果正規表示式比對失敗,則產生假值。在我們的例子中,World 比對 "Hello World" 中的第二個字,因此這個表示式為真。像這樣的表示式在條件式中很有用

if ("Hello World" =~ /World/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

這個主題有一些有用的變化。使用 !~ 運算子可以反轉比對的意義

if ("Hello World" !~ /World/) {
    print "It doesn't match\n";
}
else {
    print "It matches\n";
}

正規表示式中的字面字串可以用變數取代

my $greeting = "World";
if ("Hello World" =~ /$greeting/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

如果你要比對特殊預設變數 $_,則可以省略 $_ =~ 部分

$_ = "Hello World";
if (/World/) {
    print "It matches\n";
}
else {
    print "It doesn't match\n";
}

最後,比對的 // 預設分隔符號可以用任意分隔符號取代,方法是在前面加上 'm'

"Hello World" =~ m!World!;   # matches, delimited by '!'
"Hello World" =~ m{World};   # matches, note the paired '{}'
"/usr/bin/perl" =~ m"/perl"; # matches after '/usr/bin',
                             # '/' becomes an ordinary char

/World/m!World!m{World} 都代表相同的事物。例如,當引號 ('"') 用作分隔符號時,斜線 '/' 會變成一個普通字元,可以在這個正規表示式中毫無問題地使用。

讓我們來看看不同的正規表示式如何比對 "Hello World"

"Hello World" =~ /world/;  # doesn't match
"Hello World" =~ /o W/;    # matches
"Hello World" =~ /oW/;     # doesn't match
"Hello World" =~ /World /; # doesn't match

第一個正規表示式 world 沒有比對成功,因為正規表示式預設會區分大小寫。第二個正規表示式比對成功,因為子字串 'o W' 出現在字串 "Hello World" 中。正規表示式中的空白字元 ' ' 會像其他字元一樣處理,並且需要在這種情況下比對。沒有空白字元是第三個正規表示式 'oW' 沒有比對成功的原因。第四個正規表示式 "World " 沒有比對成功,因為正規表示式的結尾有一個空白,但字串的結尾沒有。這裡的教訓是,正規表示式必須完全比對字串的一部分,才能讓語句為真。

如果正規表示式在字串中多個地方比對成功,Perl 始終會在字串中最早可能的位置比對

"Hello World" =~ /o/;       # matches 'o' in 'Hello'
"That hat is red" =~ /hat/; # matches 'hat' in 'That'

關於字元比對,你需要知道更多一些重點。首先,並非所有字元都可以「原樣」用於比對。有些字元,稱為元字元,通常保留在正規表示式符號中使用。元字元包括

{}[]()^$.|*+?-#\

這個清單並不像看起來那麼確定(或聲稱在其他文件中的那樣)。例如,"#" 只有在使用 /x 模式修改器(如下所述)時才是元字元,而 "}""]" 只有在分別與開啟的 "{""[" 配對時才是元字元;其他陷阱適用。

這些的意義將在教學課程的其餘部分中說明,但目前,重要的是要知道,在反斜線之前加上反斜線,可以按原樣匹配元字元

"2+2=4" =~ /2+2/;    # doesn't match, + is a metacharacter
"2+2=4" =~ /2\+2/;   # matches, \+ is treated like an ordinary +
"The interval is [0,1)." =~ /[0,1)./     # is a syntax error!
"The interval is [0,1)." =~ /\[0,1\)\./  # matches
"#!/usr/bin/perl" =~ /#!\/usr\/bin\/perl/;  # matches

在最後一個正規表示式中,正斜線 '/' 也加上反斜線,因為它用於界定正規表示式。然而,這可能會導致 LTS(傾斜牙籤症候群),而且通常更易於讀取來變更分隔符號。

"#!/usr/bin/perl" =~ m!#\!/usr/bin/perl!;  # easier to read

反斜線字元 '\' 本身就是一個元字元,需要加上反斜線

'C:\WIN32' =~ /C:\\WIN/;   # matches

在特定元字元沒有意義表示其正常意義的情況下,它會自動失去其元字元特性,並變成要按字面意義匹配的普通字元。例如,'}' 僅在它是 '{' 元字元的配對時才是一個元字元。否則,它會被視為一個字面上的右大括號。這可能會導致意外的結果。use re 'strict' 可以捕捉其中一些。

除了元字元之外,還有一些 ASCII 字元沒有可列印字元等效項,而是由跳脫序列表示。常見的範例包括表示 tab 的 \t、表示換行符號的 \n、表示回車的 \r 和表示鈴聲(或警示)的 \a。如果您的字串更適合視為任意位元組序列,則八進位跳脫序列(例如 \033)或十六進位跳脫序列(例如 \x1B)可能是您位元組更自然的表示方式。以下是跳脫的一些範例

"1000\t2000" =~ m(0\t2)   # matches
"1000\n2000" =~ /0\n20/   # matches
"1000\t2000" =~ /\000\t2/ # doesn't match, "0" ne "\000"
"cat"   =~ /\o{143}\x61\x74/ # matches in ASCII, but a weird way
                             # to spell cat

如果您已經使用 Perl 一段時間,所有這些關於跳脫序列的討論可能會很熟悉。雙引號字串中使用類似的跳脫序列,事實上 Perl 中的正規表示式大多被視為雙引號字串。這表示變數也可以用於正規表示式。就像雙引號字串一樣,正規表示式中變數的值會在正規表示式評估以進行匹配之前替換。因此,我們有

$foo = 'house';
'housecat' =~ /$foo/;      # matches
'cathouse' =~ /cat$foo/;   # matches
'housecat' =~ /${foo}cat/; # matches

到目前為止,一切都很好。有了上述的知識,您已經可以使用幾乎任何您能想到的字面字串正規表示式來執行搜尋。以下是 Unix grep 程式的一個非常簡單的模擬

% cat > simple_grep
#!/usr/bin/perl
$regexp = shift;
while (<>) {
    print if /$regexp/;
}
^D

% chmod +x simple_grep

% simple_grep abba /usr/dict/words
Babbage
cabbage
cabbages
sabbath
Sabbathize
Sabbathizes
sabbatical
scabbard
scabbards

這個程式很容易理解。#!/usr/bin/perl 是從 shell 呼叫 perl 程式的標準方法。 $regexp = shift; 將第一個命令列引數儲存為要使用的 regexp,讓其餘的命令列引數作為檔案處理。 while (<>) 迴圈處理所有檔案中的所有行。對於每一行,print if /$regexp/; 如果 regexp 與該行相符,則印出該行。在此行中,print/$regexp/ 都隱含地使用預設變數 $_

對於以上所有 regexp,如果 regexp 與字串中的任何地方相符,則視為相符。然而,有時我們想要指定 regexp 應該在字串中的何處嘗試相符。為此,我們會使用錨定元字元 '^''$'。錨定 '^' 表示在字串開頭相符,而錨定 '$' 表示在字串結尾相符,或在字串結尾的新行之前相符。以下是它們的使用方式

"housekeeper" =~ /keeper/;    # matches
"housekeeper" =~ /^keeper/;   # doesn't match
"housekeeper" =~ /keeper$/;   # matches
"housekeeper\n" =~ /keeper$/; # matches

第二個 regexp 無法相符,因為 '^' 限制 keeper 只能在字串開頭相符,但 "housekeeper" 的 keeper 出現在中間。第三個 regexp 可以相符,因為 '$' 限制 keeper 只能在字串結尾相符。

當同時使用 '^''$' 時,regexp 必須與字串的開頭和結尾都相符,,regexp 與整個字串相符。考慮

"keeper" =~ /^keep$/;      # doesn't match
"keeper" =~ /^keeper$/;    # matches
""       =~ /^$/;          # ^$ matches an empty string

第一個 regexp 無法相符,因為字串比 keep 多。由於第二個 regexp 與字串完全相同,因此可以相符。在 regexp 中同時使用 '^''$' 會強制整個字串相符,因此可以完全控制哪些字串相符,哪些字串不相符。假設您正在尋找一個名叫 bert 的人,他單獨出現在一個字串中

"dogbert" =~ /bert/;   # matches, but not what you want

"dilbert" =~ /^bert/;  # doesn't match, but ..
"bertram" =~ /^bert/;  # matches, so still not good enough

"bertram" =~ /^bert$/; # doesn't match, good
"dilbert" =~ /^bert$/; # doesn't match, good
"bert"    =~ /^bert$/; # matches, perfect

當然,對於文字字串,可以輕鬆使用字串比較 $string eq 'bert',而且效率更高。^...$ regexp 在我們加入以下更強大的 regexp 工具時才真正變得有用。

使用字元類別

儘管已經可以使用上述文字字串 regexp 做很多事情,但我們只接觸到正規表示式技術的皮毛。在本節和後續各節中,我們將介紹 regexp 概念(和相關的元字元符號),這些概念將允許 regexp 不僅表示單一字元序列,還表示整個類別的字元序列。

其中一個概念是字元類別。字元類別允許一組可能的字元,而不僅僅只是一個字元,在正規表示法的特定點上進行比對。你可以定義你自己的自訂字元類別。這些類別以中括號 [...] 表示,其中包含可能比對的字元組。以下是幾個範例

/cat/;       # matches 'cat'
/[bcr]at/;   # matches 'bat, 'cat', or 'rat'
/item[0123456789]/;  # matches 'item0' or ... or 'item9'
"abc" =~ /[cab]/;    # matches 'a'

在最後一個陳述式中,即使 'c' 是類別中的第一個字元,'a' 仍會比對,因為字串中的第一個字元位置是正規表示法可以比對的最早點。

/[yY][eE][sS]/;      # match 'yes' in a case-insensitive way
                     # 'yes', 'Yes', 'YES', etc.

這個正規表示法顯示一個常見的任務:執行不分大小寫的比對。Perl 提供一種方法,可以透過在比對的結尾附加一個 'i' 來避免所有這些中括號。然後 /[yY][eE][sS]/; 可以改寫成 /yes/i;'i' 代表不分大小寫,並且是比對操作的修飾詞範例。我們稍後會在教學課程中看到其他修飾詞。

我們在上面的章節中看到有普通字元,代表它們自己,以及特殊字元,需要一個反斜線 '\' 來代表它們自己。在字元類別中也是如此,但字元類別內部和外部的普通字元和特殊字元組不同。字元類別的特殊字元是 -]\^$(以及模式分隔符,無論是什麼)。']' 是特殊的,因為它表示字元類別的結尾。'$' 是特殊的,因為它表示一個純量變數。'\' 是特殊的,因為它用於跳脫序列,就像上面一樣。以下是特殊字元 ]$\ 的處理方式

/[\]c]def/; # matches ']def' or 'cdef'
$x = 'bcr';
/[$x]at/;   # matches 'bat', 'cat', or 'rat'
/[\$x]at/;  # matches '$at' or 'xat'
/[\\$x]at/; # matches '\at', 'bat, 'cat', or 'rat'

最後兩個有點棘手。在 [\$x] 中,反斜線保護了美元符號,因此字元類別有兩個成員 '$''x'。在 [\\$x] 中,反斜線受到保護,因此 $x 被視為一個變數,並以雙引號的方式替換。

特殊字元 '-' 在字元類別中作為範圍運算子,因此可以將連續的字元集寫成一個範圍。使用範圍後,難以處理的 [0123456789][abc...xyz] 就會變成精簡的 [0-9][a-z]。以下是一些範例

/item[0-9]/;  # matches 'item0' or ... or 'item9'
/[0-9bx-z]aa/;  # matches '0aa', ..., '9aa',
                # 'baa', 'xaa', 'yaa', or 'zaa'
/[0-9a-fA-F]/;  # matches a hexadecimal digit
/[0-9a-zA-Z_]/; # matches a "word" character,
                # like those in a Perl variable name

如果 '-' 是字元類別中的第一個或最後一個字元,它會被視為一般字元;[-ab][ab-][a\-b] 都是等值的。

特殊字元 '^' 在字元類別的第一個位置表示否定字元類別,它會比對任何字元,但括號中的字元除外。[...][^...] 都必須比對一個字元,否則比對會失敗。然後

/[^a]at/;  # doesn't match 'aat' or 'at', but matches
           # all other 'bat', 'cat, '0at', '%at', etc.
/[^0-9]/;  # matches a non-numeric character
/[a^]at/;  # matches 'aat' or '^at'; here '^' is ordinary

現在,即使是 [0-9] 也可能需要多次撰寫,因此為了節省按鍵次數並讓正規表示式更易於閱讀,Perl 提供了幾個常見字元類別的縮寫,如下所示。自 Unicode 導入以來,除非 /a 修飾詞生效,否則這些字元類別比對的範圍不只 ASCII 範圍中的幾個字元。

/a 修飾詞(從 Perl 5.14 開始提供)用於將 \d\s\w 的比對範圍限制在 ASCII 範圍內。當你只想處理類似英文的文字時,它有助於讓你的程式不必不必要地暴露於完整的 Unicode(及其伴隨而來的安全性考量)。(「a」可以加倍,/aa,提供更多限制,防止 ASCII 與非 ASCII 字元進行不區分大小寫的比對;否則,Unicode「開爾文符號」將不區分大小寫地比對「k」或「K」。)

縮寫 \d\s\w\D\S\W 可同時用於括號字元類別的內部和外部。以下是部分用法

/\d\d:\d\d:\d\d/; # matches a hh:mm:ss time format
/[\d\s]/;         # matches any digit or whitespace character
/\w\W\w/;         # matches a word char, followed by a
                  # non-word char, followed by a word char
/..rt/;           # matches any two chars, followed by 'rt'
/end\./;          # matches 'end.'
/end[.]/;         # same thing, matches 'end.'

由於句點是元字元,因此必須跳脫才能比對為一般句點。例如,由於 \d\w 是字元組,因此將 [^\d\w] 視為 [\D\W] 是不正確的;事實上,[^\d\w] 等於 [^\w],而 [^\w] 等於 [\W]。請思考德摩根定律。

實際上,句點和 \d\s\w\D\S\W 縮寫本身就是字元類別的類型,因此括號括起來的那些只是其中一種字元類別。當我們需要區分時,會將它們稱為「括號字元類別」。

基本正規表示式中一個有用的錨定字元是字詞錨定字元 \b。它比對字詞字元和非字詞字元 \w\W\W\w 之間的界線

$x = "Housecat catenates house and cat";
$x =~ /cat/;    # matches cat in 'housecat'
$x =~ /\bcat/;  # matches cat in 'catenates'
$x =~ /cat\b/;  # matches cat in 'housecat'
$x =~ /\bcat\b/;  # matches 'cat' at end of string

請注意最後一個範例中,字串的結尾被視為字詞界線。

對於自然語言處理(例如,使單引號包含在字詞中),請改用 \b{wb}

"don't" =~ / .+? \b{wb} /x;  # matches the whole string

您可能會疑惑為什麼 '.' 比對所有字元,但 "\n" 除外 - 為什麼不是每個字元?原因是通常會針對行進行比對,並希望忽略換行字元。例如,字串 "\n" 雖然代表一行,但我們希望將其視為空字串。然後

""   =~ /^$/;    # matches
"\n" =~ /^$/;    # matches, $ anchors before "\n"

""   =~ /./;      # doesn't match; it needs a char
""   =~ /^.$/;    # doesn't match; it needs a char
"\n" =~ /^.$/;    # doesn't match; it needs a char other than "\n"
"a"  =~ /^.$/;    # matches
"a\n"  =~ /^.$/;  # matches, $ anchors before "\n"

這種行為很方便,因為我們通常希望在計算和比對一行中的字元時忽略換行字元。然而,有時我們想要追蹤換行字元。我們甚至可能希望 '^''$' 錨定在字串中各行的開頭和結尾,而不仅仅是字串的開頭和結尾。Perl 允許我們使用 /s/m 修飾詞在忽略和注意換行字元之間進行選擇。/s/m 分別代表單行和多行,它們決定字串是要視為一個連續的字串,還是視為一組行。這兩個修飾詞會影響正規表示式解譯的兩個面向:1) '.' 字元類別的定義方式,以及 2) 錨定字元 '^''$' 可以比對的位置。以下是四種可能的組合

以下是 /s/m 運作的範例

$x = "There once was a girl\nWho programmed in Perl\n";

$x =~ /^Who/;   # doesn't match, "Who" not at start of string
$x =~ /^Who/s;  # doesn't match, "Who" not at start of string
$x =~ /^Who/m;  # matches, "Who" at start of second line
$x =~ /^Who/sm; # matches, "Who" at start of second line

$x =~ /girl.Who/;   # doesn't match, "." doesn't match "\n"
$x =~ /girl.Who/s;  # matches, "." matches "\n"
$x =~ /girl.Who/m;  # doesn't match, "." doesn't match "\n"
$x =~ /girl.Who/sm; # matches, "." matches "\n"

大部分時間,預設行為是想要的,但 /s/m 有時非常有用。如果使用 /m,字串開頭仍可以用 \A 符合,而字串結尾仍可以用錨點 \Z (同時符合結尾和之前的換行符號,就像 '$') 和 \z (只符合結尾) 符合

$x =~ /^Who/m;   # matches, "Who" at start of second line
$x =~ /\AWho/m;  # doesn't match, "Who" is not at start of string

$x =~ /girl$/m;  # matches, "girl" at end of first line
$x =~ /girl\Z/m; # doesn't match, "girl" is not at end of string

$x =~ /Perl\Z/m; # matches, "Perl" is at newline before end
$x =~ /Perl\z/m; # doesn't match, "Perl" is not at end of string

我們現在知道如何在正規表示法中建立字元類別的選項。那字詞或字串的選項呢?此類選項會在下一個區段說明。

符合這個或那個

有時候我們希望我們的正規表示法能夠符合不同的可能字詞或字串。這是透過使用交替後設字元 '|' 來完成的。若要符合 dogcat,我們形成正規表示法 dog|cat。和之前一樣,Perl 會嘗試在字串中最早可能的位置符合正規表示法。在每個字元位置,Perl 會先嘗試符合第一個選項 dog。如果 dog 不符合,Perl 接著會嘗試下一個選項 cat。如果 cat 也不符合,則符合失敗,而 Perl 會移到字串中的下一個位置。一些範例

"cats and dogs" =~ /cat|dog|bird/;  # matches "cat"
"cats and dogs" =~ /dog|cat|bird/;  # matches "cat"

即使 dog 是第二個正規表示法中的第一個選項,cat 仍能在字串中較早配對。

"cats"          =~ /c|ca|cat|cats/; # matches "c"
"cats"          =~ /cats|cat|ca|c/; # matches "cats"

在此,所有選項都與第一個字串位置配對,因此第一個選項就是配對的選項。如果某些選項是其他選項的截斷,請先放置最長的選項,讓它們有機會配對。

"cab" =~ /a|b|c/ # matches "c"
                 # /a|b|c/ == /[abc]/

最後一個範例指出字元類別就像字元的交替。在給定的字元位置,允許正規表示法配對成功的第一個選項將是配對的選項。

群組項目和階層式配對

交替允許正規表示法在選項中進行選擇,但它本身並不令人滿意。原因是每個選項都是一個完整的正規表示法,但有時我們只想要正規表示法的一部分的選項。例如,假設我們要搜尋家貓或管家。正規表示法 housecat|housekeeper 符合需求,但效率不彰,因為我們必須輸入 house 兩次。最好讓正規表示法的一部分保持不變,例如 house,而某些部分有選項,例如 cat|keeper

群組 元字元 () 可以解決這個問題。群組允許正規表示法的一部分被視為單一單位。正規表示法的一部分會以括號括住的方式進行群組。因此,我們可以透過將正規表示法組成 house(cat|keeper) 來解決 housecat|housekeeper。正規表示法 house(cat|keeper) 的意思是配對 house,後面接 catkeeper。以下是更多範例

/(a|b)b/;    # matches 'ab' or 'bb'
/(ac|b)b/;   # matches 'acb' or 'bb'
/(^a|b)c/;   # matches 'ac' at start of string or 'bc' anywhere
/(a|[bc])d/; # matches 'ad', 'bd', or 'cd'

/house(cat|)/;  # matches either 'housecat' or 'house'
/house(cat(s|)|)/;  # matches either 'housecats' or 'housecat' or
                    # 'house'.  Note groups can be nested.

/(19|20|)\d\d/;  # match years 19xx, 20xx, or the Y2K problem, xx
"20" =~ /(19|20|)\d\d/;  # matches the null alternative '()\d\d',
                         # because '20\d\d' can't match

交替在群組中和群組外有相同的行為:在給定的字串位置,會採用最左邊的替代方案讓正規表示式相符。因此在最後一個範例的第一個字串位置,"20" 相符第二個替代方案,但是沒有任何東西可以相符下兩個數字 \d\d。因此 Perl 會移到下一個替代方案,也就是空替代方案,而且這有效,因為 "20" 是兩個數字。

嘗試一個替代方案、查看它是否相符,以及移到下一個替代方案的程序,同時如果它不相符,則從嘗試前一個替代方案的位置往回回到字串,稱為 回溯。術語「回溯」來自於相符正規表示式就像在森林中散步的想法。成功相符正規表示式就像抵達目的地。有許多可能的步道起點,每個字串位置一個,而且每個都會依序嘗試,由左至右。從每個步道起點可能會有很多路徑,有些會帶你到那裡,有些則是死路。當你沿著步道走,並遇到死路時,你必須沿著步道回溯到較早的點,嘗試另一條步道。如果你抵達目的地,你會立即停止,並忘記嘗試所有其他步道。你很堅持,而且只有在嘗試所有步道起點的所有步道,而且沒有抵達目的地時,你才會宣告失敗。具體來說,以下是 Perl 嘗試相符正規表示式時會執行的步驟分析

"abcde" =~ /(abd|abc)(df|d|de)/;
  1. 從字串 'a' 中的第一個字母開始。

  2. 嘗試第一個群組中的第一個替代方案 'abd'

  3. 比對 'a' 後接 'b'。到目前為止都很順利。

  4. 正則表示式中的 'd' 無法比對字串中的 'c' - 陷入死胡同。因此回溯兩個字元,並選取第一個群組 'abc' 中的第二個替代方案。

  5. 比對 'a' 後接 'b' 後接 'c'。我們順利進行,並滿足第一個群組。將 $1 設定為 'abc'

  6. 移至第二個群組,並選取第一個替代方案 'df'

  7. 比對 'd'

  8. 正則表示式中的 'f' 無法比對字串中的 'e',因此陷入死胡同。回溯一個字元,並選取第二個群組 'd' 中的第二個替代方案。

  9. 'd' 比對成功。第二個群組已滿足,因此將 $2 設定為 'd'

  10. 我們已到達正則表示式的結尾,因此完成了!我們已從字串 "abcde" 中比對出 'abcd'

關於此分析,有幾點需要注意。首先,第二個群組中的第三個替代方案 'de' 也允許比對,但我們在到達它之前就停止了 - 在給定的字元位置,最左邊的會獲勝。其次,我們能夠在字串 'a' 的第一個字元位置取得比對。如果在第一個位置沒有比對,Perl 會移至第二個字元位置 'b',並重新嘗試比對。只有當所有可能路徑在所有可能的字元位置都用盡時,Perl 才會放棄並宣告 $string =~ /(abd|abc)(df|d|de)/; 為 false。

即使有這麼多工作,正則表示式比對的速度仍然非常快。為了加快速度,Perl 會將正則表示式編譯成一個緊湊的 opcode 序列,這些序列通常可以放入處理器快取中。當執行程式碼時,這些 opcode 就可以全速執行並非常快速地進行搜尋。

萃取比對結果

群組的元字元 () 也提供另一個完全不同的功能:它們允許擷取符合字串的部分。這對於找出符合的部分和一般文字處理非常有用。對於每個群組,符合的部分會進入特殊變數 $1$2 等。它們可以用作一般變數

    # extract hours, minutes, seconds
    if ($time =~ /(\d\d):(\d\d):(\d\d)/) {    # match hh:mm:ss format
	$hours = $1;
	$minutes = $2;
	$seconds = $3;
    }

現在,我們知道在純量內容中,$time =~ /(\d\d):(\d\d):(\d\d)/ 會傳回 true 或 false 值。然而,在清單內容中,它會傳回符合值的清單 ($1,$2,$3)。因此,我們可以更簡潔地撰寫程式碼,如下所示

# extract hours, minutes, seconds
($hours, $minutes, $second) = ($time =~ /(\d\d):(\d\d):(\d\d)/);

如果正規表示式中的群組是巢狀的,$1 會取得最左邊開啟括號的群組,$2 會取得下一個開啟括號,依此類推。以下是包含巢狀群組的正規表示式

/(ab(cd|ef)((gi)|j))/;
 1  2      34

如果這個正規表示式符合,$1 會包含以 'ab' 開頭的字串,$2 會設定為 'cd''ef'$3 等於 'gi''j',而 $4 會設定為 'gi',就像 $3 一樣,或保持未定義。

為了方便,Perl 會將 $+ 設定為由編號最高的 $1$2 等取得的字串,並已指派(而且,有點相關,$^N 設定為 $1$2 等最近指派的字串;也就是與符合中使用的最右邊關閉括號相關聯的 $1$2 等)。

反向參照

與匹配變數 $1$2、... 緊密相關的是 反向參照 \g1\g2、... 反向參照只是可以在 regexp 內部 使用的匹配變數。這是一個非常棒的功能;regexp 中後面的匹配會依賴於 regexp 中先前的匹配。假設我們想要在文字中尋找重複的字詞,例如「the the」。下列 regexp 會找出所有中間有空格的 3 個字母的重複字詞

/\b(\w\w\w)\s\g1\b/;

群組會將值指定給 \g1,以便兩個部分都使用相同的 3 個字母序列。

類似的任務是找出由兩個相同部分組成的字詞

% simple_grep '^(\w\w\w\w|\w\w\w|\w\w|\w)\g1$' /usr/dict/words
beriberi
booboo
coco
mama
murmur
papa

regexp 有單一群組,會考慮 4 個字母的組合,然後是 3 個字母的組合,等等,並使用 \g1 來尋找重複。儘管 $1\g1 代表相同的事物,但應注意僅在 regexp 外部 使用匹配變數 $1$2、...,僅在 regexp 內部 使用反向參照 \g1\g2、...;否則可能會導致令人驚訝且不滿意的結果。

相對反向參照

只要有多於一個擷取群組,計算開啟括號以取得反向參照的正確數字就會容易出錯。Perl 5.10 提供了更方便的技術:相對反向參照。若要參照緊接在前的擷取群組,現在可以寫成 \g-1\g{-1},倒數第二個可用 \g-2\g{-2} 取得,依此類推。

除了可讀性和可維護性之外,使用相對反向參照的另一個好處,可以從下列範例中看出,其中使用了簡單的模式來匹配特殊字串

$a99a = '([a-z])(\d)\g2\g1';   # matches a11a, g22g, x33x, etc.

現在我們已經將這個模式儲存在一個方便的字串中,我們可能會想將它用作其他模式的一部分

$line = "code=e99e";
if ($line =~ /^(\w+)=$a99a$/){   # unexpected behavior!
    print "$1 is valid\n";
} else {
    print "bad line: '$line'\n";
}

但這不匹配,至少不是人們預期的。只有在插入內插的 $a99a 並查看正則運算式的完整結果文字後,才會很明顯反向引用發生錯誤。子運算式 (\w+) 搶走了數字 1,並將 $a99a 中的群組降級一級。這可以使用相對反向引用來避免

$a99a = '([a-z])(\d)\g{-1}\g{-2}';  # safe for being interpolated

命名反向引用

Perl 5.10 也引進了命名擷取群組和命名反向引用。若要將名稱附加到擷取群組,請撰寫 (?<name>...)(?'name'...)。然後反向引用可以寫成 \g{name}。允許將同一個名稱附加到多個群組,但只有同名集合中最左邊的那個可以被引用。在模式之外,可以透過 %+ hash 存取命名擷取群組。

假設我們必須比對日曆日期,日期可能以以下三種格式之一提供:yyyy-mm-dd、mm/dd/yyyy 或 dd.mm.yyyy,我們可以撰寫三個合適的模式,其中我們分別使用 'd''m''y' 作為擷取日期相關組成的群組名稱。比對操作將三個模式組合為選項

$fmt1 = '(?<y>\d\d\d\d)-(?<m>\d\d)-(?<d>\d\d)';
$fmt2 = '(?<m>\d\d)/(?<d>\d\d)/(?<y>\d\d\d\d)';
$fmt3 = '(?<d>\d\d)\.(?<m>\d\d)\.(?<y>\d\d\d\d)';
for my $d (qw(2006-10-21 15.01.2007 10/31/2005)) {
    if ( $d =~ m{$fmt1|$fmt2|$fmt3} ){
        print "day=$+{d} month=$+{m} year=$+{y}\n";
    }
}

如果任何選項相符,hash %+ 會繫結包含三個鍵值對。

替代擷取群組編號

另一種擷取群組編號技術(也來自 Perl 5.10)處理在選項集合中引用群組的問題。考慮用於比對一天中的時間(民用或軍用格式)的模式

if ( $time =~ /(\d\d|\d):(\d\d)|(\d\d)(\d\d)/ ){
    # process hour and minute
}

處理結果需要額外的 if 敘述,以判斷 $1$2$3$4 是否包含好東西。如果我們可以在第二個選項中也使用群組編號 1 和 2,會更容易,而這正是括號結構 (?|...)(設定在選項周圍)所達成的。以下是前一個模式的延伸版本

if($time =~ /(?|(\d\d|\d):(\d\d)|(\d\d)(\d\d))\s+([A-Z][A-Z][A-Z])/){
    print "hour=$1 minute=$2 zone=$3\n";
}

在替代編號群組內,群組編號在每個替代中從相同位置開始。在群組之後,編號繼續使用所有替代中達到的最大值加 1。

位置資訊

除了匹配的內容外,Perl 也提供匹配位置作為 @-@+ 陣列的內容。$-[0] 是整個匹配開始的位置,而 $+[0] 是結束的位置。類似地,$-[n]$n 匹配開始的位置,而 $+[n] 是結束的位置。如果 $n 未定義,則 $-[n]$+[n] 也是未定義的。然後,這段程式碼

$x = "Mmm...donut, thought Homer";
$x =~ /^(Mmm|Yech)\.\.\.(donut|peas)/; # matches
foreach $exp (1..$#-) {
    no strict 'refs';
    print "Match $exp: '$$exp' at position ($-[$exp],$+[$exp])\n";
}

會印出

Match 1: 'Mmm' at position (0,3)
Match 2: 'donut' at position (6,11)

即使正規表示式中沒有群組,仍然可以找出字串中確切匹配的內容。如果您使用它們,Perl 會將 $` 設定為匹配之前的字串部分,將 $& 設定為匹配的字串部分,並將 $' 設定為匹配之後的字串部分。一個範例

$x = "the cat caught the mouse";
$x =~ /cat/;  # $` = 'the ', $& = 'cat', $' = ' caught the mouse'
$x =~ /the/;  # $` = '', $& = 'the', $' = ' cat caught the mouse'

在第二次匹配中,$` 等於 '',因為正規表示式在字串的第一個字元位置匹配並停止;它從未看到第二個「the」。

如果您的程式碼要在早於 Perl 5.20 的版本上執行,值得注意的是使用 $`$' 會大幅降低正規表示式匹配的速度,而 $& 則會在較小程度上降低速度,因為如果它們在程式中的某個正規表示式中使用,它們會為程式中的所有正規表示式產生。因此,如果應用程式的目標是原始效能,則應避免使用它們。如果您需要擷取對應的子字串,請改用 @-@+

$` is the same as substr( $x, 0, $-[0] )
$& is the same as substr( $x, $-[0], $+[0]-$-[0] )
$' is the same as substr( $x, $+[0] )

從 Perl 5.10 開始,可以使用 ${^PREMATCH}${^MATCH}${^POSTMATCH} 變數。這些變數僅在存在 /p 修飾詞時設定。因此,它們不會懲罰程式的其餘部分。在 Perl 5.20 中,${^PREMATCH}${^MATCH}${^POSTMATCH} 無論是否使用 /p 都可用(忽略修飾詞),而 $`$'$& 不會造成任何速度差異。

非擷取群組

一個用來組合一組備選項的群組可能或可能不會作為一個擷取群組。如果它不是,它只會在正規表示式內外建立一個多餘的附加項目到可用的擷取群組值集合中。非擷取群組,表示為 (?:regexp),仍然允許正規表示式被視為一個單一單位,但同時不會建立一個擷取群組。擷取和非擷取群組都可以同時存在於同一個正規表示式中。由於沒有擷取,非擷取群組比擷取群組更快。非擷取群組對於選擇正規表示式哪個部分要擷取到匹配變數中也很方便

# match a number, $1-$4 are set, but we only want $1
/([+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)/;

# match a number faster , only $1 is set
/([+-]?\ *(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;

# match a number, get $1 = whole number, $2 = exponent
/([+-]?\ *(?:\d+(?:\.\d*)?|\.\d+)(?:[eE]([+-]?\d+))?)/;

非擷取群組對於移除從分割操作中收集到的令人討厭的元素也很有用,而分割操作需要使用括號

$x = '12aba34ba5';
@num = split /(a|b)+/, $x;    # @num = ('12','a','34','a','5')
@num = split /(?:a|b)+/, $x;  # @num = ('12','34','5')

在 Perl 5.22 和更新版本中,正規表示式中的所有群組都可以使用新的 /n 標記設定為非擷取

"hello" =~ /(hi|hello)/n; # $1 is not set!

請參閱 perlre 中的「n」 以取得更多資訊。

匹配重複

前一節中的範例顯示了一個惱人的弱點。我們只匹配 3 個字母的字詞,或 4 個字母或更少的字詞塊。我們希望能夠匹配字詞或更一般地說,任何長度的字串,而不用寫出像 \w\w\w\w|\w\w\w|\w\w|\w 這樣的繁瑣備選項。

這正是 量詞 元字元 '?''*''+'{} 被建立出來要解決的問題。它們允許我們界定我們視為匹配的正規表示式部分的重複次數。量詞會放在我們想要指定的字元、字元類別或群組的正後方。它們具有以下意義

如果您喜歡,可以在大括號內加入空白(tab 或空白字元),但必須緊鄰大括號,而且/或緊鄰逗號(如果有的話)。

以下是一些範例

/[a-z]+\s+\d*/;  # match a lowercase word, at least one space, and
                 # any number of digits
/(\w+)\s+\g1/;    # match doubled words of arbitrary length
/y(es)?/i;       # matches 'y', 'Y', or a case-insensitive 'yes'
$year =~ /^\d{2,4}$/;  # make sure year is at least 2 but not more
                       # than 4 digits
$year =~ /^\d{ 2, 4 }$/;    # Same; for those who like wide open
                            # spaces.
$year =~ /^\d{2, 4}$/;      # Same.
$year =~ /^\d{4}$|^\d{2}$/; # better match; throw out 3-digit dates
$year =~ /^\d{2}(\d{2})?$/; # same thing written differently.
                            # However, this captures the last two
                            # digits in $1 and the other does not.

% simple_grep '^(\w+)\g1$' /usr/dict/words   # isn't this easier?
beriberi
booboo
coco
mama
murmur
papa

對於所有這些量詞,Perl 會盡可能配對字串的內容,同時仍讓正規表示式成功。因此,使用 /a?.../ 時,Perl 會先嘗試配對包含 'a' 的正規表示式;如果失敗,Perl 會嘗試配對不包含 'a' 的正規表示式。對於量詞 '*',我們得到以下結果

$x = "the cat in the hat";
$x =~ /^(.*)(cat)(.*)$/; # matches,
                         # $1 = 'the '
                         # $2 = 'cat'
                         # $3 = ' in the hat'

這正是我們預期的,配對會找到字串中唯一的 cat 並鎖定它。不過,請考慮以下正規表示式

$x =~ /^(.*)(at)(.*)$/; # matches,
                        # $1 = 'the cat in the h'
                        # $2 = 'at'
                        # $3 = ''   (0 characters match)

一開始可能會猜測 Perl 會在 cat 中找到 at 然後停止,但這樣不會讓第一個量詞 .* 得到最長的可能字串。相反地,第一個量詞 .* 會盡可能取得字串,同時仍讓正規表示式相符。在此範例中,表示取得 at 序列,以及字串中的最後一個 at。這裡說明的另一個重要原則是,當正規表示式中有兩個或以上的元素時,最左邊的量詞(如果有)會盡可能取得字串,讓正規表示式的其餘部分爭取剩下的部分。因此在我們的範例中,第一個量詞 .* 取得大部分的字串,而第二個量詞 .* 取得空字串。盡可能取得字串的量詞稱為「最大比對」或「貪婪」量詞。

當正規表示式可以用好幾種不同的方式比對字串時,我們可以使用上述原則預測正規表示式會如何比對

如上所述,原則 0 優先於其他原則。正規表示式會盡可能提早比對,而其他原則會決定正規表示式在那個最早字元位置如何比對。

以下是這些原則運作的範例

$x = "The programming republic of Perl";
$x =~ /^(.+)(e|r)(.*)$/;  # matches,
                          # $1 = 'The programming republic of Pe'
                          # $2 = 'r'
                          # $3 = 'l'

此正規表示法在最早的字串位置 'T' 處配對。有人可能會認為最左邊的交替字元 'e' 會配對,但 'r' 會在第一個量詞中產生最長的字串。

$x =~ /(m{1,2})(.*)$/;  # matches,
                        # $1 = 'mm'
                        # $2 = 'ing republic of Perl'

在此,最早可能配對的是 programming 中的第一個 'm'm{1,2} 是第一個量詞,因此它可以配對最大的 mm

$x =~ /.*(m{1,2})(.*)$/;  # matches,
                          # $1 = 'm'
                          # $2 = 'ing republic of Perl'

在此,正規表示法在字串開頭配對。第一個量詞 .* 盡可能擷取,只留下一個 'm' 給第二個量詞 m{1,2}

$x =~ /(.?)(m{1,2})(.*)$/;  # matches,
                            # $1 = 'a'
                            # $2 = 'mm'
                            # $3 = 'ing republic of Perl'

在此,.? 在字串中最早可能的位置(programming 中的 'a')擷取其最大的字元,留下 m{1,2} 的機會來配對兩個 'm'。最後,

"aXXXb" =~ /(X*)/; # matches with $1 = ''

因為它可以在字串開頭配對零個 'X'。如果您絕對想要配對至少一個 'X',請使用 X+,而不是 X*

有時貪婪並不好。有時,我們希望量詞配對字串的最小部分,而不是最大部分。為了這個目的,Larry Wall 建立了最小配對非貪婪量詞 ??*?+?{}?。這些是附加 '?' 的通常量詞。它們具有以下意義

我們來看上面的範例,但使用最少量詞

$x = "The programming republic of Perl";
$x =~ /^(.+?)(e|r)(.*)$/; # matches,
                          # $1 = 'Th'
                          # $2 = 'e'
                          # $3 = ' programming republic of Perl'

將允許字串開頭 '^' 和交替匹配的最少字串為 Th,其中交替 e|r 匹配 'e'。第二個量詞 .* 可以自由吸收字串的其餘部分。

$x =~ /(m{1,2}?)(.*?)$/;  # matches,
                          # $1 = 'm'
                          # $2 = 'ming republic of Perl'

這個正規表示法可以匹配的第一個字串位置在 programming 中的第一個 'm'。在此位置,最小的 m{1,2}? 只匹配一個 'm'。儘管第二個量詞 .*? 偏好不匹配任何字元,但它受字串結尾錨定 '$' 約束,以匹配字串的其餘部分。

$x =~ /(.*?)(m{1,2}?)(.*)$/;  # matches,
                              # $1 = 'The progra'
                              # $2 = 'm'
                              # $3 = 'ming republic of Perl'

在這個正規表示法中,您可能會預期第一個最少量詞 .*? 匹配空字串,因為它不受 '^' 錨定約束,以匹配字詞的開頭。然而,原則 0 在這裡適用。由於整個正規表示法有可能在字串開頭匹配,因此它在字串開頭匹配。因此,第一個量詞必須匹配到第一個 'm' 為止。第二個最少量詞只匹配一個 'm',而第三個量詞匹配字串的其餘部分。

$x =~ /(.??)(m{1,2})(.*)$/;  # matches,
                             # $1 = 'a'
                             # $2 = 'mm'
                             # $3 = 'ing republic of Perl'

就像在先前的正規表示法中,第一個量詞 .?? 可以最早在位置 'a' 匹配,因此它確實匹配。第二個量詞是貪婪的,因此它匹配 mm,而第三個量詞匹配字串的其餘部分。

我們可以修改上面的原則 3,以考量非貪婪量詞

就像交替,量詞也容易回溯。以下是範例的分步分析

$x = "the cat in the hat";
$x =~ /^(.*)(at)(.*)$/; # matches,
                        # $1 = 'the cat in the h'
                        # $2 = 'at'
                        # $3 = ''   (0 matches)
  1. 從字串中的第一個字母 't' 開始。

  2. 第一個量詞 '.*' 從匹配整個字串 "the cat in the hat" 開始。

  3. 正則表示式元素 'at' 中的 'a' 不匹配字串結尾。回溯一個字元。

  4. 正則表示式元素 'at' 中的 'a' 仍然不匹配字串 't' 的最後一個字母,因此再回溯一個字元。

  5. 現在我們可以匹配 'a''t'

  6. 移至第三個元素 '.*'。由於我們在字串結尾,且 '.*' 可以匹配 0 次,因此將其指定為空字串。

  7. 我們完成了!

大多數時候,所有這些前進和回溯都很快,而且搜尋速度很快。然而,有些病態的正則表示式其執行時間會隨著字串大小呈指數成長。一個典型的結構會在你的眼前爆炸,形式如下

/(a|b+)*/;

問題在於巢狀的不確定量詞。在 '+''*' 之間對長度為 n 的字串進行分割的方法有很多:重複一次長度為 n 的 b+、重複兩次,第一次 b+ 長度為 k,第二次長度為 n-k、重複 m 次,其位元加起來長度為 n,。事實上,有指數個方法可以將字串分割為其長度的函數。正則表示式可能會運氣好,並在過程中及早匹配,但如果沒有匹配,Perl 會嘗試每個可能性,然後才放棄。因此,小心使用巢狀 '*'{n,m}'+'。傑佛瑞·弗里德爾的書籍精通正規表示式對此和其他效率問題進行了精彩的討論。

獨佔量詞

在無情地搜尋匹配項期間回溯可能會浪費時間,特別是在匹配項注定會失敗時。考慮簡單的模式

/^\w+\s+\w+$/; # a word, spaces, a word

每當這套用於不完全符合模式預期的字串,例如 "abc ""abc def ",正規表示式引擎將回溯,大約是字串中每個字元一次。但我們知道,除了將所有開頭的字元字元用於比對第一次重複之外,沒有其他方法,所有空格都必須由中間部分吃掉,而第二個字詞也是如此。

隨著 Perl 5.10 中引入佔有量詞,我們有一種方法可以指示正規表示式引擎不要回溯,使用附加 '+' 的一般量詞。這讓它們既貪婪又吝嗇;一旦成功,它們就不會放棄任何東西來允許其他解決方案。它們有以下含義

這些佔有量詞表示更一般概念的特殊情況,獨立子表示式,請參閱下方。

作為佔有量詞適用的範例,我們考慮比對引號字串,因為它出現在多種程式語言中。反斜線用作跳脫字元,表示下一個字元應逐字取用,作為字串的另一個字元。因此,在開頭引號後,我們預期一個(可能為空的)替換順序:不是未跳脫引號或反斜線的某些字元,就是跳脫字元。

/"(?:[^"\\]++|\\.)*+"/;

建立正規表示式

在這個點上,我們已經涵蓋了所有基本的 regexp 概念,所以讓我們提供一個更複雜的正規表示式範例。我們將建構一個與數字相符的 regexp。

建構 regexp 的第一個任務是決定我們想要相符的內容和我們想要排除的內容。在我們的案例中,我們想要相符整數和浮點數,並且我們想要拒絕任何不是數字的字串。

下一個任務是將問題分解成較小的問題,這些問題可以輕易轉換成 regexp。

最簡單的案例是整數。這些由一系列數字組成,前面有一個可選的符號。我們可以使用 \d+ 來表示數字,並且符號可以使用 [+-] 來相符。因此整數 regexp 是

/[+-]?\d+/;  # matches integers

浮點數可能有一個符號、一個整數部分、一個小數點、一個小數部分和一個指數。這些部分中的一個或多個是可選的,所以我們需要檢查不同的可能性。形式正確的浮點數包括 123.、0.345、.34、-1e6 和 25.4E-72。與整數一樣,前面的符號是完全可選的,並且可以使用 [+-]? 來相符。我們可以看到,如果沒有指數,浮點數必須有一個小數點,否則它們就是整數。我們可能會試著使用 \d*\.\d* 來建模這些,但這也會相符只有一個小數點,這不是數字。所以沒有指數的浮點數的三個案例是

/[+-]?\d+\./;  # 1., 321., etc.
/[+-]?\.\d+/;  # .1, .234, etc.
/[+-]?\d+\.\d+/;  # 1.0, 30.56, etc.

這些可以使用三向交替合併成一個單一的 regexp

/[+-]?(\d+\.\d+|\d+\.|\.\d+)/;  # floating point, no exponent

在這個交替中,將 '\d+\.\d+' 放在 '\d+\.' 之前很重要。如果 '\d+\.' 在前面,regexp 會很樂意相符它並忽略數字的小數部分。

現在考慮帶有指數的浮點數。此處的關鍵觀察是,指數前面允許同時出現整數和帶有小數點的數字。然後,指數(如整體符號)與我們是否匹配帶有或不帶小數點的數字無關,並且可以從尾數中「解耦」。正規表示式的整體形式現在變得清晰

/^(optional sign)(integer | f.p. mantissa)(optional exponent)$/;

指數是'e''E',後跟一個整數。因此,指數正規表示式為

/[eE][+-]?\d+/;  # exponent

將所有部分組合在一起,我們得到一個匹配數字的正規表示式

/^[+-]?(\d+\.\d+|\d+\.|\.\d+|\d+)([eE][+-]?\d+)?$/;  # Ta da!

像這樣的長正規表示式可能會給你的朋友留下深刻印象,但可能難以解讀。在像這樣複雜的情況下,匹配的/x修飾符非常有價值。它允許將幾乎任意的空白和註解放入正規表示式中,而不會影響它們的含義。使用它,我們可以將我們的「擴充」正規表示式改寫為更令人愉悅的形式

/^
   [+-]?         # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+\.\d+  # mantissa of the form a.b
      |\d+\.     # mantissa of the form a.
      |\.\d+     # mantissa of the form .b
      |\d+       # integer of the form a
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

如果空白大多無關緊要,那麼如何將空格字元包含在擴充正規表示式中?答案是用反斜線'\ '或將其放入字元類別[ ]中。磅號也是如此:使用\#[#]。例如,Perl 允許符號和尾數或整數之間有空格,我們可以將其新增到我們的正規表示式中,如下所示

/^
   [+-]?\ *      # first, match an optional sign *and space*
   (             # then match integers or f.p. mantissas:
       \d+\.\d+  # mantissa of the form a.b
      |\d+\.     # mantissa of the form a.
      |\.\d+     # mantissa of the form .b
      |\d+       # integer of the form a
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

在此形式中,更容易看到簡化交替的方法。選項 1、2 和 4 都以\d+開頭,因此可以將其分解

/^
   [+-]?\ *      # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+       # start out with a ...
       (
           \.\d* # mantissa of the form a.b or a.
       )?        # ? takes care of integers of the form a
      |\.\d+     # mantissa of the form .b
   )
   ( [eE] [+-]? \d+ )?  # finally, optionally match an exponent
$/x;

從 Perl v5.26 開始,指定/xx會將模式的方括號部分變更為忽略標籤和空格字元,除非在它們前面加上反斜線。因此,我們可以寫

/^
   [ + - ]?\ *   # first, match an optional sign
   (             # then match integers or f.p. mantissas:
       \d+       # start out with a ...
       (
           \.\d* # mantissa of the form a.b or a.
       )?        # ? takes care of integers of the form a
      |\.\d+     # mantissa of the form .b
   )
   ( [ e E ] [ + - ]? \d+ )?  # finally, optionally match an exponent
$/xx;

這並不能真正提高此範例的可讀性,但如果你需要,可以使用它。將模式壓縮成緊湊形式,我們有

/^[+-]?\ *(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;

這是我們的最終正規表示式。綜上所述,我們通過以下方式建立正規表示式

這些也是撰寫電腦程式中常見的步驟。這非常合理,因為正規表示法基本上是使用一種指定模式的小型電腦語言所寫的程式。

在 Perl 中使用正規表示法

第 1 部分的最後一個主題簡要說明正規表示法在 Perl 程式中的使用方式。它們如何融入 Perl 語法?

我們已經在預設的 /regexp/ 和任意分隔符號 m!regexp! 形式中介紹過比對運算子。我們已經使用繫結運算子 =~ 和其否定 !~ 來測試字串比對。與比對運算子相關,我們已經討論過單行 /s、多行 /m、不分大小寫 /i 和延伸 /x 修飾詞。關於比對運算子,您可能還想瞭解更多內容。

禁止替換

如果您在第一次替換發生後變更 $pattern,Perl 會忽略它。如果您完全不想要任何替換,請使用特殊分隔符號 m''

@pattern = ('Seuss');
while (<>) {
    print if m'@pattern';  # matches literal '@pattern', not 'Seuss'
}

與字串類似,m'' 在正規表示法中作用如同單引號;所有其他 'm' 分隔符號作用如同雙引號。如果正規表示法評估為空字串,則會改用 上次成功比對 中的正規表示法。因此我們有

"dog" =~ /d/;  # 'd' matches
"dogbert" =~ //;  # this matches the 'd' regexp used before

全域比對

我們將在此討論的最後兩個修飾詞 /g/c,涉及多重比對。修飾詞 /g 代表全域比對,並允許比對運算子在字串中盡可能多次比對。在純量環境中,針對字串的連續呼叫會使 /g 從一個比對跳到另一個比對,同時追蹤字串中的位置。您可以使用 pos() 函數取得或設定位置。

以下範例顯示 /g 的使用方式。假設我們有一個由空格分隔的字詞組成的字串。如果我們事先知道有多少個字詞,我們可以使用分組來擷取這些字詞

$x = "cat dog house"; # 3 words
$x =~ /^\s*(\w+)\s+(\w+)\s+(\w+)\s*$/; # matches,
                                       # $1 = 'cat'
                                       # $2 = 'dog'
                                       # $3 = 'house'

但是如果我們有數量不定的字詞呢?這正是 /g 所設計的任務。若要擷取所有字詞,請建立簡單的正則表示式 (\w+),並使用 /(\w+)/g 迴圈處理所有比對結果

while ($x =~ /(\w+)/g) {
    print "Word is $1, ends at position ", pos $x, "\n";
}

會印出

Word is cat, ends at position 3
Word is dog, ends at position 7
Word is house, ends at position 13

比對失敗或變更目標字串會重設位置。如果您不希望在比對失敗後重設位置,請新增 /c,例如 /regexp/gc。字串中的目前位置與字串相關聯,而非正則表示式。這表示不同的字串有不同的位置,而且它們各自的位置可以獨立設定或讀取。

在清單內容中,/g 會傳回比對分組的清單,或者如果沒有分組,則傳回與整個正則表示式比對的清單。因此,如果我們只想要字詞,可以使用

@words = ($x =~ /(\w+)/g);  # matches,
                            # $words[0] = 'cat'
                            # $words[1] = 'dog'
                            # $words[2] = 'house'

/g 修飾詞密切相關的是 \G 錨定。\G 錨定會在先前 /g 比對結束的地方進行比對。\G 讓我們可以輕鬆地進行內容敏感比對

$metric = 1;  # use metric units
...
$x = <FILE>;  # read in measurement
$x =~ /^([+-]?\d+)\s*/g;  # get magnitude
$weight = $1;
if ($metric) { # error checking
    print "Units error!" unless $x =~ /\Gkg\./g;
}
else {
    print "Units error!" unless $x =~ /\Glbs\./g;
}
$x =~ /\G\s+(widget|sprocket)/g;  # continue processing

/g\G 的組合讓我們可以一次處理一點字串,並使用任意的 Perl 邏輯來決定下一步要執行什麼動作。目前,\G 錨定僅在用於錨定至樣式的開頭時才獲得完全支援。

\G 在使用正則表示式處理固定長度記錄時也很有價值。假設我們有一個編碼區 DNA 片段,編碼為鹼基對字母 ATCGTTGAAT...,而且我們想要找出所有終止密碼子 TGA。在編碼區中,密碼子是 3 個字母的序列,因此我們可以將 DNA 片段視為 3 個字母記錄的序列。天真的正則表示式

# expanded, this is "ATC GTT GAA TGC AAA TGA CAT GAC"
$dna = "ATCGTTGAATGCAAATGACATGAC";
$dna =~ /TGA/;

無法運作;它可能會比對到 TGA,但無法保證比對與密碼子邊界對齊,例如,子字串 GTT GAA 會產生比對。更好的解決方案是

while ($dna =~ /(\w\w\w)*?TGA/g) {  # note the minimal *?
    print "Got a TGA stop codon at position ", pos $dna, "\n";
}

它會列印

Got a TGA stop codon at position 18
Got a TGA stop codon at position 23

位置 18 很棒,但位置 23 是錯誤的。發生了什麼事?

答案是,我們的正則表示式在我們通過最後一個真實比對之前運作良好。然後,正則表示式將無法比對同步的 TGA,並開始向前移動一個字元位置,這不是我們想要的。解決方案是使用 \G 將比對錨定至密碼子對齊

while ($dna =~ /\G(\w\w\w)*?TGA/g) {
    print "Got a TGA stop codon at position ", pos $dna, "\n";
}

它會列印

Got a TGA stop codon at position 18

這是正確答案。此範例說明了不僅要比對想要的內容,還要排除不想要的內容,這點很重要。

(還有其他可用的正規表示式修飾詞,例如 /o,但其特殊用途已超出本簡介的範圍。)

搜尋和取代

正規表示式在 Perl 的「搜尋和取代」操作中也扮演著重要的角色。搜尋和取代是使用 s/// 算子來完成。一般形式為 s/regexp/replacement/modifiers,我們所知道的所有正規表示式和修飾詞在此情況下也適用。replacement 是 Perl 雙引號字串,用於取代字串中與 regexp 相符的部分。算子 =~ 也用於在此將字串與 s/// 關聯起來。如果與 $_ 相符,則可以省略 $_ =~。如果相符,s/// 會傳回已進行取代的次數;否則會傳回 false。以下是幾個範例

$x = "Time to feed the cat!";
$x =~ s/cat/hacker/;   # $x contains "Time to feed the hacker!"
if ($x =~ s/^(Time.*hacker)!$/$1 now!/) {
    $more_insistent = 1;
}
$y = "'quoted words'";
$y =~ s/^'(.*)'$/$1/;  # strip single quotes,
                       # $y contains "quoted words"

在最後一個範例中,整個字串都相符,但只有單引號內的部份被分組。使用 s/// 算子時,相符變數 $1$2 等會立即可供在取代式中使用,因此我們使用 $1 將引號內的字串取代為僅引號內的內容。使用全域修飾詞 s///g 會搜尋並取代字串中正規表示式的所有出現

$x = "I batted 4 for 4";
$x =~ s/4/four/;   # doesn't do it all:
                   # $x contains "I batted four for 4"
$x = "I batted 4 for 4";
$x =~ s/4/four/g;  # does it all:
                   # $x contains "I batted four for four"

如果你在這個教學課程中偏好「regex」而非「regexp」,則可以使用下列程式來取代它

% cat > simple_replace
#!/usr/bin/perl
$regexp = shift;
$replacement = shift;
while (<>) {
    s/$regexp/$replacement/g;
    print;
}
^D

% simple_replace regexp regex perlretut.pod

simple_replace 中,我們使用 s///g 修飾詞來取代每一行中正規表示式的所有出現。(即使正規表示式出現在迴圈中,Perl 也夠聰明只編譯一次。)與 simple_grep 一樣,prints/$regexp/$replacement/g 都會隱含地使用 $_

如果您不想讓 s/// 改變原始變數,您可以使用非破壞性替換修飾詞 s///r。這會改變行為,讓 s///r 傳回最終替換的字串(而不是替換的次數)

$x = "I like dogs.";
$y = $x =~ s/dogs/cats/r;
print "$x $y\n";

該範例會印出「我喜歡狗。我喜歡貓」。請注意,原始的 $x 變數沒有受到影響。替換的整體結果會儲存在 $y 中。如果替換沒有影響任何內容,則會傳回原始字串

$x = "I like dogs.";
$y = $x =~ s/elephants/cougars/r;
print "$x $y\n"; # prints "I like dogs. I like dogs."

s///r 旗標允許的另一個有趣功能是串連替換

$x = "Cats are great.";
print $x =~ s/Cats/Dogs/r =~ s/Dogs/Frogs/r =~
    s/Frogs/Hedgehogs/r, "\n";
# prints "Hedgehogs are great."

專門用於搜尋和替換的修飾詞是 s///e 評估修飾詞。s///e 將替換文字視為 Perl 程式碼,而不是雙引號字串。程式碼傳回的值會替換為符合的子字串。如果您需要在替換文字的過程中進行一些運算,s///e 會很有用。這個範例會計算一行中的字元頻率

$x = "Bill the cat";
$x =~ s/(.)/$chars{$1}++;$1/eg; # final $1 replaces char with itself
print "frequency of '$_' is $chars{$_}\n"
    foreach (sort {$chars{$b} <=> $chars{$a}} keys %chars);

它會列印

frequency of ' ' is 2
frequency of 't' is 2
frequency of 'l' is 2
frequency of 'B' is 1
frequency of 'c' is 1
frequency of 'e' is 1
frequency of 'h' is 1
frequency of 'i' is 1
frequency of 'a' is 1

與比對 m// 算子一樣,s/// 可以使用其他分隔符號,例如 s!!!s{}{},甚至 s{}//。如果使用單引號 s''',則 regexp 和替換會視為單引號字串,而且沒有變數替換。清單內容中的 s/// 傳回與純量內容相同的值,,符合的次數。

分割函式

split() 函數是使用正規表示式的另一個地方。split /regexp/, string, limitstring 運算元分隔成子字串清單,並傳回該清單。正規表示式必須設計成與所需子字串的分隔符號相符。如果存在 limit,則將分隔限制在不超過 limit 個字串。例如,若要將字串分隔成字詞,請使用

$x = "Calvin and Hobbes";
@words = split /\s+/, $x;  # $word[0] = 'Calvin'
                           # $word[1] = 'and'
                           # $word[2] = 'Hobbes'

如果使用空白正規表示式 //,正規表示式總是相符,而字串會分隔成個別字元。如果正規表示式有群組,則結果清單也會包含群組中的相符子字串。例如,

$x = "/usr/bin/perl";
@dirs = split m!/!, $x;  # $dirs[0] = ''
                         # $dirs[1] = 'usr'
                         # $dirs[2] = 'bin'
                         # $dirs[3] = 'perl'
@parts = split m!(/)!, $x;  # $parts[0] = ''
                            # $parts[1] = '/'
                            # $parts[2] = 'usr'
                            # $parts[3] = '/'
                            # $parts[4] = 'bin'
                            # $parts[5] = '/'
                            # $parts[6] = 'perl'

由於 $x 的第一個字元與正規表示式相符,split 在清單前加上一個空白初始元素。

如果您已讀到這裡,恭喜!您現在擁有使用正規表示式來解決各種文字處理問題所需的所有基本工具。如果您是第一次閱讀本教學課程,不妨在此暫停並玩一下正規表示式.... 第 2 部分 涉及正規表示式的較深奧面向,而這些概念在開始時絕對不需要。

第 2 部分:強大工具

好的,您已瞭解正規表示式的基礎知識,並想要了解更多。如果比對正規表示式類似於在森林中散步,那麼第 1 部分中討論的工具類似於地形圖和指南針,這些是我們經常使用的基本工具。第 2 部分中的大多數工具類似於照明彈和衛星電話。它們在健行時並不常使用,但當我們受困時,它們可能是無價的。

以下是 Perl 正規表示式較進階、較少使用或有時較深奧的功能。在第 2 部分中,我們假設您已熟悉基礎知識,並專注於進階功能。

更多關於字元、字串和字元類別的資訊

有一些跳脫序列和字元類別我們尚未涵蓋。

有幾個跳脫序列可以將字元或字串轉換成大小寫,它們也可以在模式中使用。\l\u 分別將下一個字元轉換成小寫或大寫

$x = "perl";
$string =~ /\u$x/;  # matches 'Perl' in $string
$x = "M(rs?|s)\\."; # note the double backslash
$string =~ /\l$x/;  # matches 'mr.', 'mrs.', and 'ms.',

\L\U 表示持續轉換大小寫,直到 \E 終止或被另一個 \U\L 取代

$x = "This word is in lower case:\L SHOUT\E";
$x =~ /shout/;       # matches
$x = "I STILL KEYPUNCH CARDS FOR MY 360";
$x =~ /\Ukeypunch/;  # matches punch card string

如果沒有 \E,則會轉換大小寫直到字串結束。正規表示式 \L\u$word\u\L$word$word 的第一個字元轉換成大寫,其餘字元轉換成小寫。(在 ASCII 字元之外,情況會變得有點複雜;\u 實際上會執行標題大小寫對應,對於大多數字元來說,這與大寫相同,但並非全部;請參閱 https://unicode.org/faq/casemap_charprop.html#4。)

控制字元可以用 \c 跳脫,因此控制 Z 字元會與 \cZ 相符。跳脫序列 \Q...\E 會引用或保護大多數非字母字元。例如,

$x = "\QThat !^*&%~& cat!";
$x =~ /\Q!^*&%~&\E/;  # check for rough language

它不會保護 '$''@',因此仍然可以替換變數。

\Q\L\l\U\u\E 實際上是雙引號語法的部分,而不是正規表示式語法的部分。如果它們出現在直接嵌入程式中的正規表示式中,它們會起作用,但如果它們包含在插入模式中的字串中,則不會起作用。

Perl 正規表示式可以處理的不僅僅是標準 ASCII 字元集。Perl 支援Unicode,這是一個用於表示世界上幾乎所有書面語言的字母表和大量符號的標準。Perl 的文字字串是 Unicode 字串,因此它們可以包含值(碼點或字元編號)高於 255 的字元。

這對正規表示式來說意味著什麼?嗯,正規表示式使用者不需要知道太多關於 Perl 內部字串表示法。但他們確實需要知道 1) 如何在正規表示式中表示 Unicode 字元,以及 2) 比對操作會將要搜尋的字串視為字元序列,而不是位元組。1) 的答案是,大於 chr(255) 的 Unicode 字元使用 \x{hex} 表示法表示,因為 \xXY(沒有大括號,XY 是兩個十六進位數字)不會超過 255。(從 Perl 5.14 開始,如果您是八進位制愛好者,您也可以使用 \o{oct}。)

/\x{263a}/;   # match a Unicode smiley face :)
/\x{ 263a }/; # Same

注意:在 Perl 5.6.0 中,過去需要宣告 use utf8 才能使用任何 Unicode 功能。這不再是這樣:對於幾乎所有 Unicode 處理,都不需要明確的 utf8 實用程式。(唯一重要的情況是,如果您的 Perl 腳本是 Unicode 並編碼為 UTF-8,則需要明確的 use utf8。)

找出您想要的 Unicode 字元的十六進位序列,或解碼其他人的十六進位 Unicode regexp,這與機器碼編程一樣有趣。因此,指定 Unicode 字元的另一種方式是使用命名字元跳脫序列 \N{name}name 是 Unicode 字元的名稱,如 Unicode 標準中所指定。例如,如果我們想要表示或比對水星的占星符號,我們可以使用

$x = "abc\N{MERCURY}def";
$x =~ /\N{MERCURY}/;   # matches
$x =~ /\N{ MERCURY }/; # Also matches

也可以使用「簡短」名稱

print "\N{GREEK SMALL LETTER SIGMA} is called sigma.\n";
print "\N{greek:Sigma} is an upper-case sigma.\n";

您也可以透過指定 charnames pragma 將名稱限制在特定字母表

use charnames qw(greek);
print "\N{sigma} is Greek sigma\n";

Unicode Consortium 提供線上字元名稱索引,https://www.unicode.org/charts/charindex.html;說明文件,其中包含其他資源的連結,請參閱 https://www.unicode.org/standard/where

從 Perl v5.32 開始,可以使用 \N{...} 的替代方案來表示全名,也就是說

/\p{Name=greek small letter sigma}/

\p{} 中使用時,字元名稱的大小寫無關緊要,大多數空格、底線和連字元也是如此。(少數異常字元會造成問題,無法總是忽略所有字元。詳細資訊(當您變得更熟練且需要時,可以查詢)請參閱 https://www.unicode.org/reports/tr44/tr44-24.html#UAX44-LM2)。

需求 2) 的答案是 regexp(大多數)使用 Unicode 字元。「大多數」是因為混亂的向下相容性原因,但從 Perl 5.14 開始,在 use feature 'unicode_strings' 範圍內編譯的任何 regexp(在 use v5.12 或更高版本範圍內會自動開啟)會將「大多數」變成「總是」。如果您想要正確處理 Unicode,您應該確保開啟 'unicode_strings'。在內部,這會使用 UTF-8 或原生 8 位元編碼編碼為位元組,具體取決於字串的歷程,但從概念上來說,它是一系列字元,而不是位元組。請參閱 perlunitut 以取得相關教學課程。

現在讓我們來討論 Unicode 字元類別,通常稱為「字元屬性」。這些由 \p{name} 逸出序列表示。其否定為 \P{name}。例如,若要比對小寫和字元,

$x = "BOB";
$x =~ /^\p{IsUpper}/;   # matches, uppercase char class
$x =~ /^\P{IsUpper}/;   # doesn't match, char class sans uppercase
$x =~ /^\p{IsLower}/;   # doesn't match, lowercase char class
$x =~ /^\P{IsLower}/;   # matches, char class sans lowercase

(「Is」為選用。)

有許多、許多的 Unicode 字元屬性。完整清單請參閱 perluniprops。其中大多數都有較短名稱的同義詞,也列在其中。有些同義詞為單一字元。對於這些字元,您可以省略大括號。例如,\pM\p{Mark} 相同,表示重音符號等內容。

Unicode \p{Script}\p{Script_Extensions} 屬性用於將每個 Unicode 字元分類到其所寫的語言腳本中。例如,英文、法文和許多其他歐洲語言都以拉丁腳本寫成。但也有希臘腳本、泰文腳本、片假名腳本等(ScriptScript_Extensions 的較舊、較不進階的形式,僅保留以維持向後相容性)。您可以測試字元是否在特定腳本中,例如 \p{Latin}\p{Greek}\p{Katakana}。若要測試它是否不在巴厘腳本中,您可以使用 \P{Balinese}。(這些在底層都使用 Script_Extensions,因為這樣可以得到更好的結果。)

到目前為止,我們已經描述了 \p{...} 字元類別的單一形式。還有一個複合形式,您可能會遇到。它們看起來像 \p{name=value}\p{name:value}(等號和冒號可以互換使用)。這些比單一形式更通用,事實上,大多數單一形式只是 Perl 定義的常用複合形式的捷徑。例如,前一段中的腳本範例可以等效地寫成 \p{Script_Extensions=Latin}\p{Script_Extensions:Greek}\p{script_extensions=katakana}\P{script_extensions=balinese}(大寫小寫在 {} 大括號之間無關緊要)。您可能永遠不需要使用複合形式,但有時是有必要的,而且使用它們可以讓您的程式碼更容易理解。

\X 是用於包含 Unicode 擴充字形群集 的字元類別的縮寫。這表示一個「邏輯字元」:看起來像單一字元,但可能在內部以多個字元表示。例如,使用 Unicode 全名,例如,「A + 組合環」是一個字形群集,其基本字元為「A」,組合字元為「組合環」,在丹麥語中轉換為「A」加上圓圈,就像在字詞 Ångstrom 中一樣。

有關 Unicode 的完整最新資訊,請參閱最新的 Unicode 標準,或 Unicode 聯盟網站 https://www.unicode.org

彷彿這些類別還不夠用,Perl 也定義了 POSIX 風格的字元類別。這些類別的格式為 [:名稱:],其中 名稱 為 POSIX 類別的名稱。POSIX 類別包括 alphaalnumasciicntrldigitgraphlowerprintpunctspaceupperxdigit,以及兩個延伸,word(Perl 延伸以符合 \w)和 blank(GNU 延伸)。/a 修改器將這些類別限制為僅在 ASCII 範圍內符合;否則,它們可以符合與其對應的 Perl Unicode 類別相同:[:upper:]\p{IsUpper} 相同,等等。(這有一些例外和陷阱;請參閱 perlrecharclass 以取得完整說明。)[:digit:][:word:][:space:] 對應於熟悉的 \d\w\s 字元類別。若要否定 POSIX 類別,請在名稱前面加上 '^',例如,[:^digit:] 對應於 \D,在 Unicode 下對應於 \P{IsDigit}。Unicode 和 POSIX 字元類別可以用於 \d,但例外情況是 POSIX 字元類別只能用於字元類別內

/\s+[abc[:digit:]xyz]\s*/;  # match a,b,c,x,y,z, or a digit
/^=item\s[[:digit:]]/;      # match '=item',
                            # followed by a space and a digit
/\s+[abc\p{IsDigit}xyz]\s+/;  # match a,b,c,x,y,z, or a digit
/^=item\s\p{IsDigit}/;        # match '=item',
                              # followed by a space and a digit

呼!這些就是所有其他字元和字元類別。

編譯和儲存正規表示式

在第 1 部分中,我們提到 Perl 會將正規表示式編譯成一連串緊湊的 opcode。因此,編譯後的正規表示式是一個資料結構,可以儲存一次並重複使用。正規表示式引用碼 qr// 正好可以做到這一點:qr/string/string 編譯為正規表示式,並將結果轉換成可以指定給變數的形式

$reg = qr/foo+bar?/;  # reg contains a compiled regexp

然後 $reg 可以用作正規表示式

$x = "fooooba";
$x =~ $reg;     # matches, just like /foo+bar?/
$x =~ /$reg/;   # same thing, alternate form

$reg 也可以內插到更大的正規表示式中

$x =~ /(abc)?$reg/;  # still matches

與比對運算子一樣,正規表示式引用碼可以使用不同的分隔符號,例如qr!!qr{}qr~~。使用撇號作為分隔符號 (qr'') 會禁止任何內插。

預先編譯的正規表示式對於建立不需要在每次遇到時重新編譯的動態比對很有用。使用預先編譯的正規表示式,我們撰寫一個 grep_step 程式,用於比對一系列模式,並在滿足其中一個模式後進展到下一個模式。

% cat > grep_step
#!/usr/bin/perl
# grep_step - match <number> regexps, one after the other
# usage: multi_grep <number> regexp1 regexp2 ... file1 file2 ...

$number = shift;
$regexp[$_] = shift foreach (0..$number-1);
@compiled = map qr/$_/, @regexp;
while ($line = <>) {
    if ($line =~ /$compiled[0]/) {
        print $line;
        shift @compiled;
        last unless @compiled;
    }
}
^D

% grep_step 3 shift print last grep_step
$number = shift;
        print $line;
        last unless @compiled;

將預先編譯的正規表示式儲存在陣列 @compiled 中,讓我們可以簡單地迴圈處理正規表示式,而不需要重新編譯,從而獲得靈活性而不犧牲速度。

在執行階段撰寫正規表示式

回溯比使用不同的正規表示式重複嘗試更有效率。如果有好幾個正規表示式,而且與其中任何一個正規表示式比對都是可以接受的,那麼就可以將它們組合成一組選項。如果個別表示式是輸入資料,則可以透過編寫連接運算來做到這一點。我們將在進階版的 simple_grep 程式中運用這個概念:一個用於比對多個模式的程式

% cat > multi_grep
#!/usr/bin/perl
# multi_grep - match any of <number> regexps
# usage: multi_grep <number> regexp1 regexp2 ... file1 file2 ...

$number = shift;
$regexp[$_] = shift foreach (0..$number-1);
$pattern = join '|', @regexp;

while ($line = <>) {
    print $line if $line =~ /$pattern/;
}
^D

% multi_grep 2 shift for multi_grep
$number = shift;
$regexp[$_] = shift foreach (0..$number-1);

有時,從要分析的輸入中建構模式並在比對運算的左側使用允許值是有利的。作為這個有點矛盾的情況的一個範例,讓我們假設我們的輸入包含一個命令動詞,該動詞應比對一組可用的命令動詞中的其中一個,額外的條件是命令可以縮寫,只要給定的字串是唯一的即可。以下程式示範了基本演算法。

% cat > keymatch
#!/usr/bin/perl
$kwds = 'copy compare list print';
while( $cmd = <> ){
    $cmd =~ s/^\s+|\s+$//g;  # trim leading and trailing spaces
    if( ( @matches = $kwds =~ /\b$cmd\w*/g ) == 1 ){
        print "command: '@matches'\n";
    } elsif( @matches == 0 ){
        print "no such command: '$cmd'\n";
    } else {
        print "not unique: '$cmd' (could be one of: @matches)\n";
    }
}
^D

% keymatch
li
command: 'list'
co
not unique: 'co' (could be one of: copy compare)
printer
no such command: 'printer'

我們不會嘗試比對輸入與關鍵字,而是比對關鍵字的組合集合與輸入。樣式比對操作 $kwds =~ /\b($cmd\w*)/g 同時執行多項工作。它確保給定的指令從關鍵字開始 (\b)。它容許縮寫,因為加入了 \w*。它會告訴我們比對次數 (scalar @matches) 和實際比對的所有關鍵字。你很難要求更多了。

在正規表示式中內嵌註解和修飾詞

從本節開始,我們將討論 Perl 的一組延伸樣式。這些是傳統正規表示式語法的延伸,提供強大的新工具來進行樣式比對。我們已經在最小比對建構 ??*?+?{n,m}?{n,}?{,n}? 中看過延伸。下面大部分的延伸都採用 (?char...) 形式,其中 char 是決定延伸類型的字元。

第一個延伸是內嵌註解 (?#text)。這會在正規表示式中內嵌註解,而不會影響其意義。註解在文字中不應該有任何關閉括號。範例如下:

/(?# Match an integer:)[+-]?\d+/;

這種註解風格在很大程度上已被 /x 修飾詞允許的原始自由格式註解所取代。

大部分的修飾詞,例如 /i/m/s/x (或任何組合),也可以使用 (?i)(?m)(?s)(?x) 內嵌在正規表示式中。例如:

/(?i)yes/;  # match 'yes' case insensitively
/yes/i;     # same thing
/(?x)(          # freeform version of an integer regexp
         [+-]?  # match an optional sign
         \d+    # match a sequence of digits
     )
/x;

內嵌修飾詞相較於一般的修飾詞可能具有兩個重要的優點。內嵌修飾詞允許為每個正規表示式樣式自訂一組修飾詞。這對於比對必須具有不同修飾詞的正規表示式陣列非常有用

$pattern[0] = '(?i)doctor';
$pattern[1] = 'Johnson';
...
while (<>) {
    foreach $patt (@pattern) {
        print if /$patt/;
    }
}

第二個優點是內嵌修飾詞 (除了修改整個正規表示式的 /p) 僅影響內嵌修飾詞所包含的群組內的正規表示式。因此,可以利用群組來定位修飾詞的效果

/Answer: ((?i)yes)/;  # matches 'Answer: yes', 'Answer: YES', etc.

內嵌修飾詞也可以使用 例如 (?-i) 來關閉任何已存在的修飾詞。修飾詞也可以組合成單一表達式,例如 (?s-i) 會開啟單行模式並關閉不區分大小寫。

嵌入式修飾詞也可以新增到非擷取群組中。(?i-m:regexp) 是非擷取群組,不區分大小寫地比對 regexp,並關閉多行模式。

向前察看和向後察看

本節探討向前察看和向後察看斷言。首先,一點背景知識。

在 Perl 正規表示式中,大多數正規表示式元素在比對時會「吃掉」一定數量的字串。例如,正規表示式元素 [abc] 在比對時會吃掉字串的一個字元,也就是說,Perl 會在比對後移到字串中的下一個字元位置。不過,有些元素在比對時並不會吃掉字元(推進字元位置)。到目前為止,我們看過的範例是錨點。錨點 '^' 比對行首,但不會吃掉任何字元。類似地,字詞邊界錨點 \b 比對字元比對 \w 的位置在不比對字元的旁邊,但它本身不會吃掉任何字元。錨點是零寬度斷言的範例:零寬度是因為它們不消耗任何字元,斷言是因為它們測試字串的某個屬性。在我們將正規表示式比對比喻為在森林中散步的類比中,大多數正規表示式元素會讓我們沿著步道前進,但錨點會讓我們停下來查看周圍環境。如果周遭環境符合條件,我們就可以繼續前進。但如果周遭環境不符合我們的要求,我們就必須回溯。

查看環境包括向前察看步道、向後察看,或同時進行。'^' 向後察看,以查看前面沒有字元。'$' 向前察看,以查看後面沒有字元。\b 同時向前和向後察看,以查看兩側的字元在「字詞性」上是否不同。

向前察看和向後察看斷言是錨點概念的概括。向前察看和向後察看是零寬度斷言,讓我們可以指定要測試哪些字元。向前察看斷言表示為 (?=regexp) 或(從 5.32 版開始,在 5.28 版中為實驗性質)(*pla:regexp)(*positive_lookahead:regexp);向後察看斷言表示為 (?<=fixed-regexp) 或(從 5.32 版開始,在 5.28 版中為實驗性質)(*plb:fixed-regexp)(*positive_lookbehind:fixed-regexp)。一些範例如下:

$x = "I catch the housecat 'Tom-cat' with catnip";
$x =~ /cat(*pla:\s)/;   # matches 'cat' in 'housecat'
@catwords = ($x =~ /(?<=\s)cat\w+/g);  # matches,
                                       # $catwords[0] = 'catch'
                                       # $catwords[1] = 'catnip'
$x =~ /\bcat\b/;  # matches 'cat' in 'Tom-cat'
$x =~ /(?<=\s)cat(?=\s)/; # doesn't match; no isolated 'cat' in
                          # middle of $x

請注意,這些括號是非擷取的,因為這些是非寬度斷言。因此,在第二個正規表示式中,擷取的子字串是整個正規表示式本身的子字串。先行斷言可以比對任意正規表示式,但後行斷言在 5.30 之前(?<=fixed-regexp)僅適用於固定寬度的正規表示式,固定數量的字元長度。因此,(?<=(ab|bc))是正確的,但 5.30 之前的(?<=(ab)*)則不是。

先行斷言和後行斷言的否定版本分別表示為(?!regexp)(?<!fixed-regexp)。或者,從 5.32 開始(在 5.28 中為實驗性),(*nla:regexp)(*negative_lookahead:regexp)(*nlb:regexp)(*negative_lookbehind:regexp)。如果正規表示式比對,則它們會評估為 true

$x = "foobar";
$x =~ /foo(?!bar)/;  # doesn't match, 'bar' follows 'foo'
$x =~ /foo(?!baz)/;  # matches, 'baz' doesn't follow 'foo'
$x =~ /(?<!\s)foo/;  # matches, there is no \s before 'foo'

以下是一個範例,其中包含空白分隔的字詞、數字和單破折號的字串要拆分為其組成部分。單獨使用/\s+/無法運作,因為破折號之間、或字詞或破折號之間不需要有空白。透過先行和後行斷言,可以建立拆分的其他位置

$str = "one two - --6-8";
@toks = split / \s+              # a run of spaces
              | (?<=\S) (?=-)    # any non-space followed by '-'
              | (?<=-)  (?=\S)   # a '-' followed by any non-space
              /x, $str;          # @toks = qw(one two - - - 6 - 8)

使用獨立子表示式來防止回溯

獨立子表示式(或原子子表示式)是在較大正規表示式的背景下,獨立於較大正規表示式運作的正規表示式。也就是說,它們會消耗字串中任意多或任意少的字元,而不考慮較大正規表示式是否能比對。獨立子表示式表示為(?>regexp)或(從 5.32 開始,在 5.28 中為實驗性)(*atomic:regexp)。我們可以先考慮一個一般正規表示式來說明它們的行為

$x = "ab";
$x =~ /a*ab/;  # matches

這顯然可以比對,但在比對的過程中,子表示式a*首先取得'a'。然而,這麼做會導致整個正規表示式無法比對,因此,在回溯之後,a*最終會放棄'a'並比對空字串。在此,a*比對的內容取決於正規表示式的其他部分比對的內容。

與獨立子表示式形成對比

$x =~ /(?>a*)ab/;  # doesn't match!

獨立子表達式 (?>a*) 不會理會正規表示式的其他部分,所以它看到一個 'a' 就會把它抓取。然後正規表示式的其他部分 ab 就無法配對。因為 (?>a*) 是獨立的,所以不會回溯,而獨立子表達式也不會放棄它的 'a'。因此整個正規表示式的配對會失敗。完全獨立的正規表示式也會發生類似的行為

$x = "ab";
$x =~ /a*/g;   # matches, eats an 'a'
$x =~ /\Gab/g; # doesn't match, no 'a' available

這裡 /g\G 創造了一個「標籤團隊」將字串從一個正規表示式傳遞到另一個正規表示式。有獨立子表達式的正規表示式很像這樣,將字串傳遞到獨立子表達式,再將字串傳遞回包覆的正規表示式。

獨立子表達式防止回溯的能力非常有用。假設我們想要配對一個非空字串,它被括號包覆,深度最多兩層。然後以下正規表示式會配對

$x = "abc(de(fg)h";  # unbalanced parentheses
$x =~ /\( ( [ ^ () ]+ | \( [ ^ () ]* \) )+ \)/xx;

正規表示式配對一個左括號、一個或多個交替的複本,以及一個右括號。交替是雙向的,第一個選項 [^()]+ 配對一個沒有括號的子字串,第二個選項 \([^()]*\) 配對一個由括號分隔的子字串。這個正規表示式存在的問題是它具有病態:它具有形式為 (a+|b)+ 的巢狀不確定量詞。我們在第 1 部分討論過,如果沒有可能的配對,這種巢狀量詞可能會花費指數級的時間來執行。為了防止指數級爆炸,我們需要在某個時間點防止無用的回溯。這可以透過將內部量詞包覆為獨立子表達式來完成

$x =~ /\( ( (?> [ ^ () ]+ ) | \([ ^ () ]* \) )+ \)/xx;

這裡,(?>[^()]+) 透過盡可能地吞噬字串並保留它來打破字串分割的簡併。然後配對失敗會失敗得更快。

條件式表達式

條件式表達式 是一種 if-then-else 陳述式,它允許我們根據某些條件選擇要配對的模式。有兩種條件式表達式:(?(條件)yes-regexp)(?(condition)yes-regexp|no-regexp)(?(條件)yes-regexp) 類似 Perl 中的 'if () {}' 陳述式。如果 條件 為真,則會配對 yes-regexp。如果 條件 為假,則會跳過 yes-regexp,而 Perl 會移到下一個正規表示式元素。第二種形式類似 Perl 中的 'if () {} else {}' 陳述式。如果 條件 為真,則會配對 yes-regexp,否則會配對 no-regexp

條件可以有數種形式。第一種形式僅為括號中的整數值(整數值)。如果對應的反向參照\整數值在正規表示式中較早比對成功,則為真。使用與擷取群組相關聯的名稱,寫成(<名稱>)('名稱'),也可以執行相同的動作。第二種形式為單獨的零寬度斷言(?...),可能是向前觀望、向後觀望或程式碼斷言(下一節會討論)。第三組形式提供測試,如果表達式在遞迴中執行((R))或從某些擷取群組呼叫,則傳回真,其參照方式為數字((R1)(R2),...)或名稱((R&名稱))。

條件的整數值或名稱形式讓我們能更靈活地選擇要比對的內容,根據正規表示式中較早比對成功的內容。這會搜尋形式為"$x$x""$x$y$y$x"的字詞

% simple_grep '^(\w+)(\w+)?(?(2)\g2\g1|\g1)$' /usr/dict/words
beriberi
coco
couscous
deed
...
toot
toto
tutu

條件的向後觀望允許較早的比對部分,連同反向參照,影響較後的比對部分。例如,

/[ATGC]+(?(?<=AA)G|C)$/;

比對 DNA 序列,其結尾為AAG,或其他鹼基對組合和'C'。請注意,形式為(?(?<=AA)G|C),而非(?((?<=AA))G|C);對於向前觀望、向後觀望或程式碼斷言,不需要在條件周圍加上括號。

定義命名模式

某些正規表示式在多個地方使用相同的子模式。從 Perl 5.10 開始,可以在模式的部分中定義命名子模式,以便可以在模式中的任何地方按名稱呼叫它們。此定義群組的語法模式為(?(DEFINE)(?<名稱>模式)...)。插入命名模式寫成(?&名稱)

以下範例說明此功能,使用前面提供的浮點數模式。三次以上使用的三個子模式是選用符號、整數的數字序列和小數部分。模式結尾的DEFINE群組包含其定義。請注意,小數部分模式是我們可以重複使用整數值模式的第一個地方。

/^ (?&osg)\ * ( (?&int)(?&dec)? | (?&dec) )
   (?: [eE](?&osg)(?&int) )?
 $
 (?(DEFINE)
   (?<osg>[-+]?)         # optional sign
   (?<int>\d++)          # integer
   (?<dec>\.(?&int))     # decimal fraction
 )/x

遞迴模式

此功能(在 Perl 5.10 中引入)大幅擴充了 Perl 模式比對的功能。透過使用建構 (?group-ref),在模式中的任何位置參照其他擷取群組,參考群組內的模式會用作獨立的子模式,取代群組參考本身。由於群組參考可以包含它所參考的群組,現在可以將模式比對套用於過去需要遞迴剖析器的任務。

為了說明此功能,我們將設計一個模式,用於比對字串中是否包含迴文。 (迴文是一種單字或句子,忽略空格、標點符號和大小寫後,從前往後讀和從後往前讀都一樣。 我們從觀察空字串或只包含一個字元字元的字串是迴文開始。 否則,它必須在前面有一個字元字元,在後面也有同一個字元字元,中間再有一個迴文。

/(?: (\w) (?...Here be a palindrome...) \g{ -1 } | \w? )/x

在兩端加上 \W* 來消除要忽略的內容,我們已經有了完整的模式

my $pp = qr/^(\W* (?: (\w) (?1) \g{-1} | \w? ) \W*)$/ix;
for $s ( "saippuakauppias", "A man, a plan, a canal: Panama!" ){
    print "'$s' is a palindrome\n" if $s =~ /$pp/;
}

(?...) 中,可以使用絕對和相對反向參考。整個模式可以用 (?R)(?0) 重新插入。如果您偏好為群組命名,可以使用 (?&name) 來遞迴到該群組。

一點魔法:在正規表示式中執行 Perl 程式碼

一般來說,正規表示式是 Perl 表示式的一部分。程式碼評估表示式則相反,允許任意 Perl 程式碼成為正規表示式的一部分。程式碼評估表示式標示為 (?{code}),其中code 是 Perl 陳述式的字串。

程式碼表示式是零寬度斷言,它們傳回的值取決於其環境。有兩種可能性:程式碼表示式用作條件式表示式 (?(condition)...) 中的條件,或不用於條件式表示式。如果程式碼表示式是條件,則會評估程式碼,並使用結果(最後一個陳述式的結果)來判斷真或假。如果程式碼表示式不用作條件,則斷言總是評估為真,並且結果會放入特殊變數 $^R 中。然後可以在正規表示式中後面的程式碼表示式中使用變數 $^R。以下是一些愚蠢的範例

$x = "abcdef";
$x =~ /abc(?{print "Hi Mom!";})def/; # matches,
                                     # prints 'Hi Mom!'
$x =~ /aaa(?{print "Hi Mom!";})def/; # doesn't match,
                                     # no 'Hi Mom!'

請仔細注意以下範例

$x =~ /abc(?{print "Hi Mom!";})ddd/; # doesn't match,
                                     # no 'Hi Mom!'
                                     # but why not?

乍看之下,您會認為它不應該列印,因為顯然 ddd 無法比對目標字串。但請看以下範例

$x =~ /abc(?{print "Hi Mom!";})[dD]dd/; # doesn't match,
                                        # but _does_ print

嗯。這裡發生了什麼事?如果您有持續關注,您會知道上述模式應與最後一個模式實際上(幾乎)相同;將 'd' 括在字元類別中不會改變它比對的內容。那麼,為什麼第一個不會列印,而第二個會列印呢?

答案在於正規表示式引擎所做的最佳化。在第一個情況中,引擎只看到純粹的舊字元(除了 ?{} 建構)。它夠聰明,可以在實際執行模式之前,就意識到字串 'ddd' 沒有出現在我們的目標字串中。但在第二個情況中,我們欺騙了它,讓它以為我們的模式比較複雜。它看了一下,看到了我們的字元類別,並決定它必須實際執行模式,才能判定它是否比對,而它在執行的過程中,在發現我們沒有比對之前,就遇到了列印陳述式。

若要更仔細了解引擎如何進行最佳化,請參閱以下「實用程式和偵錯」區段 "Pragmas and debugging"

使用 ?{} 的更多樂趣

$x =~ /(?{print "Hi Mom!";})/;         # matches,
                                       # prints 'Hi Mom!'
$x =~ /(?{$c = 1;})(?{print "$c";})/;  # matches,
                                       # prints '1'
$x =~ /(?{$c = 1;})(?{print "$^R";})/; # matches,
                                       # prints '1'

在尋找比對的過程中,當正規表示式回溯時,會發生區段標題中提到的神奇情況。如果正規表示式回溯到程式碼表達式,且使用 local 將內部使用的變數區域化,程式碼表達式產生的變數變更就會被取消!因此,如果我們想要計算某個字元在群組中比對的次數,我們可以使用,例如

$x = "aaaa";
$count = 0;  # initialize 'a' count
$c = "bob";  # test if $c gets clobbered
$x =~ /(?{local $c = 0;})         # initialize count
       ( a                        # match 'a'
         (?{local $c = $c + 1;})  # increment count
       )*                         # do this any number of times,
       aa                         # but match 'aa' at the end
       (?{$count = $c;})          # copy local $c var into $count
      /x;
print "'a' count is $count, \$c variable is '$c'\n";

它會列印

'a' count is 2, $c variable is 'bob'

如果我們將 (?{local $c = $c + 1;}) 替換為 (?{$c = $c + 1;}),變數變更在回溯期間不會被取消,我們會得到

'a' count is 4, $c variable is 'bob'

請注意,只有區域化變數變更才會被取消。程式碼表達式執行的其他副作用是永久的。因此

$x = "aaaa";
$x =~ /(a(?{print "Yow\n";}))*aa/;

產生

Yow
Yow
Yow
Yow

結果 $^R 會自動在地化,因此在存在回溯的情況下,它將會適當地運作。

此範例在條件中使用程式碼表達式,以比對定冠詞,在英文中為 'the',在德文中為 'der|die|das'

$lang = 'DE';  # use German
...
$text = "das";
print "matched\n"
    if $text =~ /(?(?{
                      $lang eq 'EN'; # is the language English?
                     })
                   the |             # if so, then match 'the'
                   (der|die|das)     # else, match 'der|die|das'
                 )
                /xi;

請注意,此處的語法為 (?(?{...})yes-regexp|no-regexp),而非 (?((?{...}))yes-regexp|no-regexp)。換句話說,在程式碼表達式的情況下,我們不需要在條件周圍加上額外的括號。

如果您嘗試使用程式碼表達式,其中程式碼文字包含在內插變數中,而不是直接出現在模式中,Perl 可能會讓您感到驚訝

$bar = 5;
$pat = '(?{ 1 })';
/foo(?{ $bar })bar/; # compiles ok, $bar not interpolated
/foo(?{ 1 })$bar/;   # compiles ok, $bar interpolated
/foo${pat}bar/;      # compile error!

$pat = qr/(?{ $foo = 1 })/;  # precompile code regexp
/foo${pat}bar/;      # compiles ok

如果正則表示式具有內插程式碼表達式的變數,Perl 會將正則表示式視為錯誤。然而,如果程式碼表達式已預先編譯成變數,則內插是允許的。問題是,為什麼這會是一個錯誤?

原因是變數內插和程式碼表達式一起會構成安全風險。此組合很危險,因為許多撰寫搜尋引擎的程式設計師通常會取得使用者輸入,並將其直接插入正則表示式中

$regexp = <>;       # read user-supplied regexp
$chomp $regexp;     # get rid of possible newline
$text =~ /$regexp/; # search $text for the $regexp

如果 $regexp 變數包含程式碼表達式,使用者便可以執行任意 Perl 程式碼。例如,某些小丑可能會搜尋 system('rm -rf *'); 來刪除您的檔案。在此意義上,內插和程式碼表達式的組合會「污染」您的正則表示式。因此,預設情況下,不允許在同一個正則表示式中同時使用內插和程式碼表達式。如果您不擔心惡意使用者,可以透過呼叫 use re 'eval' 來繞過此安全檢查

use re 'eval';       # throw caution out the door
$bar = 5;
$pat = '(?{ 1 })';
/foo${pat}bar/;      # compiles ok

另一種形式的程式碼表達式為「模式程式碼表達式」。模式程式碼表達式類似於一般程式碼表達式,但程式碼評估的結果會視為正則表示式,並立即比對。一個簡單的範例為

$length = 5;
$char = 'a';
$x = 'aaaaabb';
$x =~ /(??{$char x $length})/x; # matches, there are 5 of 'a'

此最後一個範例包含一般和模式程式碼表達式。它會偵測二進位字串 1101010010001... 是否具有 '1' 的費氏數列間距 0,1,1,2,3,5,...

    $x = "1101010010001000001";
    $z0 = ''; $z1 = '0';   # initial conditions
    print "It is a Fibonacci sequence\n"
        if $x =~ /^1         # match an initial '1'
                    (?:
                       ((??{ $z0 })) # match some '0'
                       1             # and then a '1'
		       (?{ $z0 = $z1; $z1 .= $^N; })
                    )+   # repeat as needed
                  $      # that is all there is
                 /x;
    printf "Largest sequence matched was %d\n", length($z1)-length($z0);

請記住,$^N 會設定為最後完成的擷取群組所配對到的內容。這會列印

It is a Fibonacci sequence
Largest sequence matched was 5

哈!用你的花園品種的正規表示式套件試試看...

請注意,變數 $z0$z1 沒有在編譯正規表示式時替換,這會發生在程式碼運算式外的普通變數。相反地,整個程式碼區塊會在 Perl 編譯包含字面正規表示式樣式的程式碼時,同時解析為 Perl 程式碼。

沒有 /x 修飾詞的正規表示式是

/^1(?:((??{ $z0 }))1(?{ $z0 = $z1; $z1 .= $^N; }))+$/

這表示程式碼區塊中仍然可以使用空白。不過,在使用程式碼和條件運算式時,正規表示式的延伸形式幾乎是建立和除錯正規表示式時必要的。

回溯控制動詞

Perl 5.10 導入了許多控制動詞,旨在提供對回溯程序的詳細控制,方法是直接影響正規表示式引擎,並提供監控技術。請參閱 "perlre 中的特殊回溯控制動詞" 以取得詳細說明。

以下只是一個範例,說明控制動詞 (*FAIL),它可以縮寫為 (*F)。如果將它插入正規表示式中,它將導致正規表示式失敗,就像它在樣式和字串之間不匹配時一樣。正規表示式的處理會像在任何「正常」失敗後一樣繼續進行,因此,例如,會嘗試字串中的下一個位置或另一個替代方案。由於不匹配不會保留擷取群組或產生結果,因此可能需要與嵌入式程式碼結合使用。

%count = ();
"supercalifragilisticexpialidocious" =~
    /([aeiou])(?{ $count{$1}++; })(*FAIL)/i;
printf "%3d '%s'\n", $count{$_}, $_ for (sort keys %count);

樣式從匹配字母子集的類別開始。每當它匹配時,就會執行類似 $count{'a'}++; 的陳述式,增加字母的計數器。然後 (*FAIL) 會執行它所說的,正規表示式引擎會根據手冊進行:只要尚未到達字串的結尾,就會在尋找另一個母音之前推進位置。因此,匹配或不匹配沒有差別,正規表示式引擎會進行處理,直到檢查完整個字串為止。(值得注意的是,使用類似

$count{lc($_)}++ for split('', "supercalifragilisticexpialidocious");
printf "%3d '%s'\n", $count2{$_}, $_ for ( qw{ a e i o u } );

的替代方案會慢得多。)

Pragmas 和偵錯

說到偵錯,Perl 中有幾個 pragma 可用於控制和偵錯正規表示式。我們在上一節已經遇到一個 pragma,use re 'eval';,它允許變數內插和程式碼表達式共存在正規表示式中。其他 pragma 為

use re 'taint';
$tainted = <>;
@parts = ($tainted =~ /(\w+)\s+(\w+)/; # @parts is now tainted

如果您的 perl 支援污染(請參閱 perlsec),taint pragma 會導致與污染變數匹配的任何子字串也受到污染。這通常不會發生,因為正規表示式通常用於從污染變數中提取安全位元。當您不提取安全位元,而是執行其他處理時,請使用 tainttainteval pragma 都是詞法作用域,這表示它們僅在封裝 pragma 的區塊結束之前才有效。

use re '/m';  # or any other flags
$multiline_string =~ /^foo/; # /m is implied

re '/flags' pragma(在 Perl 5.14 中引入)會開啟指定的正規表示式旗標,直到詞法作用域結束。請參閱 "'/flags' 模式" 在 re 中,以取得更多詳細資訊。

use re 'debug';
/^(.*)$/s;       # output debugging info

use re 'debugcolor';
/^(.*)$/s;       # output debugging info in living color

全域 debugdebugcolor pragma 允許使用者取得有關正規表示式編譯和執行之詳細偵錯資訊。debugcolor 與 debug 相同,除了偵錯資訊會以彩色顯示在可顯示終端機能力色彩序列的終端機上。以下是範例輸出

% perl -e 'use re "debug"; "abc" =~ /a*b+c/;'
Compiling REx 'a*b+c'
size 9 first at 1
   1: STAR(4)
   2:   EXACT <a>(0)
   4: PLUS(7)
   5:   EXACT <b>(0)
   7: EXACT <c>(9)
   9: END(0)
floating 'bc' at 0..2147483647 (checking floating) minlen 2
Guessing start of match, REx 'a*b+c' against 'abc'...
Found floating substr 'bc' at offset 1...
Guessed: match at offset 0
Matching REx 'a*b+c' against 'abc'
  Setting an EVAL scope, savestack=3
   0 <> <abc>           |  1:  STAR
                         EXACT <a> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   1 <a> <bc>           |  4:    PLUS
                         EXACT <b> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   2 <ab> <c>           |  7:      EXACT <c>
   3 <abc> <>           |  9:      END
Match successful!
Freeing REx: 'a*b+c'

如果您已經學習到本教學課程的這個部分,您可能可以猜出偵錯輸出的不同部分告訴您的內容。第一部分

Compiling REx 'a*b+c'
size 9 first at 1
   1: STAR(4)
   2:   EXACT <a>(0)
   4: PLUS(7)
   5:   EXACT <b>(0)
   7: EXACT <c>(9)
   9: END(0)

描述編譯階段。STAR(4) 表示有一個星號物件,在本例中為 'a',如果它匹配,則跳至第 4 行,PLUS(7)。中間幾行描述在匹配之前執行的某些啟發法和最佳化

floating 'bc' at 0..2147483647 (checking floating) minlen 2
Guessing start of match, REx 'a*b+c' against 'abc'...
Found floating substr 'bc' at offset 1...
Guessed: match at offset 0

然後執行匹配,其餘幾行描述此程序

Matching REx 'a*b+c' against 'abc'
  Setting an EVAL scope, savestack=3
   0 <> <abc>           |  1:  STAR
                         EXACT <a> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   1 <a> <bc>           |  4:    PLUS
                         EXACT <b> can match 1 times out of 32767...
  Setting an EVAL scope, savestack=3
   2 <ab> <c>           |  7:      EXACT <c>
   3 <abc> <>           |  9:      END
Match successful!
Freeing REx: 'a*b+c'

每一步驟都採用 n <x> <y> 格式,其中 <x> 是匹配的字串部分,而 <y> 是尚未匹配的部分。| 1: STAR 表示 Perl 在上述編譯清單中的第 1 行。請參閱 "perldebguts 中的「偵錯正規表示式」,以取得更多詳細資訊。

偵錯正規表示式的另一種方法是在正規表示式中嵌入 print 陳述式。這提供了交替中回溯的逐字逐句說明

"that this" =~ m@(?{print "Start at position ", pos, "\n";})
                 t(?{print "t1\n";})
                 h(?{print "h1\n";})
                 i(?{print "i1\n";})
                 s(?{print "s1\n";})
                     |
                 t(?{print "t2\n";})
                 h(?{print "h2\n";})
                 a(?{print "a2\n";})
                 t(?{print "t2\n";})
                 (?{print "Done at position ", pos, "\n";})
                @x;

會印出

Start at position 0
t1
h1
t2
h2
a2
t2
Done at position 4

另請參閱

這只是一個教學課程。有關 Perl 正規表示式的完整說明,請參閱 perlre 正規表示式參考頁面。

有關比對 m// 和替換 s/// 算子的更多資訊,請參閱 perlop 中的「類 Regexp 引用算子」。有關 split 算子的資訊,請參閱 perlfunc 中的「split」

有關正規表示式照護和餵養的絕佳全方位資源,請參閱 Jeffrey Friedl 所著的書籍《Mastering Regular Expressions》(由 O'Reilly 出版,ISBN 1556592-257-3)。

作者和版權

版權所有 (c) 2000 Mark Kvale。保留所有權利。目前由 Perl 搬運工維護。

本文件可根據與 Perl 相同的條款進行散布。

致謝

終止密碼 DNA 範例的靈感來自《Mastering Regular Expressions》第 7 章中的郵遞區號範例。

作者要感謝 Jeff Pinyan、Andrew Johnson、Peter Haworth、Ronald J Kimball 和 Joe Smith 提供所有有用的意見。