perlreguts - Perl 正規表示式引擎說明。
這份文件試圖說明正規表示式引擎的核心運作方式。正規表示式引擎是 perl 程式碼庫中相當重要的一部分,但卻鮮少人了解。這份文件試圖改善這個情況。它來自於作者的經驗、原始碼中的註解、其他關於正規表示式引擎的論文、perl5-porters 郵件列表中的回饋,以及其他地方的資訊。
注意!務必了解本文中討論的行為和結構,反映的是作者在撰寫本文時對引擎的理解。它不是 API 定義,純粹是供想要修改正規表示式引擎或了解正規表示式引擎如何運作的人參考的內部指南。閱讀本文的文件的讀者預期已經詳細了解 perl 的正規表示式語法及其用法。如果您想了解 Perl 正規表示式的基礎知識,請參閱 perlre。如果您想用自己的正規表示式引擎取代現有的引擎,請參閱 perlreapi。
關於是說「regexp」還是「regex」存在一些爭議。在本文中,我們將使用「regex」一詞,除非有特殊原因不使用,在這種情況下,我們將說明原因。
在討論 regex 時,我們需要區分它們的原始碼形式和內部形式。在本文中,當我們談論它們的文字、原始碼形式時,我們將使用「模式」一詞,當我們談論它們的內部表示時,我們將使用「程式」一詞。這些對應於 Mark Jason Dominus 在他的「Rx」論文中使用的術語S-regex和B-regex(請參閱 「參考文獻」 中的 [1])。
正規表示式引擎是一個程式,它會採用一組以迷你語言指定的約束,然後將這些約束套用至目標字串,並判斷該字串是否符合這些約束。請參閱 perlre 以取得語言的完整定義。
用較不誇張的說法,這項工作的首要部分是將樣式轉換成電腦可以有效率地用來尋找字串中配對點的東西,而第二個部分則是執行搜尋本身。
為此,我們需要透過剖析文字來產生一個程式。然後,我們需要執行該程式來尋找與字串配對的點。而且我們需要有效率地完成這整個過程。
儘管有點令人困惑,而且有些人反對這個術語,但有必要檢視 regexp.h 中已經存在多年的註解
這基本上是一種非確定有限狀態機的線性編碼(又稱為剖析技術中的語法圖表或「鐵路正規形式」)。
「鐵路正規形式」這個術語有點深奧,較常見的說法是「語法圖表」或「鐵路圖表」。儘管如此,它提供了正規表示式程式的有用心智圖像:每個節點都可以視為一個軌道單元,只有一個入口,在大部分情況下只有一個出口點(有一些軌道會分岔,但統計上並不多),而且整體形成一個只有一個入口和一個出口點的配置。配對過程可以視為一輛沿著軌道行駛的車子,而系統中的特定路線則由在每個可能的連接點讀取的字元決定。車子可以在任何一點脫軌,但它只能在與軌道配對時繼續前進。
因此,樣式 /foo(?:\w+|\d+|\s+)bar/
可以視為下列圖表
[start]
|
<foo>
|
+-----+-----+
| | |
<\w+> <\d+> <\s+>
| | |
+-----+-----+
|
<bar>
|
[end]
事實上,Perl 的正規表示式現今比這種結構複雜許多,但以這種方式視覺化它有助於在嘗試了解其概況時,而且它與目前的實作相當接近。
更精確地說,我們會說正規表示式程式是圖形的編碼。圖形中的每個節點對應於原始正規表示式模式的一部分,例如文字字串或分支,並有一個指標指向表示要比對的下一個元件的節點。由於「節點」和「操作碼」在 Perl 原始碼中已有其他意思,我們會將正規表示式程式中的節點稱為「regop」。
程式以 regnode
結構的陣列表示,其中一個或多個表示程式的單一 regop。結構 regnode
是所需最小的結構,且具有與所有其他較大結構共用的欄位結構。(在此文件之外,「regnode」一詞有時用於表示「regop」,這可能會造成混淆。)
除了 BRANCH
之外,所有 regop 的「next」指標都實作串接;在兩端都有 BRANCH
的「next」指標會連接兩個替代方案。[這裡我們有一個微妙的語法相依性:一個個別的 BRANCH
(相對於它們的集合)永遠不會因為運算子優先順序而與任何東西串接。]
某些類型 regop 的運算元是文字字串;對其他類型來說,它是引導至子程式的 regop。特別是,BRANCH
節點的運算元是分支的第一個 regop。
注意:正如鐵路比喻所暗示的,這不是樹狀結構:分支的尾端會連接到 BRANCH
集合之後的事物。這就像單一鐵軌線,在進入車站或鐵路調車場時會分岔,在從另一側出來時會重新連接。
regop 的基本結構在 regexp.h 中定義如下
struct regnode {
U8 flags; /* Various purposes, sometimes overridden */
U8 type; /* Opcode value as specified by regnodes.h */
U16 next_off; /* Offset in size regnode */
};
其他較大的類似 regnode
的結構定義在 regcomp.h 中。它們幾乎像是子類,因為它們具有與 regnode
相同的欄位,結構中可能接著有其他欄位,而且在某些情況下,某些基本欄位的特定含義(和名稱)會被覆寫。以下是更完整的說明。
regnode_1
regnode_2
regnode_1
結構具有相同的標頭,後接一個四位元組的引數;regnode_2
結構則包含兩個兩位元組的引數
regnode_1 U32 arg1;
regnode_2 U16 arg1; U16 arg2;
regnode_string
用於字串常數的 regnode_string
結構,在標頭後接一個一位元組的長度,然後是字串資料。字串的尾端會以零位元組補齊,以便節點的總長度為四位元組的倍數
regnode_string char string[1];
U8 str_len; /* overrides flags */
regnode_charclass
方括號字元類別由 regnode_charclass
結構表示,它有一個四位元組的引數,然後是一個 32 位元組(256 位元)的位元圖,表示 Latin1 範圍內哪些字元包含在類別中。
regnode_charclass U32 arg1;
char bitmap[ANYOF_BITMAP_SIZE];
名稱以 ANYOF_
開頭的各種旗標用於特殊情況。上述 Latin1 比對和執行時才知道的事情儲存在 "Perl 的私人結構" 中。
regnode_charclass_posixl
還有一個較大的字元類別結構形式,用於表示 /l
比對下的 POSIX 字元類別,稱為 regnode_charclass_posixl
,它有一個額外的 32 位元位元圖,表示已包含哪些 POSIX 字元類別。
regnode_charclass_posixl U32 arg1;
char bitmap[ANYOF_BITMAP_SIZE];
U32 classflags;
regnodes.h 定義了一個名為 PL_regnode_arg_len[]
的陣列,它以 regnode
大小(4 位元組)為單位提供每個操作碼的大小。巨集用於根據 str_len
欄位計算 EXACT
節點的大小。
regop 定義在 regnodes.h 中,它是由 regcomp.pl 從 regcomp.sym 產生的。目前,可能的不同 regop 的最大數量限制為 256,其中約有四分之一已使用。
一組巨集讓存取欄位更容易且更一致。這些巨集包括 OP()
,用於確定類似 regnode
的結構類型;NEXT_OFF()
,這是到下一個節點的偏移量(稍後會詳細說明);ARG()
、ARG1()
、ARG2()
、ARG_SET()
,以及用於讀取和設定引數的等效巨集;以及用於操作字串和 regop 承載類型的 STR_LEN()
、STRING()
和 OPERAND()
。
在正規表示式引擎中,有兩個不同的「下一個 regnode」概念,在思考時務必將它們區分開來,因為它們在許多地方的概念上重疊,但在不重疊的地方,差異至關重要。對大多數 regnode 類型來說,這兩個概念在實務上(幾乎)相同。這兩種類型為 REGNODE_AFTER
,在編譯期間大量使用,但在執行期間僅偶爾使用,以及 regnext
,在執行期間大量使用,而在編譯期間僅偶爾使用。
這是已編譯正規表示式程式中的「位置上後一個 regnode」。對於較小的 regnode 類型,它在底層是 regnode_ptr+1
,但由於 regnode 大小會有所不同,且會隨著時間而改變,我們提供巨集來隱藏這些可怕的細節。
它在編譯階段大量使用,但僅在執行階段由少數選定的 regnode 類型使用。它也在用於傾印正規表示式程式以進行除錯的程式碼中大量使用。
有許多巨集可用於根據情況盡可能有效率地計算此值。正規巨集為 REGNODE_AFTER()
,它是最強大的,應可處理我們遇到的任何情況,但也有可能是最慢的。對於您知道目前的 regnode 大小為常數,且知道其類型或操作碼的特殊情況,還有兩個額外的巨集。在這種情況下,您可以使用 REGNODE_AFTER_opcode()
或 REGNODE_AFTER_type()
。
在正規表示式引擎的舊版本中,REGNODE_AFTER()
稱為 NEXTOPER
,但發現這會造成混淆,因此將其重新命名。還有一個 REGNODE_BEFORE()
,但它不安全,不應在新的程式碼中使用。
這是可以透過跳躍前進 regnode 的 NEXT_OFF()
成員值來到達的 regnode,或在少數情況下,透過 regnode_1
結構的 arg1
欄位進行較長的跳躍。子常式 regnext()
會透明地處理這一點。在大部分情況下,regnode 的 regnext
是在目前 regnode 成功配對後應該執行的 regnode,但在某些情況下可能並非如此。在迴圈控制和分支控制 regnode 類型中,regnext 可能表示一些特殊情況,對於 BRANCH 節點,regnext
是如果目前節點執行失敗,應該執行的下一個 BRANCH,而某些迴圈控制 regnode 會將 regnext 設為迴圈的結尾,以便如果目前反覆運算無法配對,它們可以跳到自己的清理程式。
大多數 regnode 類型不會在執行流程中建立分支,而撇開最佳化不談,「下一個」這兩個概念是相同的。例如,在編譯階段,SBOL opcode 的 regnext
和 REGNODE_AFTER
是相同的。這不成立的主要地方是 BRANCH
regnode,其中 REGNODE_AFTER
代表分支中的模式起點,而 regnext
代表如果這個分支無法配對,則連結到下一個 BRANCH,或如果這是最後一個分支,則為 0。量詞的迴圈邏輯也會以類似的方式使用這兩種類型的區別,其中 REGNODE_AFTER
是迴圈結構的內部,而 regnext
指向迴圈的結尾。
在編譯期間,引擎可能不知道特定節點的 regnext 為何,因此在編譯期間,regnext
僅用於必須使用且已知為正確的地方。在編譯階段的最後,我們會遍歷正規表示式程式並適當地修正 regnext 資料,並執行各種最佳化,這些最佳化可能會導致在建構期間需要的 regnode 變得多餘,或者我們可能會用一個更小的 regnode 取代一個較大的 regnode,並用最佳化 regnode 填補空隙。因此,我們可能會從類似以下的內容開始
BRANCH
EXACT "foo"
BRANCH
EXACT "bar"
EXACT "!"
並用類似的方式取代
TRIE foo|bar
OPTIMIZED
OPTIMIZED
OPTIMIZED
EXACT "!"
TRIE
節點的 REGNODE_AFTER
將會是一個 OPTIMIZED
regnode,理論上 regnext
會與 REGNODE_AFTER
相同。但將 OPTIMIZED
regnode 作為 noop 執行三次會很低效,因此最佳化器會修正 regnext
,讓此類節點在執行階段跳過。
在執行階段,我們幾乎只使用 regnext()
,只有在特定情況下才會使用 REGNODE_AFTER
,而這類情況對特定 regnode 類型來說具有明確的意義。例如 /x+/ 會產生
PLUS
EXACT "x"
END
PLUS
regnode 的 regnext
是 END
regnode,而 PLUS
regnode 的 REGNODE_AFTER
是 EXACT
regnode。EXACT
regnode 的 regnext
和 REGNODE_AFTER
是 END
regnode。
廣義來說,執行字串與樣式的比對包含以下步驟
這些步驟在 perl 程式實際執行時發生的位置,取決於樣式是否包含任何字串變數的內插。如果發生內插,則編譯會在執行時發生。如果沒有,則編譯會在編譯時執行。(/o
修改器會改變這一點,而 qr//
在某種程度上也會改變。) 引擎並不太在意這一點。
此程式碼主要位於 regcomp.c 中,以及標頭檔 regcomp.h、regexp.h 和 regnodes.h 中。
編譯從 pregcomp()
開始,它大多是一個初始化包裝器,將工作分配給其他兩個例程以進行繁重的工作:第一個是 reg()
,它是解析的起點;第二個是 study_chunk()
,負責最佳化。
pregcomp()
中的初始化大多包含建立和填入資料的特殊結構 RExC_state_t
(定義於 regcomp.c)。regcomp.h 中幾乎所有內部使用的例程都將其中一個結構的指標作為第一個參數,名稱為 pRExC_state
。此結構用於儲存編譯狀態,並包含許多欄位。同樣地,有許多巨集在這個變數上執行:任何看起來像 RExC_xxxx
的都是操作此指標/結構的巨集。
reg()
是解析程序的開頭。它負責解析模式的任意區塊,直到字串的結尾或在模式中遇到的第一個右括號。這表示它可用於解析頂層正規表示式或群組括號內的任何區段。它也處理 Perl 正規表示式中的「特殊括號」。例如,在解析 /x(?:foo)y/
時,reg()
將在某個時間點被呼叫來解析從「?」符號到「)」符號(包含「)」符號)的區塊。
此外,reg()
負責解析模式中的一個或多個分支,並透過正確設定其下一個指標來「完成它們」。為了進行解析,它會重複呼叫 regbranch()
,regbranch()
負責處理它看到的最多第一個 |
符號。
regbranch()
會呼叫 regpiece()
,regpiece()
處理量化詞後面的「項目」。為了解析「項目」,會呼叫 regatom()
。這是最低層級的例程,它會解析常數字串、字元類別和各種特殊符號,例如 $
。如果 regatom()
遇到「(」字元,它會呼叫 reg()
。
解析通常會包含兩個主要步驟,第一個是計算已編譯程式的規模,第二個則是實際編譯程式。但現在只有一個主要步驟,根據輸入模式的長度進行最初的粗略猜測,如果需要,在解析過程中會增加長度,然後再修剪為實際使用的長度。
然而,在過程中發生各種情況時,可能會發生必須從頭開始重新解析的情況。一個範例是如果程式變得很長,導致程式中的跳躍無法放入正常可用的 16 位元中。有兩個特殊 regop 可以容納更大的跳躍目的地,BRANCHJ 和 LONGBRANCH。解析會重新開始,並使用這些 regop 取代正常的較短 regop。每當需要重新開始解析時,函式會傳回失敗並設定一個旗標,指出需要執行的動作。這會傳遞到頂層常式,該常式會採取適當的動作並從頭開始重新開始。如果需要更長的跳躍,會在 RExC_state_t 結構中設定 RExC_use_BRANCHJ 旗標,函式會在決定如何執行分支之前檢查該結構。
在大多數情況下,發現問題的函式會設定因果旗標並立即傳回失敗。"解析複雜性" 包含這個運作方式的明確範例。在其他情況下,例如對編號的括號群組進行前向參考,我們需要完成解析才能知道該編號群組是否實際出現在模式中。在這些情況下,解析會在最後重新執行,並了解其中發生了多少群組。
常式 regtail() 由 reg() 和 regbranch() 呼叫,以便正確「設定尾端指標」。在執行時,當我們到達分支的尾端時,我們需要前往群組括號後的節點。然而,在解析時,我們不知道尾端在哪裡,直到我們到達那裡,因此當我們到達時,我們必須返回並適當地更新偏移量。regtail 用於讓這件事變得更容易。
解析過程的微妙之處在於,像 /foo/ 這樣的正規表示式最初會解析成具有單一分支的交替。只有在之後,最佳化器才會將單一分支交替轉換為更簡單的形式。
呼叫圖如下所示
reg() # parse a top level regex, or inside of
# parens
regbranch() # parse a single branch of an alternation
regpiece() # parse a pattern followed by a quantifier
regatom() # parse a simple pattern
regclass() # used to handle a class
reg() # used to handle a parenthesised
# subpattern
....
...
regtail() # finish off the branch
...
regtail() # finish off the branch sequence. Tie each
# branch's tail to the tail of the
# sequence
# (NEW) In Debug mode this is
# regtail_study().
語法形式可能是這樣
atom : constant | class
quant : '*' | '+' | '?' | '{min,max}'
_branch: piece
| piece _branch
| nothing
branch: _branch
| _branch '|' branch
group : '(' branch ')'
_piece: atom | group
piece : _piece
| _piece quant
上述描述的含意是,包含巢狀括弧的模式會導致呼叫圖表在 reg()
、regbranch()
、regpiece()
、regatom()
、reg()
、regbranch()
等中循環多次,直到達到最深的巢狀層級。上述所有常式都會傳回一個指向 regnode
的指標,通常是最後一個新增至程式的 regnode
。然而,一個複雜之處在於,reg()
會傳回 NULL 來剖析用於內嵌修改器的 (?:)
語法,設定旗標 TRYAGAIN
。TRYAGAIN
會向上傳播,直到被擷取為止,在某些情況下會由 regatom()
擷取,否則會由 regbranch()
無條件擷取。因此,regbranch()
永遠不會將此旗標傳回給 reg()
。此旗標允許將模式(例如 (?i)+
)偵測為錯誤(量詞在正規表示式中沒有後續項目;在 m/(?i)+ <-- HERE / 中標示為 <-- HERE)。
另一個複雜之處在於,如果程式需要儲存 Unicode,則用於程式的表示會有所不同,但直到剖析過程中,才有可能確定是否需要儲存 Unicode。Unicode 的程式表示較大,且無法有效地進行比對。(請參閱下方的〈「Unicode 和在地化支援」〉,以取得更多詳細資訊,了解原因。)如果模式包含 Unicode 文字,則程式顯然需要儲存 Unicode。否則,剖析器會樂觀地假設可以使用較有效率的表示,並在此基礎上開始調整大小。然而,如果剖析器在模式中遇到必須儲存為 Unicode 的項目,例如表示字元文字的 \x{...}
逸出序列,則表示所有先前計算的大小都需要使用適用於 Unicode 表示的值重新執行。這是剖析需要重新啟動的另一個範例,而且可以且會立即執行。函數傳回失敗,並設定旗標 RESTART_UTF8
(使用巨集 REQUIRE_UTF8
封裝)。此重新啟動要求會以類似的方式在呼叫鏈中向上傳播,直到在 Perl_re_op_compile()
中「被擷取」,將模式標示為包含 Unicode,並重新啟動調整大小的通道。執行時期程式碼區塊中的結構也有可能需要 Unicode 表示,這會由 S_compile_runtime_code()
傳回 false 給 Perl_re_op_compile()
來發出訊號。
重新啟動先前使用 regatom()
中的 longjmp
回到 Perl_re_op_compile()
中的 setjmp
來實作,但此方法證明是有問題的,因為後者是一個包含許多自動變數的大函數,與 setjmp
的浮現控制流程互動不良。
從 perl 的 5.9.x 開發版本開始,您可以 use re Debug => 'PARSE'
來查看有關解析程序的一些追蹤資訊。我們將從一些簡單的模式開始,並建立更複雜的模式。
因此,當我們解析 /foo/
時,我們會看到類似下表的內容。左側顯示正在解析的內容,數字表示下一個 regop 將轉到的位置。右側的內容是圖形的追蹤輸出。這些名稱選擇簡短,以減少螢幕上的密度。'tsdy' 是 regtail()
的特殊形式,它會執行一些額外的分析。
>foo< 1 reg
brnc
piec
atom
>< 4 tsdy~ EXACT <foo> (EXACT) (1)
~ attach to END (3) offset to 2
然後,產生的程式看起來像
1: EXACT <foo>(3)
3: END(0)
如您所見,即使我們解析出一個分支和一個部分,它最終只是一個原子。最終程式向我們展示了事情如何運作。我們有一個 EXACT
regop,後跟一個 END
regop。括號中的數字表示節點的 regnext
轉到的位置。END
regop 的 regnext
未使用,因為 END
regop 表示我們已成功配對。左側的數字表示 regnode 陣列中 regop 的位置。
現在讓我們嘗試一個更困難的模式。我們將新增一個量詞,所以現在我們有模式 /foo+/
。我們將看到 regbranch()
呼叫 regpiece()
兩次。
>foo+< 1 reg
brnc
piec
atom
>o+< 3 piec
atom
>< 6 tail~ EXACT <fo> (1)
7 tsdy~ EXACT <fo> (EXACT) (1)
~ PLUS (END) (3)
~ attach to END (6) offset to 3
最後我們得到程式
1: EXACT <fo>(3)
3: PLUS(6)
4: EXACT <o>(0)
6: END(0)
現在我們有一個特殊情況。EXACT
regop 的 regnext
為 0。這是因為如果它相符,就應該嘗試再次比對自己。PLUS
regop 處理 EXACT
regop 的實際失敗,並採取適當的動作(如果 EXACT
至少相符一次,則轉到 regnode 6,否則失敗)。
現在來看更複雜的內容:/x(?:foo*|b[a][rR])(foo|bar)$/
>x(?:foo*|b... 1 reg
brnc
piec
atom
>(?:foo*|b[... 3 piec
atom
>?:foo*|b[a... reg
>foo*|b[a][... brnc
piec
atom
>o*|b[a][rR... 5 piec
atom
>|b[a][rR])... 8 tail~ EXACT <fo> (3)
>b[a][rR])(... 9 brnc
10 piec
atom
>[a][rR])(f... 12 piec
atom
>a][rR])(fo... clas
>[rR])(foo|... 14 tail~ EXACT <b> (10)
piec
atom
>rR])(foo|b... clas
>)(foo|bar)... 25 tail~ EXACT <a> (12)
tail~ BRANCH (3)
26 tsdy~ BRANCH (END) (9)
~ attach to TAIL (25) offset to 16
tsdy~ EXACT <fo> (EXACT) (4)
~ STAR (END) (6)
~ attach to TAIL (25) offset to 19
tsdy~ EXACT <b> (EXACT) (10)
~ EXACT <a> (EXACT) (12)
~ ANYOF[Rr] (END) (14)
~ attach to TAIL (25) offset to 11
>(foo|bar)$< tail~ EXACT <x> (1)
piec
atom
>foo|bar)$< reg
28 brnc
piec
atom
>|bar)$< 31 tail~ OPEN1 (26)
>bar)$< brnc
32 piec
atom
>)$< 34 tail~ BRANCH (28)
36 tsdy~ BRANCH (END) (31)
~ attach to CLOSE1 (34) offset to 3
tsdy~ EXACT <foo> (EXACT) (29)
~ attach to CLOSE1 (34) offset to 5
tsdy~ EXACT <bar> (EXACT) (32)
~ attach to CLOSE1 (34) offset to 2
>$< tail~ BRANCH (3)
~ BRANCH (9)
~ TAIL (25)
piec
atom
>< 37 tail~ OPEN1 (26)
~ BRANCH (28)
~ BRANCH (31)
~ CLOSE1 (34)
38 tsdy~ EXACT <x> (EXACT) (1)
~ BRANCH (END) (3)
~ BRANCH (END) (9)
~ TAIL (END) (25)
~ OPEN1 (END) (26)
~ BRANCH (END) (28)
~ BRANCH (END) (31)
~ CLOSE1 (END) (34)
~ EOL (END) (36)
~ attach to END (37) offset to 1
產生程式
1: EXACT <x>(3)
3: BRANCH(9)
4: EXACT <fo>(6)
6: STAR(26)
7: EXACT <o>(0)
9: BRANCH(25)
10: EXACT <ba>(14)
12: OPTIMIZED (2 nodes)
14: ANYOF[Rr](26)
25: TAIL(26)
26: OPEN1(28)
28: TRIE-EXACT(34)
[StS:1 Wds:2 Cs:6 Uq:5 #Sts:7 Mn:3 Mx:3 Stcls:bf]
<foo>
<bar>
30: OPTIMIZED (4 nodes)
34: CLOSE1(36)
36: EOL(37)
37: END(0)
在這裡,我們可以看到更複雜的程式,其中包含各種最佳化。在 regnode 10 中,我們看到一個範例,其中只包含一個字元的字元類別轉換成 EXACT
節點。我們也可以看到整個交替轉換成 TRIE-EXACT
節點的地方。因此,有些 regnode 已標記為最佳化移除。我們可以看到 $
符號已轉換成 EOL
regop,這是一個尋找 \n
或字串結尾的特殊程式碼片段。
BRANCH
的下一個指標很有趣,它指向分支失敗時執行應該前往的位置。執行時,如果引擎嘗試從分支橫越到非分支的 regnext
,則引擎將知道整組分支都已失敗。
正規表示式引擎可能是一個重量級的工具。對於長字串和複雜模式,它可能必須執行大量工作才能找到相符項,甚至更多工作才能決定沒有可能的相符項。考慮以下模式的情況。
'ababababababababababab' =~ /(a|b)*z/
(a|b)*
部分可以在字串中的每個字元相符,然後每次都失敗,因為字串中沒有 z
。因此,很明顯,除非字串中有 z
,否則我們可以避免使用正規表示式引擎。同樣地,在像這樣的模式中
/foo(\w+)bar/
在這種情況下,我們知道字串必須包含 foo
,而 foo
之後必須接著 bar
。我們可以使用 fbm_instr()
中實作的 Fast Boyer-Moore 比對來找出這些字串的位置。如果這些字串不存在,我們就不需要求助於昂貴許多的正規表示法引擎。更好的是,如果這些字串確實存在,我們可以使用它們的位置來縮小正規表示法引擎需要涵蓋的搜尋範圍,以判斷整個模式是否相符。
模式有各種面向,可以利用這些面向來促進優化
錨定的固定字串
浮動的固定字串
最小和最大長度需求
開始類別
行首/行尾位置
另一種可以發生的優化形式是後置解析的「窺孔」優化,其中會以更有效率的建構取代效率不佳的建構。用於解析期間標記分支結束和群組結束的 TAIL
regop 就是範例。這些 regop 在建構期間用作佔位符,而且「總是相符」,因此可以透過讓指向 TAIL
的項目指向 TAIL
指向的項目來「最佳化」,進而「略過」節點。
另一種可以發生的優化是「EXACT
合併」,其中會將兩個連續的 EXACT
節點合併成單一 regop。更積極的形式是將 EXACT BRANCH ... EXACT
形式的分支序列轉換成 TRIE-EXACT
regop。
所有這些都發生在 study_chunk()
常式中,它使用特殊結構 scan_data_t
來儲存它執行的分析,並在執行期間進行「窺孔」優化。
study_chunk()
中涉及的程式碼極為難懂。小心。 :-)
執行正規表示式通常包含兩個階段,第一個階段是找出字串中我們應該從哪開始比對的起始點,第二個階段則是執行 regop 解譯器。
如果我們可以判斷沒有有效的起始點,那麼我們根本不會費心執行解譯器。同樣地,如果我們從分析階段知道我們無法偵測到起始位置的捷徑,我們會直接進入解譯器。
這兩個進入點是 re_intuit_start()
和 pregexec()
。這些常式在它們的功能之間有著有點亂倫的關係,而且 pregexec()
甚至可能會自行呼叫 re_intuit_start()
。不過,perl 原始碼的其他部分可能會呼叫任一或兩個常式。
解譯器本身的執行曾經是遞迴的,但感謝 Dave Mitchell 在 5.9.x 開發軌道上的努力,這已經改變了:現在在堆疊上維護一個內部堆疊,而且常式是完全反覆的。這可能會很棘手,因為程式碼對於它儲存的狀態相當保守,導致程式碼中的兩行連續行實際上可能會因為模擬遞迴而執行在完全不同的內容中。
re_intuit_start()
負責處理起始點和不匹配最佳化,這是由 study_chunk()
完成的分析結果所決定的(並在「窺孔最佳化和分析」中說明)。
這個常式的基本結構是嘗試找出樣式可能匹配的起始點和/或終止點,並確保字串夠長以匹配樣式。它嘗試使用較不有效率的方法來使用較有效率的方法,並且可能涉及大量約束的交叉檢查,以找出與字串匹配的位置。例如,它可能會嘗試確定給定的固定字串不僅必須存在,而且必須在字串結束前一定數量的字元,或其他任何情況。
它呼叫其他幾個常式,例如執行快速 Boyer Moore 匹配的 fbm_instr()
,以及負責使用程式中第一個強制 regop 找出起始位置的 find_byclass()
。
當最佳化條件已滿足時,會呼叫 reg_try()
來執行比對。
pregexec()
是執行正規表示式的主要入口點。它包含支援初始化正規表示式直譯器的狀態、在需要時執行 re_intuit_start()
,以及在需要時從各種起始位置對字串執行直譯器。當需要使用正規表示式直譯器時,pregexec()
會呼叫 regtry()
。
regtry()
是正規表示式直譯器的入口點。它預期參數為指向 regmatch_info
結構的指標和指向字串的指標。它會傳回整數 1 表示成功,0 表示失敗。它基本上是 regmatch()
周圍的設定包裝器。
regmatch
是直譯器的主要「遞迴迴圈」。它基本上是一個巨大的 switch 陳述式,實作一個狀態機,其中可能的狀態是 regop 本身,加上許多其他中間和失敗狀態。少數狀態實作為子常式,但大部分是內聯程式碼。
在處理包含無法使用八位元字元集表示的字元的字串時,perl 使用內部表示法,它是 Unicode 的 UTF-8 編碼[2] 的允許版本。這使用單一位元組表示 ASCII 字元集中的字元,並使用兩個或更多位元組的序列表示所有其他字元。(請參閱 perlunitut 以取得有關 UTF-8 和 perl 編碼 utf8 之間關係的更多資訊。此差異對於此討論並不重要。)
無論如何看待,Unicode 支援在正規表示式引擎中都會造成困擾。當您有 256 個可能的字元時可能很好的技巧,通常無法擴充到處理 UTF-8 字元集的大小。您在 ASCII 中可以視為理所當然的事情,在 Unicode 中可能不是真的。例如,在 ASCII 中,可以安全地假設 sizeof(char1) == sizeof(char2)
,但在 UTF-8 中則不然。Unicode 字元大小寫轉換比 ASCII 的簡單規則複雜得多,即使不使用 Unicode 而只使用在地化的單一位元組編碼,事情也可能變得棘手(例如,LATIN SMALL LETTER SHARP S (U+00DF, ß) 應該在在地化不區分大小寫的比對中與「SS」相符)。
更糟糕的是,UTF-8 支援是稍後才新增到正規表示式引擎(如同 Perl)的,這勢必讓事情變得更複雜。顯然,從一開始就設計一個支援 Unicode 的正規表示式引擎比事後才將其改裝到一個原本不支援的引擎來得容易。
幾乎所有涉及查看輸入字串的正則運算都具有兩種情況,一種是針對 UTF-8,另一種則不是。事實上,情況通常比這更複雜,因為模式也可能是 UTF-8。
進行變更時必須小心,以確保在編譯時間和執行時間正確處理 UTF-8,包括字串和模式不匹配的情況。
在 perlreapi 中描述的 regexp
結構對所有正規表示式引擎都是通用的。它的兩個欄位是供編譯模式的正規表示式引擎私下使用。這些欄位是 intflags
和 pprivate 成員。pprivate
是指向任意結構的 void 指標,其使用和管理是編譯引擎的責任。Perl 永遠不會修改這些值中的任何一個。對於庫存引擎,pprivate
指向的結構稱為 regexp_internal
。
它的 pprivate
和 intflags
欄位包含特定於每個引擎的資料。
有兩個結構用於儲存已編譯的正規表示式。其中一個,在 perlreapi 中描述的 regexp
結構是由目前使用的引擎填入,而 Perl 會讀取它的某些欄位來實作例如 qr//
的字串化。
另一個結構是由 regexp
結構的 pprivate
指向,而且除了同一個結構中的 intflags
之外,還被視為編譯正規表示式的正規表示式引擎的屬性;
regexp 結構包含 Perl 需要知道的所有資料,以便正確使用正規表示式。它包含有關最佳化的資料,Perl 可以使用這些資料來判斷是否真的應該使用正規表示式引擎,以及各種其他控制資訊,這些資訊對於在各種情況下正確執行模式是必要的,例如模式是否以某種方式錨定,或是在編譯期間使用了哪些旗標,或程式是否包含 Perl 需要知道的特殊結構。
此外,它包含兩個欄位,供編譯模式的正規運算式引擎私下使用。它們是 intflags
和 pprivate
成員。pprivate
是指向任意結構的 void 指標,其使用和管理是編譯引擎的責任。perl 永遠不會修改這些值。
如前所述,在預設引擎的情況下,pprivate
會是指向 regexp_internal 結構的指標,其中包含已編譯的程式和正規運算式引擎實作的任何其他私有資料。
pprivate
結構下列結構用作 perl 正規運算式引擎的 pprivate
結構。由於它是特定於 perl,因此它僅對其他引擎實作具有好奇的價值。
typedef struct regexp_internal {
regnode *regstclass;
struct reg_data *data;
struct reg_code_blocks *code_blocks;
U32 proglen;
U32 name_list_idx;
regnode program[1];
} regexp_internal;
屬性的說明如下
regstclass
re_intuit_start()
使用的特殊 regop,用於檢查模式是否可以在特定位置進行比對。例如,如果正規運算式引擎知道模式必須以「Z」開頭,則它可以掃描字串,直到找到一個「Z」,然後從那裡啟動正規運算式引擎。處理此項目的常式稱為 find_by_class()
。有時這個欄位會指向程式中嵌入的 regop,有時會指向最佳化器建構的獨立合成 regop。
data
這個欄位會指向 reg_data
結構,其定義如下
struct reg_data {
U32 count;
U8 *what;
void* data[1];
};
此結構用於處理正規運算式引擎在編譯產品上進行複製或釋放作業時需要特別處理的資料結構。資料陣列中的每個元素在 what 陣列中都有對應的元素。在編譯過程中,需要儲存特殊結構的 regop 會使用 add_data() 常式將元素新增到每個陣列,然後將索引儲存在 regop 中。
在現代的 perl 中,此結構的第 0 個元素是保留的,且絕不用來儲存任何有用的東西。這是為了讓需要索引到此陣列的事物能表示「沒有值」。
code_blocks
此選用結構用於管理模式中的 (?{})
建構。它由下列結構組成。
/* record the position of a (?{...}) within a pattern */
struct reg_code_block {
STRLEN start;
STRLEN end;
OP *block;
REGEXP *src_regex;
};
/* array of reg_code_block's plus header info */
struct reg_code_blocks {
int refcnt; /* we may be pointed to from a regex
and from the savestack */
int count; /* how many code blocks */
struct reg_code_block *cb; /* array of reg_code_block's */
};
proglen
儲存已編譯程式的長度,單位為 regop。
name_list_idx
這是資料陣列中的索引,其中儲存一個 AV,它包含模式中任何已命名擷取緩衝區的名稱(如果有)。這只用於正規表示式引擎的偵錯版本,以及當 RXp_PAREN_NAMES(prog) 為 true 時。如果沒有此類資料,它將為 0。
program
已編譯程式。內嵌到結構中,因此整個結構可以視為單一 blob。
Yves Orton,2006 年。
摘錄自 Perl,並包含 Ronald J. Kimball、Dave Mitchell、Dominic Dunlop、Mark Jason Dominus、Stephen McCamant 和 David Landgren 的貢獻和建議。
目前由 Perl 5 Porters 維護。
與 Perl 相同的條款。