讀古今文學網 > 精通正則表達式(第3版) > 常用的元字符和特性 >

常用的元字符和特性

Common Metacharacters and Features

本章的其他部分——剩下大約30頁的內容簡要介紹下一頁列出的常見正則表達式元字符和概念。這裡的介紹並不是全面徹底的,不過也沒有任何一種正則工具涉及其中的所有內容。

從某種意義上說,這一節只是前兩章內容的總結,但同時也是為本章介紹更全面更深刻的知識做準備。讀者第一次接觸時,只需略讀本章就可以繼續閱讀下面各章,以後在需要的時候可以隨時回過頭來查閱細節。

有的工具添加了大量的新功能,也可能毫無根據地改變某些通用表示法,以滿足它們的特殊要求。儘管我有時會提到這些特殊的工具,但不會花太多的筆墨在工具的細節問題上。相反,在這一節我只希望介紹常見的元字符及其作用,以及與此相關的一些問題。我希望讀者能夠參考自己擅長的工具提供的使用手冊。

本節介紹的結構

字符表示法

☞115字符縮略表示法:\n、\t、\a、\b、\e、\f、\r、\v、…

☞116八進制轉義:\num

☞117十六進制/Unicode轉義:\xnum、\x{num}、\unum、\Unum,…

☞117控制字符:\cchar

字符組及相關結構

☞118 普通字符組:[a-z]和[^a-z]

☞119 幾乎能匹配任何字符的元字符:點號

☞120 單個字節:\C

☞120 Unicode組合字符序列:\X

☞120 字符組縮略表示法:\w、\d、\s、\W、\D、\S

☞121 Unicode屬性、區塊和分類:\p{Prop}、\P{Prop}

☞125 字符組運算符:[[a-z]&&[^aeiou]]

☞127 POSIX「字符組」方括號表示法:[[:alpha:]]

☞128 POSIX「collating序列」方括號表示法:[[.span-ll.]]

☞128 POSIX「字符等價類」方括號表示法:[[=n=]]

☞128 Emacs 語法類

錨點及其他「零長度斷言」

☞129 行/字符串起點:^、\A

☞129 行/字符串終點:$、\Z、\z

☞130 本次匹配的開始位置(或者上次匹配的結束位置):\G

☞133 單詞分界符:\b、\B、\<、\>,….

☞133 順序環視 (?=…)、(?!…);逆序環視 (?<=…)、(?<!…)

註釋和模式修飾詞

☞135 模式修飾詞:(?modifier),例如(?i)或(?-i)

☞135 模式作用範圍:(?modifier:…),例如(?i:…)

☞136 註釋:(?#…) 和#…

☞136 文字文本範圍:\Q…\E

分組、捕獲、條件判斷和控制

☞137 捕獲/分組括號:(…)、\1、\2,…

☞137 僅用於分組的括號:(?:…)

☞138 命名捕獲:(?<Name>…)

☞139 固化分組:(?>…)

☞139 多選結構:…|…|…

☞140 條件判斷:(?if then|else)

☞141 匹配優先量詞:*、+、?、{num,num}

☞141 忽略優先量詞:*?、+?、??、{num,num}?

☞142 佔有優先量詞:*+、++、?+、{num,num}+

字符表示法

Character Representations

這一組元字符能夠以清晰美觀的方式匹配其他方式中很難描述的某些字符。

字符縮略表示法

許多工具軟件提供了表示某些控制字符的元字符,其中有一些在所有機器上都是不變的,但也有些是很難輸入或觀察的:

\a 警報(例如,在「打印」時揚聲器發聲)。通常對應ASCII中的<BEL>字符,八進

制編碼007。

\b 退格 通常對應ASCII中的<BS>字符,八進制編碼010。(在許多流派中,「\b」只有在字符組內部才表示這樣的意義,否則代表單詞分界符☞133)。

\e Escape字符 通常對應ASCII中的<ESC>字符,八進制編碼033。

\f 進紙符 通常對應ASCII中的<FF>字符,八進制編碼014。

\n 換行符 出現在幾乎所有平台(包括Unix和DOS/Windows)上,通常對應ASCII的<LF>字符,八進制編碼 012。在MacOS 中通常對應ASCII的<CR>字符,十進制編碼 015。在 Java 或任意一種.NET 語言中,不論採用什麼平台,都對應ASCII<LF>字符。

\r 回車 通常對應ASCII的<CR>字符。在MacOS中,對應到ASCII的<LF>字符。在

Java或任意一種.NET語言中,不論採用什麼平台,都對應到ASCII的<CR>字符。\t 水平製表符 對應ASCII的<HT>字符,八進制編碼011。

\v 垂直製表符 對應ASCII的<VT>字符,八進制編碼013。

表3-6列出了幾種常用的工具及它們提供的某些字符縮略表示法。之前已經說過,某些語言在支持字符串文字時已經提供了同樣的字符縮略表示法。請不要忘記那一節(☞101),因為它涉及某些相關的陷阱。

會根據機器變化的字符?

從該表可以看出,在許多工具中,\n和\r的意義是隨操作系統的變化而變化的(注11),所以在使用時應格外小心。如果你需要在程序可能運行的所有平台上都能通用的「換行符」,請使用\n。如果需要一個對應特殊值的字符,例如HTTP協議定義的分隔符,請使用\012之類標準規定的字符(\012是ASCII中的換行符的八進制編碼)。如果你希望匹配DOS中的行終結字符,請使用「\015\012」。如果希望同時匹配 DOS 或 Unix 的換行字符,請使用「\015?\012」(它們通常是匹配行尾的字符,如果希望匹配行的開頭位置或結尾位置,請使用行錨點☞129)。

表3-6:幾款工具軟件及它們提供的元字符簡寫法

八進制轉義\num

支持八進制(以8為基數)轉義的實現方式通常容許以2到3位數字表示該值所代表的字節或字符。例如,「\015\012」表示ASCII 的CR/LF 序列。八進制轉義可以很方便地在正則表達式中插入平時難以輸入的字符。例如,在Perl中,我們可以使用「\e」作為ASCII的轉義字符,但是在awk中不行。

因為awk支持八進制轉義,我們可以直接使用ASCII代碼來表示escape字符:「\033」。

下一頁的表3-7列出了部分工具支持的八進制轉義。

有些實現方式很特殊,在其中「\0」能夠匹配字節 NUL。有的支持一位數字的八進制轉義,不過如果同時支持「\1」之類的反向引用,就不會提供這種功能。如果兩者發生衝突,則反向引用一般要優先於八進制轉義。有的容許出現 4 位數字的八進制轉義,不過通常會要求任何八進制轉義都必須以0開頭(例如java.util.regex)。

你可能會想,如果遇到\565 之類超出範圍的轉義數值(8 位的八進制數值範圍從\000 到\377)會發生什麼。大約一半的實現方式將其視為多餘一個字節的值(如果支持Unicode,則可能是Unicode字符),而其他實現方式會將其截斷為一個字節。一般來說,最好的辦法還是不要使用超過\377的八進制轉義。

十六進制及Unicode轉義:\xnum、\x{num}、\unum、\Unum

除了八進制轉義之外,許多工具軟件也支持十六進制轉義(以16為基數),以\x、\u或者是\U開頭。如果支持\x,則「\x0D\x0A」匹配CR/LF序列。表3-7列出了部分工具軟件支持的十六進制轉義。

除了知道採用的是哪種轉義之外,你可能還希望知道各種轉義能識別多少位的轉義值,以及是否能夠(或者必須)在數字兩端使用花括號。對此,表3-7同樣作了說明。

控制字符:\cchar

許多流派中可以用「\cchar」來匹配編碼值小於32的控制字符(有些能支持更大的值)。例如,「\cH」匹配一個Control-H字符,也就是ASCII中的退格符,而「\cJ」匹配ASCII的換行符(通常使用「\n」,不過有時也使用「\r」,這取決於具體的平台☞115)。

支持此結構的系統在細節上有所不同。與這個例子一樣,通常使用大寫英文字母是不會有問題的。在大多數實現方式中,你也可以使用小寫字母,不過也有的軟件不支持它們,例如Sun的Java regex Package。流派不同,對字母和數字之外的字符的處理是非常不同的,所以我推薦在使用\c時只使用大寫字母。

相關提示:GNU Emacs支持此功能,但它使用的元序列非常奇特「?\^char」(例如:「?\^H」 匹配ASCII編碼中的退格字符)。

表3-7:部分工具軟件及它們的正則表達式支持的八進制和十六進制轉義

字符組及相關結構

Character Classes and Class-Like Constructs

現在許多流派中,都有多種方法在正則表達式的某個位置指定一組字符,不過最通行的方法還是使用普通字符組。

普通字符組:[a-z]和[^a-z]

我們已經介紹過字符組的基本概念,不過我還是要強調,元字符的規定在字符組內外是有差別的。例如,在字符組內部「*」永遠都不是元字符,而「-」通常都是元字符。有些元序列,例如「\b」,在字符組內外的意義是不一樣的(☞116)。

在大多數系統中,字符組內部的順序環視是無關緊要的,而且使用範圍表示法而不是列出範圍內的所有字符並不會影響執行速度(例如,[0-9]與[908176354]是一樣的)。相反,某些實現方式不能完全優化字符組(比如Sun提供的Java regex package),所以最好是使用範圍表示法,因為如果有差別,這種表示法的速度會快一些。

字符組通常表示肯定斷言(positive assertion)。也就是說,它們必須匹配一個字符。排除型字符組仍然需要匹配一個字符,只是它沒有在字符組中列出而已。把排除型字符組理解為「匹配未列出字符的字符組」或許更容易一些(請務必閱讀下一節中關於點號和排除型字符組的警告)。「[^LMNOP]」通常等價於「[\x00-kQ-\xFF]」,即使在規定嚴格的8位系統中,這仍然成立,但是在Unicode之類字符的值可能大於255(\xFF)的系統中,排除型字符組「[^LMNOP]」可能包括成千上萬個字符——只是不包含L、M、N、O和P。

請務必理解使用範圍表示法的基本字符組。例如,用「[a-Z]」匹配字母就很可能存在遺漏,而且在任何情況下顯然都不是「所有字母」。而[a-zA-Z]則能匹配所有字母,至少對於ASCII編碼來說是這樣的(請參考「Unicode 屬性」中的\p{L}☞121)。當然,在處理二進制數據時,字符組中的『\x80-\xFF』範圍表示法完全適用。

幾乎能匹配任何字符的元字符:點號

在某些工具軟件中,點號用來縮略表示可以匹配任何字符的字符組,而在其他工具中,點號能匹配除了換行符之外的任何字符。這差別很細微,但如果所用的工具能夠處理包含多個邏輯行的目標文本(或者是文本編輯器中的多個邏輯行的文本),它就非常重要。關於點號,需要注意的有:

●在Sun的Java regex package之類的支持Unicode的系統中,點號不能匹配Unicode的行終結符(☞109)。

●匹配模式(☞111)會改變點號的匹配規則。

●POSIX規定,點號不能匹配NUL(值為0的字符),儘管大多數腳本語言容許文本中出現NULL(而且可以用點號來匹配)。

點號,還是排除型字符組

如果所使用的工具能夠在多行文本中進行搜索,請務必注意點號,它在通常情況下不能匹配換行符,而排除型字符組「[^〞]」通常都可以。如果把「〞.*〞」替換為「〞[^〞]*〞」,可能會帶來意想不到的效果。點號的匹配規定一般可以通過變換匹配模式來更改——請參考第 111頁的「點號通配模式」。

單個字節

Perl 和 PCRE(也包括 PHP)支持用\C匹配單個字節,即使該字節位於某個多字節編碼的字符之中(相反,其他功能都是基於字符的)。這個功能很危險,如果運用不當,可能會導致內部錯誤,所以只有在清楚自己所作所為的情況下,才能使用它。我找不到恰當運用的例子,所以下文不再提及。

Unicode組合字符序列:\X

Perl 和PHP 支持用\X縮略表示「\P{M}\p{M}*」,它可以視為點號的擴展。它匹配一個基本字符(除\p{M}之外的任何字符),之後可能有任意數目的組合字符(除\p{M}之外)。

之前已經介紹過(☞107),Unicode體系包括基本字符和組合字符,二者可以合成「看起來」的單個字符,例如a(『a』的編碼是U+0061,讀音符號的編碼是U+0300)。有的字符可能包含不止一個的組合字符。例如,就包括『c』,然後是變音符,最後是短音符(Unicode編碼分別是U+0063、U+0327和U+0306)。

如果希望匹配「francais」或者「franCais」,僅僅使用「fran.ais」或者「fran[cC]ais」還不夠保險,因為此方法假設『C』用單個Unicode代碼點U+00C7表示,而不是『c』加上變音符(U+0063加上U+0327)。如果需要專門處理,可以使用「fran(c,?|C)ais」,不過在這裡,用「fran\Xais」取代「fran.ais」是個好辦法。

除了能夠匹配結尾的組合字符之外,\X與點號還有兩個差別。其一是,\X始終能匹配換行符和其他Unicode行終結符(☞109),而點號只有在點號通配模式(☞111),或者工具軟件提供的其他匹配模式下才可以。另一點是,點號通配模式下的點號無論什麼情況下都能匹配任何字符,而「\X」不能匹配以組合字符開頭的字符。

字符組簡記法:\w、\d、\s、\W、\D、\S

通常支持的簡記法有:

\d 數字 等價於「[0-9]」,如果工具軟件支持Unicode,能匹配所有的Unicode數字。

\D 非數字字符 等價於「[^\d]」。

\w 單詞中的字符 一般等價於「[a-zA-Z0-9_]」。某些工具軟件中「\w」不能匹配下畫線,而另一些工具軟件的「\w」則能支持當前locale(☞87)中的所有數字和字符。如果支持Unicode,「\w」通常能表示所有數字和字符,而在java.util.regex和PCRE (也包括PHP)中,「\w」嚴格等價於「[a-zA-Z0-9_]」。

\W 非單詞字符 等價於「[^\w]」。

\s 空白字符 在支持ASCII的系統中,它通常等價於「[·\f\n\r\t\v]」。在支持Unicode的系統中,有時包含 Unicode 的「換行」控制字符 U+0085,有時包含「空白(whitespace)」屬性\p{Z}(參見下一節的介紹)。

\S 非空白字符 等價於「[^\s]」。

87 頁已經介紹過,POSIX 的 locale 設定會影響這些簡記符號的含義(尤其是\w)。支持Unicode的程序中,\w通常能匹配更多的字符,例如\p{L}(下一節介紹)和下畫線。

Unicode屬性,字母表和區塊:\p{Prop}、\P{Prop}

表面上看,Unicode只是一套字符映射規則(☞106),其實Unicode標準遠遠不止這些。它還定義了每個字符的性質(qualities),例如「這個字符是小寫字母」,「這個字符是從右往左看的」,「這個字符是標記字符(mark),它必須與其他字符一同使用」等等。

不同的正則表達式系統對這些屬性的支持也不相同,但是許多支持Unicode的程序能夠通過「\p{quality}」和「\P{quality}」支持其中的一部分。比如「\p{L}」就是個簡單的例子,這裡『L』的意思是「字母(letter)」(相對於數字number、標點punctuation和口音accent,之類)。『L』是一種普通屬性(general property,也稱為分類category)。我們馬上會瞭解到,可以用「\p{…}」和「\P{…}」來測試其他「屬性」,當然支持最廣泛的還是常見的屬性。

常見的屬性請見表3-8。每個字符(實際上是代碼點,包括那些目前沒有對應字符的代碼點)都可以用一個普通屬性匹配。普通屬性的名字是單個字符(例如『L』表示字母letter,『S』表示符號 symbol,等等),但是某些系統中可以用多個字母表述屬性(例如『Letter』和『Symbol』),比如Perl就支持這樣。

在某些系統中,單字母屬性名可能不需要花括號(例如,用\pL 而不是\p{L})。有的系統可能要求(或者是容許)使用『In』或『Is』前綴(例如\p{IsL})。講解擴展屬性(additional qualities)時,我們會見到要求使用Is/In前綴的例子(注12)。

按照表 3-9,每個用單字符表示的普通屬性可以進一步分為多個雙字母表示的子屬性(sub-property),例如「字母(letter)」又可以分為「小寫字母」、「大寫字母」、「標題首字母(titlecase letter)」、「修飾符字母」和「其他字母」。每個代碼點能且只能屬於一種子屬性。

表3-8:基本的Unicode屬性分類

要補充的是,某些實現方式支持特殊的復合子屬性,例如用\p{L&}表示「分大小寫的(cased)」字母,也就是說「[\p{Lu}\p{Ll}\p{Lt}]」。

表3-9還給出了某些實現方式支持的屬性名的全稱(例如,「Lowercase_Letter」,而不是「Ll」)。按照 Unicode 的標準,各種形式都應該能夠接受(例如『LowercaseLetter』、『LOWERCASE_LETTER』、『Lowercase·Letter』、『lowercase-letter』),不過,為了保持一致,我推薦使用表3-9中的形式)。

字母表(Scripts)有的系統能夠按照字母表(書寫系統 writing system)的名字以「\p{…}」來匹配。例如,用\p{Hebrew}匹配希伯來文獨有的字符(但不包含其他書寫系統中常見的字符,例如空格和標點)。

某些字母表是基於語言的(例如印度古哈拉地語、泰國語、切羅基語,等等)。有的覆蓋了多種語言(例如拉丁文、西裡爾文),還有些語言包含多種字母表,例如日語的字符就來自平假名、片假名、漢語和拉丁語。請讀者參考自己系統的文檔獲取完整的信息。

字母表不會包含特定的書寫系統中的所有字符,而只包含獨屬於(或者幾乎獨屬於)此書寫系統中的字符。常見的字符,例如空格和標點不屬於任何字母表,而是屬於通用的IsCommon偽字母表(pseudo-script),用「\p{IsCommon}」匹配。還有一個偽字母表Inherited,它包括從其所屬的字母表中基本字符繼承而來的組合字符。

表3-9:基本的Unicode子屬性

區塊(Block)。區塊類似(但是比不上)字母表,區塊表示Unicode字符映射表中一定範圍內的代碼點。例如,Tibetan區塊表是從U+0F00到U+0FFF的256個代碼點。其中的字符,在 Perl 和 java.util.regex 中可以用\p{InTibetan}來匹配,在.NET 中可以用\p{IsTibetan}來匹配(細節見後文)。

區塊有許多種,包括對應大多數書寫系統的區塊(希伯來、泰米爾、基本拉丁語、西裡爾文等等),以及特殊的字符組型(貨幣、箭頭、文本框、印刷符號等)。

Tibetan 是一個典型的區塊,因為其中的所有字符都是按照西藏文定義的,此區塊之外不存在專屬於藏語的字符。不過,區塊仍然不如字母表,原因如下:

●區塊可能包含未賦值的代碼點。例如,Tibetan區塊中大約有25%的代碼點沒有分配字符。

●並不是看起來與區塊相關的所有字符都在區塊內部。例如,在 Currency 區塊中就沒有通用的貨幣符號『¤』,也沒有常見的$、¢、£、和¥(幸好,這時候我們可以用currency屬性\p{Sc})。

●區塊通常包含不相關的字符。例如¥(表示「元」)屬於Latin_1_Supplement區塊。

● 屬於某個字母表的字符可能同時包含於多個區塊。例如,希臘字符同時出現在 Greek和Greek_Extended區塊中。

對區塊的支持比對字母表的支持更普遍。不過這兩者很容易混淆,因為在命名上存在許多重疊(例如,Unicode同時提供了Tibetan字母表和Tibetan區塊)。

此外,按照下頁的表3-10所示,這些命名本身也沒有統一的標準。在Perl和 java.util.regex中,Tibetan區塊表示為「\p{InTibetan}」,但是在.NET中又表示為\p{IsTibetan}(更糟糕的是,在Perl中這是Tibetan字母表的另一種表示法)。

其他屬性(Other properties/qualities)上面介紹的知識並不是通用的。表3-10詳細介紹了它們的適用情況。

要補充的是,Unicode還定義了許多能夠通過「\p{…}」結構訪問的屬性,其中包括字符的書寫順序環視(從左至右還是從右至左,等等)、與字符相關的元音,以及其他屬性。有些實現方式還容許用戶根據需要臨時創建屬性。請參考具體的程序提供的文檔瞭解細節。

表3-10:屬性/字母表/區塊的支持情況

簡單的字符組減法:[[a-z]-[aeiou]]

.NET提供的字符組「減法」容許我們在字符組中進行減法運算。例如,「[[a-z]-[aeiou]]」匹配的字符就是「[a-z]」能夠匹配字符的減去「[aeiou]」能夠匹配的字符,也就是ASCII編碼中小寫的非元音字母。

另一個例子是「[\p{P}-[\p{Ps}\p{Pe}]]」,它能夠匹配\p{P}中除「[\p{Ps}\p{Pe}]」之外的字符,也就是說,它能匹配除了》和(之類成對的符號之外的所有標點符號。

完整的字符組集合運算:[[a-z]&&[^aeiou]]

Sun的Java regex package中的字符組能夠進行完整的集合運算(並、減、交)。它的語法有別於前一節中簡單的字符組減法(尤其是,在Java中匹配小寫非元音字母的字符組[[a-z]&&[^aeiou]])。在詳細介紹減法之前,我們先來看兩個簡單的集合運算:OR和AND。

OR容許用戶以字符組方式在字符組中添加字符:[abcxyz]也可以表示為[[abc][xyz]]、[abc[xyz]]或 [[abc]xyz]等等。OR用來把多個集合合併為新的集合。從概念上說,它有點像多種語言提供的「按位或」運算符:『|』或是『or』。在字符組中,OR 只不過是一種簡記法,儘管包括排除型字符組在某些情況下更方便。

AND對兩個集合進行概念上的「與」運算,只保留同時屬於兩個字符組的字符。它的寫法是在兩個字符組中添加特殊的字符組元字符&&。例如[\p{InThai}&&\P{Cn}],它通過對\p{InThai}和\P{Cn}進行交運算(只保留同時屬於兩個集合的字符),匹配Thai區塊中所有已經賦值的代碼點。\P{…}中的『P』是大寫,匹配不具備此屬性的字符,所以\P{Cn}匹配的就是除未賦值的代碼點之外的代碼點,也就是已經賦值的代碼點(要是 Sun 能夠識別已賦值屬性(Assigned quality),就可以用\p{Assigned}替換\P{Cn})。

請不要混淆OR和AND。它們的含義取決於用戶的看法。例如[[this][that]]讀作「[this]或者[that]匹配的字符」,其實它的真正意思是「[this]和[that]能夠匹配的所有字符」,這只是對同一個問題的兩種看法。

相比之下,AND要清楚一些:[\p{InThai}&&\P{Cn}]讀作「只匹配在\p{InThai}和\P{Cn}中出現的字符」,儘管它有時候也讀作「匹配屬於\p{InThai}和\P{Cn}的交集中的字符。」

看法的不同可能會造成混亂:我叫做 OR 和 AND 的運算,某些人可能叫做 AND 和INTERSECTION。

以集合運算符進行字符組的減法\P{Cn}可以寫作[^\p{Cn}],所以在匹配「Thai block中已經賦值的字符」時,[\p{InThai}&&\P{Cn}]也可以寫作[\p{InThai}&&[^\p{Cn}]]。這樣的改變並沒有多少意義,只是它有助於說明一個通用的模式:「Thai block中已賦值字符」比「所有 Thai block 中的字符,減去未賦值的字符」更好理解,於是我們知道表示「減去」。

這樣就回到了本節開頭「[[a-z]&&[^aeiou]]」的例子,現在我們知道如何進行字符組的減法了。其模式為:「[this&&[^that]]」表示「[this]減去[that]」。我發現用&&和[^…]進行雙重否定很難記憶,所以記住模式「[…&&[^…]]」就夠了。

通過環視功能模擬字符組的集合運算 如果所使用的程序不支持字符組集合運算,但支持環視功能(☞133),則可以自己模擬集合運算。可以用環視功能寫成(注13)。儘管它並不如內建的集合運算有效率,環視仍然是非常方便的做法。這個例子可以用4種不同的方式來實現(在.NET中需要以 IsThai替換InThai☞125)。

POSIX「字符組」方括號表示法

我們通常所說的字符組,在POSIX標準中稱為方括號表達式(bracket expression)。POSIX中的術語「字符組」指的是在方括號表達式(注 14)內部使用的一種特殊的功能(special feature),而我們可以認為它們是Unicode的字符屬性的原型。

POSIX字符組是POSIX方括號表達式使用的幾種特殊元字符序列之一。比如[:lower:]表示當前locale(☞87)中的所有小寫字母。對英文文本來說,[:lower:]等於 a-z。因為整個序列只有在方括號表達式內才是有效的,所以對應的完整的字符組應該是「[[:lower:]]」。這種表示法的確很難看。但是,它比「[a-z]」更好用,因為它能包含 o,n 之類當前 locale中定義的「小寫字母」。

POSIX字符組的詳細列表根據locale的變化而變化,但是下面這些通常都能支持:

[:alnum:] 字母字符和數字字符。

[:alpha:] 字母。

[:blank:] 空格和製表符。

[:cntrl:] 控制字符。

[:digit:] 數字。

[:graph:] 非空字符(即空白字符,控制字符之外的字符)。

[:lower:] 小寫字母。

[:print:] 類似[:graph:],但是包含空白字符。

[:punct:] 標點符號。

[:space:] 所有的空白字符([:blank:]、換行符、回車符及其他)。

[:upper:] 大寫字母。

[:xdigit:] 十六進制中容許出現的數字(例如0-9a-fA-F)。

支持Unicode屬性(☞121)的系統可能會在Unicode支持中加入這些POSIX結構。Unicode屬性結構更為強大,所以如果可能,這些結構應該有提供。

POSIX「collating序列」方括號表示法:[[.span-ll.]]

Local可以包含對應的collating序列,用來決定其中的字符如何排序。例如,在西班牙語中,按照慣例,ll兩個字母在排序時作為一個邏輯字符(例如在tortilla中),排在l和m之間,日爾曼語字母s位於s和t中間,但是排序時類似它等價於兩個字母ss。這些規則可能用collating序列命名來表示,例如,span-ll和eszet。

collating序列會把多個實體字符映射到單個邏輯字符,在span-ll的例子中,ll被視為「一個字符」,來保持與POSIX正則引擎的兼容。也就是說,「[^abc]」能夠匹配『ll』兩個字母。

collating 序列的元素可以包含在方括號表達式中,使用[.….]表示法:「torti[[.span-ll.]]a」匹配tortilla。單個collating序列可以匹配組合而成的字符。此種情況下,方括號表達式可以匹配多個實體字符。

POSIX「字符等價類」方括號表示法:[[=n=]]

有的locale定義了字符等價類,表示某些字符在進行排序之類的操作時應視為等價。例如,某locale可能定義了這樣一個等價類『n』,包含n和n,或者是另一個等價類『a』,包含a、a和a。等價類的表示法類似[:…:],但是用等號取代冒號,我們可以在方括號表達式中引用這些等價類:「[[=n=][=a=]]」能夠匹配剛才出現的任意一個字符。

如果一個字符等價類的名稱只包含一個字母,但沒有在locale中定義,則它默認就等於同樣名字的collating序列。local通常包含作為collating序列的普通字符[.a.]、[.b.]、[.c.]之類——如果沒有定義特殊的等價類,「[[=n=][=a=]]」就等於「[na]」。

Emacs語法類

GNU Emacs不支持傳統的「\w」、「\s」之類;相反,它使用特殊的序列來引用「語法類(syntax classes)」:

\schar 匹配Emacs語法類中char描述的字符。

\Schar 匹配不在Emacs語法類中的字符。

「\sw」匹配「構成單詞(word constituent)」的字符,而「\s-」匹配「空白字符」。在其他系統中,它們分別寫作「\w」和「\s」。

Emacs 的特殊之處在於,在 Emacs 中,這些字符組包含的字符是可以臨時更換的,所以,構成單詞的字符組中的字符可以根據所編輯文本的變化而變化。

錨點及其他「零長度斷言」

Anchors and Other"Zero-Width Assertions"

錨點和其他「零長度斷言」並不會匹配實際的文本,而是尋找文本中的位置。

行/字符串的起始位置:^、\A

脫字符「^」匹配需要搜索的文本的起始位置,如果使用了增強的行錨點匹配模式(☞112),它還能匹配每個換行符之後的位置。在某些系統中,增強模式下「^」還能匹配Unicode的行終結符(☞109)。

如果可以使用,則無論在什麼匹配模式下,「\A」總是能夠匹配待搜索文本的起始位置。

行/字符串的結束位置:$、\Z和\z

從下一頁的表格3-11可以看出,「行結束位置(end of line)」的概念比行開頭位置要複雜。在不同的工具軟件中,「$」的意義也不同,不過最常見的意思是匹配目標字符串的末尾,也可以匹配整個字符串末尾的換行符之前的位置。後一種情況更為常見,它容許「s$」(匹配「以

s結尾的行」)來匹配,即以s和換行符結尾的行。

「$」的另兩種常見的意思是,只匹配目標文本的結束位置,或是匹配任何一個換行符之前的位置。在某些Unicode系統中,這些規則中的換行符會被替換為Unicode的行終結符(☞109) (Java為了處理Unicode的行終結符,為「$」設定了非常複雜的語意☞370)。

匹配模式(☞112)可以改變「$」的意義,匹配字符串中的任何換行符(或者是Unicode中的行終結符)。

如果支持,「\Z」通常表示「未指定任何模式下」「$」匹配的字符,通常是字符串的末尾位置,或者是在字符串末尾的換行符之前的位置。作為補充,「\z」只匹配字符串的末尾,而不考慮任何換行符。表3-11中列出了少數例外。

表3-11:腳本語言中的行錨點

註:

在這些情況下,Sun的Java regex package支持Unicode的行終結符(☞109)。

Ruby的$和^能匹配字符串中的換行符,但是\A和\Z則不能。

Python的\Z只能匹配字符串的結束位置。

Ruby的\A與^不同,只能匹配字符串的起始位置。

Ruby的\Z與$不同,可以匹配字符串的結尾位置,或是字符串結尾的換行符之前的位置。

(請參考第91頁的版本信息)

匹配的起始位置(或者是上一次匹配的結束位置):\G

「\G」首先出現在Perl中。在使用/g(☞51)的匹配中,\G對迭代操作非常有用,它能夠匹配上一次匹配結束的位置。在第一次迭代時,「\G」匹配字符串的開頭,與「\A」一樣。

如果匹配不成功,「\G」的匹配會重新指向字符串的起始位置。這樣,如果重複應用某個正則表達式,例如進行Perl的「s/…/…/g」操作,或者在其他語言中調用「找出所有匹配(match all)」函數,在匹配失敗的同時,「\G」也會指向字符串的開頭位置,這樣以後進行其他類型的匹配操作便不受影響。

根據我的觀察,Perl的「\G」有3個值得注意而且很有用的方面:

●「\G」的指向位置是每個目標字符串的屬性,而不是設定這些位置的正則表達式的屬性。也就是說,多個正則表達式可以依次對同一個字符串進行匹配,都使用上一輪匹配設定的「\G」。

●Perl 的正則運算符有一個選項(Perl 的/c修飾符☞315),它規定了,如果匹配失敗,不要重新設定「\G」,而只是保持之前的值不變化。如果結合上面那一點,就可以從某個位置開始嘗試用多個正則表達式進行匹配,直到匹配能夠成功,然後在下面的文本中繼續尋找匹配。

●「\G」對應的屬性可以用與正則表達式無關的結構(Perl的pos函數☞313)來檢查和修改。可能有人希望設定這個位置來「規定」從什麼位置開始尋找匹配,以及只從那個位置開始的匹配。同樣,如果語言支持本條功能,而沒有直接提供上一條功能,我們可以用本條功能來模擬。

下頁的補充內容中有個例子展示了這些特性的用法。除了這些便捷之外,Perl的「\G」還存在一個問題,即它必須出現在正則表達式的開頭,這樣才能正常工作。不過幸運的是,這似乎是最自然的用法。

之前匹配的結束位置,還是當前匹配的開始位置?

不同的實現方式之間存在一個區別,「\G」匹配的到底是「當前匹配的起始位置」還是「前一次匹配的結束位置」。在絕大多數情況下,這兩者是等價的,所以大多數時候這個問題並不要緊。但也有些不常見的情況下,它們是有區別的。215頁有個例子說明了這種情況,不過最容易的還是用一個專門的例子來理解:把「x?」應用到『abcde』。這個表達式能夠在『☞abcde』匹配成功,但其實它沒有匹配任何文本。在進行全局查找-替換時,正則表達式會重複應用,每次處理上一次操作之後的文本,除非傳動裝置會做些特別的處理,「上次匹配完成的位置」總是它開始的位置。為了避免無窮循環,在這種情況下傳動裝置會強行前進到下一個字符(☞148),如果對『abcde』應用s/x?/!/g,結果就是『!a!b!c!d!e!』。

傳動裝置這樣處理會帶來一個問題:「上一次匹配的終點」不等於「本次匹配的起點」。如果是這樣,問題就來了:「\G」匹配哪個位置呢?在 Perl 中,對『abcde』應用 s/\Gx?/!/g得到『!abcde』,所以我們知道,在Perl中,「\G」只匹配上一次匹配的結束位置。如果傳動裝置自行驅動,Perl的\G肯定無法匹配。

另一方面,在其他某些工具軟件中使用同樣的查找-替換命令,會得到『!a!b!c!d!e!』,也就是說\G是在每次匹配的起始位置匹配成功,然後由傳動裝置進行驅動。

關於「\G」的匹配,也不能完全相信文檔,微軟的.NET和Sun的Java文檔,在我通知這兩家公司之前,都是錯誤的(然後他們才修正)。現在的狀態就是,PHP和Ruby中的「\G」指向當前匹配的開頭位置,而Perl、java.util.regex和.NET匹配上一次匹配的結束位置。

單詞分界符:\b、\B、\<、\>…

單詞分界符的作用與行錨點一樣,也是匹配字符串中的某些位置。單詞分界符可以分為兩類,一類中單詞起始位置分界符和結束位置分界符是相同的(通常是\<和\>),另一類則以統一的分界符來匹配(通常是\b)。兩類都提供了非單詞分界符序列(通常是\B)。表 3-12給出了一些例子。如果所使用的工具軟件沒有提供單獨的起始位置和結束位置分界符,但支持環視功能,用戶也可以用它來模擬那兩種單詞分界符。在下面的表格中,如果程序本身沒有提供分開的單詞分界符,我會列出實踐中的做法。

單詞分界符通常可以這樣理解,這個位置的一邊是「單詞字符(word character)」,另一邊則不是。每種工具軟件對「單詞字符」的理解都不一樣,對單詞邊界的理解也是這樣。如果單詞分界符等於\w當然好辦,但很多時候事實並非如此。例如,在PHP和java.util.regex 中,\w 只能匹配 ASCII 字符,而不是 Unicode 字符,所以在表格中我會使用帶有Unicode單詞屬性\pL(這是「\p{L}」的縮略表示法☞121)的環視功能。

無論單詞分界符怎麼定義「單詞字符」,單詞分界符的測試通常只是簡單的字符相鄰測試。所有的正則引擎都不會對單詞進行語意分析:它們認為「NE14AD8」是一個單詞,而「M.I.T.」不是。

順序環視(?=…)、(?!…);逆序環視(?<=…)、(?<!…)

在前一章「使用環視功能在數值中插入逗號」(☞59)的例子中,我們已經介紹過順序環視和逆序環視結構(統稱為環視)。但關於它們還有很重要的一點沒有介紹,那就是環視結構中能夠出現什麼樣的表達式。大多數實現方式都限制了逆序環視中的表達式的長度(但是順序環視則沒有限制)。

Perl 和 Python 的限制是最嚴格的,逆序環視只能匹配固定長度的文本。使用(?<!\w)和(?<!this|that)不會出錯,但是 (?<!books?)和(?<!^\w+:)則不行,因為它們匹配的文本的長度是不確定的。某些情況下,(?<!books?)可以重寫為「(?<!book)(?<!books)」,儘管第一眼看上去它並不好理解。

表3-12:若幹工具軟件中使用的單詞分界符元字符

(請參考第91頁的版本信息)

更高一層次的支持容許逆序環視中出現不同長度的多選分支,所以(?<!books?)可以寫作(?<!book|books)。PCRE(因此也包括PHP中的preg套件)支持此功能。

最高層次的支持可以匹配任意長度的文本,只是其長度不能為無限。「(?<!books?)」可以直接使用,但是(?<!^\w+:)則不行,因為\w+能夠匹配的長度沒有限制。Sun 的 Java regex package支持這樣。

就問題本身來說,這三級支持其實是一樣的,因為它們都表達同樣的意思,儘管有的表達方式可能不太好看,而且對匹配的長度進行了嚴格的限制。中間一級只不過是「語法(syntactic sugar)」,表達方式更美觀而已。而第四級支持容許逆序環視結構中的子表達式匹配任意長度的文本,甚至包括「(?<!^\w+:)」。微軟的.NET就支持這一級,它無疑是最棒的,但是如果運用不當,也可能帶來嚴重的效率問題(如果逆序環視能夠匹配任意長度的文本,引擎必須從字符串的起始位置開始檢查逆序環視表達式,如果逆序環視是從長字符串的尾端開始的,這樣就會浪費許多工夫)。

註釋和模式修飾符

Comments and Mode Modifiers

在許多流派中,使用下面的結構,就能夠在正則表達式內部,切換使用之前介紹的正則表達式模式和匹配模式(☞110)。

模式修飾符:(?modifier),例如(?i)和(?-i)

現在,有許多流派容許在正則表達式中設定匹配模式(☞110)。常見的就是「(?i)」,它會啟用不區分大小寫的匹配,而「(?-i)」會停用此功能。例如,「<B>(?i)very(?-i)</B>」會對中間的「very」進行不區分大小寫的匹配。而兩端的 tag 仍然必須為大寫。它可以匹配『<B>VERY</B>』 和『<B>Very</B>』,但不能匹配『<b>Very</b>』。

這個例子在大多數支持「(?i)」的系統中都可以運行,例如Perl、PHP、java.util.regex、Ruby(注15)和.NET。在Python和Tcl中則不行,因為它們不支持「(?-i)」。

除Python之外,大多數實現方式中,「(?i)」的作用範圍都只限於括號內部(也就是說,在閉括號之後就失效)。所以,我們可以拿掉「(?-i)」,將整個不需要區分大小寫的部分放在一個括號裡,把「(?i)」放在最前面:「<B>(?:(?i)very)</B>」。

模式修飾符中能夠出現的不只有『i』。在大多數系統中,我們至少可以使用表3-13列出的修飾符。有的系統還提供了更多的選項。比如 PHP 就提供了少數其他選項(☞446),Tcl也是如此(請參考文檔)。

表3-13:常見模式修飾符字母

模式作用範圍:(?modifier:…),例如(?i:…)

如果所使用的系統支持模式修飾範圍,這樣前一節的例子可以更加簡化。「(?i:…)」表示模式修飾符的作用範圍只有在括號內有效。這樣,「<B>(?:(?i)very)</B>」就可以化簡為「<B>(?i:very)</B>」。

如果支持,這種格式一般可以應用於所有的模式修飾符字母。Tcl 和 Python 都支持「(?i)」格式,但是不支持「(?i:…)」格式。

註釋:(?#…)和#…

某些流派支持用「(?#…)」添加註釋。實際上,如果流派支持寬鬆排列和註釋模式(☞111),就很少使用這種功能。不過,如果在字符串文字中很難插入換行符,用這種格式加入註釋就非常方便,例如VB.NET就是如此(☞99,420)。

文字文本範圍:\Q…\E

\Q…\E是由Perl引入的,它會消除其中除\E之外所有元字符的特殊含義(如果沒有\E,就會一直作用到正則表達式末端)。其中的所有字符都會被當成普通文字文本來對待。如果在構建正則表達式時包含變量,此功能就非常有用。

舉例來說,為了響應Web檢索,我們可能希望把用戶輸入的內容保存在$query中,然後使用m/$query/i。但是,如果$query包含某些字符,例如『C:\WINDOWS\』,結果是運行時錯誤,因為這不是一個合法的正則表達式(最後有一個單獨的反斜線)。

「\Q…\E」可以解決這個問題。如果在Perl中使用m/\Q$query\E/i,則$query就從『C:\WIN-DOWS\』變成「C\:\\WINDOWS\\」,結果能找到用戶期望的『C:\WINDOWS\』。

但是在面向對像和程序式處理(☞95)中,這個特性的用處要打折扣。在構建需要用在正則表達式中的字符串時,有很方便的函數對這個值「上保險」,以便用在正則表達式中。例如,在VB中,我們可以使用Regex.Escape(☞432);PHP提供了preg_quote函數(☞470),Java有quote方法(☞395)。

就我所知,支持「\Q…\E」的引擎只有java.util.regex和PCRE(也包括PHP的preg套件)。請注意,我剛剛提到,這個功能是在Perl中引入的(而且我給出了Perl的例子),你可能覺得很奇怪,為什麼剛剛沒有提到Perl。Perl支持正則文字中的\Q…\E(也就是直接出現在程序中的正則表達式),但是不能在可能使用插值的內容和變量上使用。細節問題請參考第 7章(☞290)。

在低於1.6.0 的Java 中,java.util.regex對字符組中的「\Q…\E」支持是不可靠的,不建議使用。

分組,捕獲,條件判斷和控制

Grouping,Capturing,Conditionals,and Control

捕獲/分組括號:(…)和\1,\2,…

普通的無特殊意義的括號通常有兩種功能:分組和捕獲。普通括號常見的形式是「(…)」,但有的流派中使用「\(…\)」,例如GNU Emacs、sed、vi和grep。

如41、43 和 57 頁的圖所示,捕獲型括號的編號是按照開括號出現的次序,從左到右計算的。如果提供了反向引用,則這些括號內的子表達式匹配的文本可以在表達式的後面部分用「\1」、「\2」來引用。

括號的常用功能之一是從字符串中提取數據。括號中的子表達式匹配的文本(也可以稱為「括號匹配文本(the text matched by the parentheses)」)在不同的程序中可以通過不同的方式來引用,例如Perl的$1和$2(常見的錯誤是在正則表達式之外使用「\1」,這種形式只在sed和vi中能用)。下一頁的表3-14說明了各種程序中,匹配完成之後訪問文本的方法。它還說明了訪問整個表達式匹配的文本,或者某一組捕獲型括號所匹配文本的做法。

僅用於分組的括號:(?:…)

僅用於分組的括號「(?:…)」不能用來提取文本,而只能用來規定多選結構或者量詞的作用對象。它們不會按照$1、$2之類編號。在「(1|one)(?:and|or)(2|two)」匹配之後,$1包含『1』或者『one』,$2包含『2』或者『two』。只用於分組的括號也叫非捕獲型括號(non-capturing parentheses)。

非捕獲型括號的價值體現在好幾個方面。它們能夠把複雜的表達式變得清晰,這樣讀者不會擔心在其他地方用到$1會產生混亂。而且它們還有助於提高效率。如果正則引擎不需要記錄捕獲型括號匹配的內容,速度會更快,所用的內存也更少(第6章詳細講解效率問題)。

非捕獲型括號的另一個用途是利用多個成分構建正則表達式。在第76頁的例子中,$Host-nameRegex保存的是用來匹配主機名的正則表達式。如果使用它來提取主機名兩端的空白,在Perl中是m/(\s*)$HostnameRegex(\s*)/。然後$1和$2分別保存開頭和結尾的空白,但結尾的空白其實是保存在$4中的,因為$HostnameRegex包含兩組捕獲型括號。

$HostnameRegex=qr/[-a-z0-9]+(\.[-a-z0-9]+)*\.(com|edu|info)/i;

表3-14:若幹工具軟件及其中訪問捕獲文本的方法

如果這兩組括號是非捕獲型的,我們就可以按照直觀的方式使用$HostnameRegex。另一種辦法是使用命名捕獲,儘管Perl沒有提供這種功能,我們還是會介紹它。

命名捕獲:(?<Name>…)

Python和PHP的preg引擎,以及.NET引擎,都能夠為捕獲內容命名。Python和PHP使用的語法是「(?P<name>…)」,而.NET使用「(?<name>…)」。我更喜歡.NET的語法。下面是一個.NET的例子:

「\b(?<Area>\d\d\d\)-(?<Exch>\d\d\d)-(?<Num>\d\d\d\d)\b」

在Python/PHP中是這樣:

「\b(?P<Area>\d\d\d\)-(?P<Exch>\d\d\d)-(?P<Num>\d\d\d\d)\b」

這個表達式會用美國電話號碼的各個部分「填充」Area、Exch和Num命名的內容。然後我們可以通過名稱來訪問各個括號捕獲的內容,例如在VB.NET和大多數.NET語言中,可以使用RegexObj.Groups(〞Area〞),在C#中使用RegexObj.Groups[〞Area〞],在Python中使用RegexObj.group(〞Area〞),在PHP 中使用$matches[〞Area〞]。這樣程序看起來更清晰。

如果要在正則表達式內部引用捕獲的文本,.NET中使用「\k<Area>」,Python和PHP中使用「(?P=Area)」。

在Pyhon和.NET(但不包括PHP)中,可以在同一個表達式中多次使用同樣的命名。例如美國電話號碼的區號部分的形式是『(###)』或者『###-』,為了匹配,我們可以使用(用.NET語法):「…(?:\((?<Area>\d\d\d)\)|(?<Area>\d\d\d)-)…」。無論哪一組匹配成功,都會把3位的區號保存到Area中。

固化分組:(?>…)

如果詳細瞭解正則引擎的匹配原理(☞169),就很容易理解固化分組。在這裡只說一點,就是一旦括號內的子表達式匹配之後,匹配的內容就固定下來(固化(atomic)下來無法改變),在接下來的匹配過程中不會變化,除非整個固化分組的括號都被棄用,在外部回溯中重新應用。下面這個簡單的例子會幫助我們理解這種匹配的「固化」性質。

「!.*!」能夠匹配『!Hola!』,但是如果「.*」在固化分組「!(?>.*)!」中就無法匹配。在這兩種情況下,「.*」都會首先匹配盡可能多的內容,但是之後的「!」無法匹配,會強迫「.*」釋放之前匹配的某些內容(最後的『!』)。如果使用了固化分組,就無法實現,因為「.*」在固化分組中,它永遠也不會「交還」已經匹配的任何內容。

儘管這個例子沒有什麼實際價值,固化分組還是有重要的用途。尤其是,它能夠提高匹配的效率(☞171),而且能夠對什麼能匹配,什麼不能匹配進行準確地控制(☞269)。

多選結構:…|…|…

多選結構能夠在同一位置測試多個子表達式。每個子表達式稱為一個多選分支(alternative)。符號「|」有很多稱呼,不過「或(or)」和「豎線(bar)」最為常見。有的流派使用「\|」。

多選結構的優先級很低,所以「this and|or that」的匹配等價於「(this and)|(or that)」,而不是「this (and|or) that」,雖然and|or看上去是一個單位。

大多數流派都容許出現空的多選分支,例如。空表達式在任何情況下都能匹配,所以這個例子等於「(this|that)?」(注16)。

POSIX標準禁止出現空多選分支,lex和大多數版本的awk也是如此。我認為,考慮到空多選分支的簡便和清晰,保留它不無益處。Larry Wall告訴我:「我認為,保留它就好像在數學中保留0一樣有意義」。

條件判斷:(?if then|else)

這個結構容許用戶在正則表達式中使用 if/then/else 判斷。if 部分是特殊的條件表達式(a special kind of conditional expression),下文馬上會有介紹。then和else部分是普通的子表達式。如果if部分測試為真,則嘗試then的表達式,否則嘗試else部分(else部分也可以不出現,果真如此的話,可以省略『|』)。

if的種類因流派的不同而不同,但是大多數實現方式都容許在其中引用捕獲的子表達式和環視結構。

測試對捕獲型括號的特殊引用。如果 if 部分是一個括號中的編號,而對應編號的捕獲型括號參與了匹配,其值為「true」。下面的例子匹配<IMG> tag,無論是是單獨出現的,或者是在<A>…</A>中出現的。代碼採用帶註釋的寬鬆排列格式,條件判斷結構(這裡的沒有else部分)以粗體標注。

「(?(1)…)」測試中的(1)會測試第一組捕獲型括號是否參與了匹配。「參與匹配」不等於「實際匹配了文本」,來看個簡單的例子:

下面兩種辦法都可以匹配可能包含在「<…>」中的單詞:「(<)?\w+(?(1)>)」可以,「(<?)\w+(?(1)>)」則不行。它們之間唯一的區別在於第一個問號出現的位置。在第一種(正確的)辦法中,問號作用於整個捕獲型括號,所以括號(以及包含的內容)不是必須匹配的。在第二個例子中,捕獲型括號不是可選的——只有其中的「<」才是,所以無論『<』是否匹配了文本,它都「參與匹配」。也就是說,「(?(1)…)」中的if部分總是「true」。

如果能夠使用命名捕獲(☞138),就可以在括號中使用命名,而不是編號。

用環視做測試。完整的環視結構,例如「(?=…)」和「(?<=…)」,可以用於 if 測試。如果環視能夠匹配,它返回「true」,執行then部分。否則會執行else部分。來看個專門設計的例子,它會在「NUM:」之後的位置嘗試匹配「\d+」,但是在其他位置嘗試使用「\w+」。環視條件判斷以下畫線標注。

條件判斷的其他測試。Perl 提供了一種複雜的條件判斷結構,容許在測試中使用任意 Perl代碼。返回值作為測試的值,根據它來判斷then或者else部分是否應該嘗試。詳細信息請參考第7章的第327頁。

匹配優先量詞:*、+、?、{num,num}

量詞(星號、加號、問號,以及區間元字符,它們能夠限製作用對象的匹配次數)已經有過詳細介紹。不過,需要注意的是,在某些工具中使用「\+」和「\?」來取代「+」和「?」。同樣,在某些更老的工具中,量詞不能限定反向引用,也不能限定括號。

區間:{min,max}或者\{min,max\}

區間可以被認為是「計數量詞(counting quantifier)」,因為用戶可以通過區間指定匹配成功所必須的下限和上限。如果只設置了一個數值(例如「[a-z]{3}」或者「[a-z]\{3\}」,依流派決定),匹配的次數就等於這個值。它等同於「[a-z][a-z][a-z]」(儘管兩者的效率可能有所不同☞251)。

需要注意的是,不要認為「X{0,0}」的意思是「X 不能出現」。「X{0,0}」沒有意義,因為它的意思是「不需要匹配「X」,也就是說實際上根本不需要進行任何嘗試」。它基本等於「X{0,0}」不存在——如果存在 X,它也可以被正則表達式之後出現的某些元素匹配,所以這種做法是行不通的(注17)。要實現「不容許存在」,請使用否定性環視。

忽略優先量詞:*?、+?、??、{num,num}?

有的工具提供了不那麼美觀的量詞:*?、+?、??和{min,max}?。這些是忽略優先的量詞。量詞在正常情況下都是「匹配優先(greedy)」的,匹配盡可能多的內容。相反,這些忽略優先的量詞會匹配盡可能少的內容,只需要滿足下限,匹配就能成功。其中的差異有深遠的影響,詳細的介紹在下一章(☞159)。

佔有優先量詞:*+、++、?+、{num,num}+

這些量詞目前只有java.util.regex和PCRE(以及PHP)提供,但是很可能會流行開來,佔有優先量詞類似普通的匹配優先量詞,不過他們一旦匹配某些內容,就不會「交還」。它們類似固化分組,如果理解了基本的匹配過程,就很容易理解佔有優先量詞。

從某種意義上來說,佔有優先的量詞只是些表面工夫,因為它們可以用固化分組來模擬。「.++」與「(?>.+)」的結果完全一樣,只是足夠智能的實現方式能對佔有優先量詞進行更多的優化。