大千世界,茫茫人海,我老是能夠一眼便認出你。這個過程裏包含着一個叫作解析的過程。計算機程序也可以經過這樣的過程,在一堆文本中認出一些特定形式的文本。在短暫又漫長的計算機語言編譯原理的發展過程當中,誕生了不少種形式化文本解析方法,PEG 是其中一種。node
注:在寫這篇文檔的時候,我沒學習過編譯原理,僅對正則表達式略知一二。如果有的地方錯得離譜,要麼讓我錯下去,要麼就幫我改正。
PEG,全稱是 Parsing Expression Grammar,可譯做「解析表達式語法」。PEG 所肩負的歷史重任是取代正則表達式(RE)以及對正則表達式的一些特定擴展。LPEG 是 PEG 的 Lua 實現,這意味着 PEG 像是一種語言解析算法,亦即經過 PEG,可解析某種形式化語言。ConTeXt MkIV 即是經過 LPEG 庫解析待高亮處理的程序代碼 [1]。正則表達式
人一般沒法在短期內識別本身從未見過的事物,可是卻可以想象本身從未見過的事物。這也許是由於,凡是見過的事物,就會在大腦裏造成相應的模式。這些模式通過分割重組,即可以構造本身從未見過的事物的想象,而對於新的事物,大腦裏既有的任何一種模式,一時之間皆沒法與之匹配,於是便沒法將其識別出來。固然,這並不是意味着新的事物永遠不能被咱們識別。一般狀況下,既有的模式能夠再度分割重組,再通過一些時間以後,有可能會創建與這種新的事物匹配的模式。算法
LPEG 只能替咱們完成模式與文本的匹配工做,由於它即沒有製造模式的能力,也沒有獲取輸入的能力。若讓 LPEG 識別某種語言,咱們須要將本身對這種語言的認知轉化爲模式,而後以 LPEG 可以接受的方式傳遞給它,以後再將待解析的文本傳遞給它。不過,幸虧 LPEG 不具有這些能力。編程
最簡單的模式莫過於「原樣」。我最心愛的一把刀,若它的刃部崩出來一個豁口,我就會以爲這再不是原來的刀了,並對此很介意。這是由於我爲這把刀所創建的模式與現實中崩口的刀再也不匹配。大腦裏的模式與實際的事物匹配不起來,結果只有一種,即失望。模式越具體,越容易失望。一把崩口的刀,豁口在刀的總體所佔的比例即便不過 1%,但我會由於這 1% 的失望而忽略剩下那 99% 的無缺。segmentfault
對現實更寬容的人,在遭遇失望時,會對大腦中的已有模式進行調整。若我足夠寬容,會認爲,好在個人這把刀還有 99% 的地方是好的。因而,我在大腦中更新了這把刀的模式,這個模式會持續到它下一次受到損壞之時。編程語言
世上最寬容的人也許是柏拉圖。他認爲存在一個理型世界,這個世界裏的一切都是由很是具體(完美)的模式構成。或者說,這個理型世界裏具備咱們現實世界裏一切事物的模具。模具老是要比它鑄造出來的事物更完美。不過,柏拉圖只能在大腦裏構造這種世界,實際上他構造的只是一種又一種模式罷了。也許是他對現實過於失望,因此便又構造了一個大但願——咱們生自理型世界,一輩子的追求只是爲了迴歸這個世界。函數
LPEG 不具有創建模式的功能,所以,它在遭遇了一把刀受到損傷之時,只會單純地失望:學習
$ lua Lua 5.3.3 Copyright (C) 1994-2016 Lua.org, PUC-Rio > lpeg = require("lpeg") > knife = lpeg.P("knife") > knife:match("knif e") nil
注 1:這是 Lua 解釋器在加載了 LPEG 庫以後開啓的交互模式。注 2:
knife:match("knif e")
意思是用knife
這個模式去與"knif e"
這個字串進行匹配。LPEG 爲每一個模式提供了match
方法,專事於匹配。ui
我是個寬容的人,我但願傳遞給 LPEG 的模式也可以寬容一些。所以,我對 knife
模式做了如下調整:lua
> P = lpeg.P > s = P(" ")^0 > knife = P("k") * s * P("n") * s * P("i") * s * P("f") * s * P("e") > x = "k n i f e" > knife:match(x) 32
只要匹配結果不是 nil
就說明模式與事物(在程序裏就是字串或文本)匹配成功。上述匹配結果 32
,意思是字串 x
的前 31 個字符匹配成功。因爲 x
的長度是 31,所以這個結果意味着 x
與模式 knife
徹底匹配,不管它身上有多麼大的「崩口」。
上述我創建的 knife
模式,它的含義是,在 k、n、i、f、e 這些字母之間能夠存在 0 個或多個空格。顯然,x
符合這種模式,所以我便不會失望。糊塗很可貴到,由於須要將本身大腦裏的各類模式調整到可以包容各類事物的程度。改變不了世界,就去改變本身,這須要耗費極大心力。不少人不是真的糊塗,而是裝糊塗。
LPEG 庫提供的 P
函數用於建立簡單或基本模式,也就是原樣模式。若想讓模式具有足夠的包容性,須要對原樣模式進行分割與組合。在 LPEG 中,模式之間具有加法、乘法、減法以及冪運算。
在上述對 knife
模式的調整中,我用了冪運算符 ^
和乘法運算符 *
。P(" ")
建立的是一個空格模式,對它取 0 次冪,即 s = P(" ")^0
,意思是這個空格模式會連續出現 0 次或更屢次。
當我將 s
放到 P("k")
和 P("n")
之間,再用乘法運算符 *
將它們鏈接起來,即 P("k") * s * P("n")
,這意味着我構造了一個「字母 k 與 n 之間可能存在空格」的模式,亦即這個模式可以匹配
kn k n k n k n ... ... ...
這種形式的字串。顯然,它比 P("k") * P("n")
更寬容。
乘法運算能夠將一些模式組織成一個系統。例如「P("k") * P("n") * P("i") * P("f") * P("e")
」 與「P("knife")
」等價。加法運算則並不是如此。例如「P("k") + P("n") + P("i") + P("f") + P("e")
」,它的意思是字母集合 {k, n, i, f, e}
中的一個字母,因此它只能匹配一個字母,而不是一個字串。
注:「P("k") + P("n") + P("i") + P("f") + P("e")
」與lpeg.S("knife")
等價。S
是「集合(Set)」的縮寫。
當我說乘法的本質是用一些元素構建一個系統,意思是說這些元素與這個系統中的其餘全部元素存在聯繫。加法卻構建不起來這樣的系統。若是說乘法運算能夠將一些零件組裝成一部機器,那麼加法只能算是把這些零件簡單地堆了起來。
減法運算的意思是某事物不能出現,或者除某事物以外。例如,P(1)
的意思是「任意一個字母」,那麼 P(1) - P("n"))
意思就是「除了字母 n 以外的全部字母中的任意一個」。
注:在有運算符的狀況下,P(1)
可簡寫爲1
。所以,P(1) - P("n"))
可寫爲1 - P("n")
注:
P(n)
表示任意 n 個字母。P(-1) 相似於正則表達式裏的$
,表示字串的結尾。
知道了上述知識,就能夠利用它們去寫一些複雜的模式了。
假設有一個字串 "有事能夠給我發郵件,個人郵箱是 lyr.m2@live.cn"
。我能夠寫出一個可以匹配這個字串的模式:
> x = "有事能夠給我發郵件,個人郵箱是 lyr.m2@live.cn" > pat = P(1)^0 > pat:match(x) 61
實際上 P(1)^0
能夠匹配任何字串,由於它的意思是「任意一個字母出現 0 次或屢次」,這是最爲寬容的模式。
如今,若要限定 pat
只匹配到 x
中的電子郵箱的的首字母位置,可做如下修改:
> R = lpeg.R > S = lpeg.S > user = (R("az") + S("._") + R("09"))^0 > server = (R("az") + S(".-_") + R("09"))^0 > mail = user * P("@") * server > pat = (1 - mail)^0 > pat:match(x) 47
這些代碼的玄機有二。首先是郵箱地址模式的構造:
> user = (R("az") + S(".-_") + R("09") - P("@"))^1 > server = user > mail = user * P("@") * server
這裏我使用了 LPEG 的 R
和 S
函數。S
函數的用處在上文中已述。R
函數的用法與 S
類似,也是表達一個集合,可是 R
表示的是字母或數字範圍。例如 R("az")
表示由 a 到 z 的的全部小寫字母構成的集合,而 R("09")
則表示從數字 0 到 9 的全部數字構成的集合。利用模式的加法、乘法和冪運算,就能夠構造一個可以匹配相似 lyr.m2@live.cn
這樣的郵箱地址的模式。
因爲前面提出的限定是,pat
只匹配到 x
中的電子郵箱的的首字母位置。將這個限定引入到原先的 P(1)^0
,結果就是:
pat = (1 - mail)^0
熟悉正則表達式的人,在這裏必定要注意了,mail
模式所能匹配的事物,在 (1 - mail)
裏變成了一個「字符」同樣的存在,這就是上述代碼中的第二個玄機。當我發現這樣居然能夠工做的時候,若不認可 PEG 比 RE 更強大且更好用,那麼我只好懷疑,我在用錯了 LPEG 的前提下,獲得了正確的結果。
用模式去匹配字串,最直接的用途是斷定一個字串是否與既定模式相符。更進一步的用途是從字串中捕獲符合既定模式的子集,這一用途有些相似於照片編輯軟件提供的「摳圖」功能,後者本質上也能夠認爲是從一幅圖片中捕獲符合某種模式的局部區域。
對於字串 x
> x = "有事能夠給我發郵件,個人郵箱是 lyr.m2@live.cn"
如何從中捕獲郵箱地址?
LPEG 提供了 C
函數,能夠將模式變爲捕獲器。例如
> C = lpeg.C > pat = (1 - mail)^0 * C(mail) * (1 - mail)^0 > pat:match(x) lyr.m2@live.cn
如今,將 x
更改一下,
> x = "有事能夠給我發郵件,個人郵箱是 lyr.m2@live.cn,也能夠發給川普 trump@gmail.com。"
再使用 pat
去匹配 x
,
> pat:match(x) lyr.m2@live.cn
結果裏沒有出現第二個郵箱地址。這是由於 pat
模式在完成一次匹配以後,若匹配成功,它的匹配過程也就終止了,所以只能捕獲到字串中第一個郵件地址,我在 x
中新增長的那個郵件地址沒有機會被捕獲。
爲了捕獲字串中全部的郵件地址,須要將 pat
修改成
> pat = ((1 - mail)^0 * C(mail))^0
再度進行匹配和捕獲,
> pat:match(x) lyr.m2@live.cn trump@gmail.com
此次獲得的結果符合預期。
含有捕獲的模式,與字串匹配的結果即是捕獲結果。若模式中含有多個捕獲,若想獲得這些結果,須要用相應數量的變量去容納模式匹配的返回值。例如:
> mail_1, mail_2 = pat:match(x) > print(mail_1) lyr.m2@live.cn > print(mail_2) trump@gmail.com
這樣作有些繁瑣。爲此,LPEG 提供了 Ct
函數,可將模式匹配的多個結果歸入一個表中。例如:
> Ct = lpeg.Ct > pat = Ct(((1 - mail)^0 * C(mail))^0) > result = pat:match(x) > for i, v in pairs(result) do print(v) end lyr.m2@live.cn trump@gmail.com
一段複雜的文本,若它能夠被程序解析,那麼必定存在某種語法可以與之匹配。這種語法一定是由一些簡單的模式複合而成。
下面是一個簡單的整數運算表達式:
> x = "20 * (5 + 6) - 30 / 2"
爲了解析這個表達式,須要定義一些簡單的模式:
> space = P(" ")^0 > integer = C(R("09")^1) > add_or_sub = space * C(S("+-")) * space > mul_or_div = space * C(S("*/")) * space > lpar = space * C(P("(")) * space > rpar = space * C(P(")")) * space
結合上文所涉及的 LPEG 的模式構造方法,上述模式的含義應當不難理解。如今,我要基於它們來構造一種能夠解析整數運算表達式的語法。
對於 x
這樣的整數運算表達式,它們不過是由一些帶括號的項和整數經過 +
、-
、*
、/
運算符鏈接起來的形式而已,下面這個模式
> V = lpeg.V > e = V("t") + V("f") + integer
足以與之匹配,其中 V("t")
表示和式, V("f")
表示因式,integer
爲整數模式。整數四則運算表達式除了這些模式以外,不可能再有其餘形式。
這裏使用了 LPEG 的 V 函數。LPEG 的文檔 [2] 裏稱 V 函數能夠爲語法構造一個非終結符(變量)。因爲我沒學過編譯原理,一開始看不懂這個說法。糾結了兩天,發現這不過是至關於編程語言裏只聲明變量但不爲之賦值的作法。V("f")
對於 LPEG 而言,表示一個模式,只不過它還沒有被定義,V("t")
與之同理。
任何一個和式,一定是從一個因式或一個帶括號的項或一個整數開始,加上或減去模式 e
可以匹配的表達式。所以,可將模式 t
定義爲
> t = (V("f") + V("e_in_par") + integer) * add_or_sub * V("e")
其中,V("e_in_par")
表示帶括號的項。
模式 t
的定義大有玄機。由於在模式 e
的定義中使用了未定義的模式 t
,而在 模式 t
的定義中又將 e
視爲未定義的模式。此時,若將 t
的定義代入到 e
的定義中,結果爲
> e = (V("f") + V("e_in_par") + integer) * add_or_sub * V("e") + V("f") + integer
能夠看到,V("e")
出如今自身所聲明的模式 e
的定義中了。這意味着自指,或自引用。凡出現自指,必造成遞歸,因此上述定義的模式 e
本質上是一個遞歸模式。支持遞歸模式,PEG 的強大之處,正在於此。
同理,可將模式 f
定義爲
> f = (V("e_in_par") + integer) * mul_or_div * (V("f") + V("t_in_par") + integer),
須要注意的是,f
的 mul_or_div
右側的部分再也不是 V(e)
了。這是由於,對於一個因式而言,它老是由因式、帶括號的項或整數構成。若是將mul_or_div
右側的部分寫爲 V(e)
,這意味着 f
模式會將 2 * 3 + 1
也視爲因式,顯然這是錯誤的。
至於 e_in_par
,就是模式 e
所可以匹配的四則運算表達式的外圍裹上一層括號:
> e_in_par = lpar * V("e") * rpar
至此,全部未定義的模式皆已定義完畢。亦即,現已具有一個可以匹配全部的整數四則運算語句的模式。
可是,若讓帶有 V("模式名")
的模式生效,必須將它們放到一個表中,而後交由 P
函數構造出一個總的模式:
> P{ "e", e = V("t") + V("f") + integer, t = (V("f") + V("e_in_par") + integer) * add_or_sub * V("e"), f = (V("e_in_par") + integer) * mul_or_div * (V("f") + V("e_in_par") + inteter), e_in_par = lpar * V("e") * rpar }
LPEG 將這種結構稱爲語法(Grammar)。
下面,採用這種語法對整數四則運算表達式進行匹配:
> calculator = P{ "e", e = V("t") + V("f") + integer, t = (V("f") + V("e_in_par") + integer) * add_or_sub * V("e"), f = (V("e_in_par") + integer) * mul_or_div * (V("f") + V("e_in_par") + integer), e_in_par = lpar * V("e") * rpar } > calculator:match(x) 20 * ( 5 + 6 ) - 30 / 2
結果正確。
這個四則運算的語法,還有一種更簡單的寫法:
> caculator = P{ "e", e = node(V("f") * (add_or_sub * (V("f") + integer))^0), f = node(V("t") * (mul_or_div * (V("t") + integer))^0), t = lpar * V("e") * rpar + integer, }
上一節編寫的語法,即 calculator
,我天資愚鈍,是在不斷失敗中寫出來的。最後我發現,這是一種從上而下,從左而右生長的樹形結構。這是由於在模式 t
、f
以及 e_in_par
中皆包含了模式 e
,這意味着在 e
的定義中出現了三種形式的自指。自指必致使遞歸,一種事物內部的多種自指所引發的遞歸,一定是樹形結構。所以,不妨將 calculator
視爲一種具備自增加能力的模式樹。
這種模式樹的匹配結果一定也是樹形結構。例如,calculator
對
> x = "20 * (5 + 6) - 30 / 2"
的匹配結果,表面上看起來與 x
的形式相同,但實際上,它的結構是樹狀的,以下圖所示:
這樣的解析結果稱爲語法樹(Abstract Syntax Tree,AST)。
獲得語法樹有什麼用呢?寫解釋器 [3]。