Lpeg 教程 |
|
LPeg 是一種用於比對文字資料強大的標記,比 Lua 字串樣式和標準正規表示法更為強大。然而,和任何語言一樣,你需要知道基本字詞以及如何組合它們。
學習的最佳方式是在互動式會話中使用樣式,首先從定義一些捷徑開始
$ lua -llpeg Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio > match = lpeg.match -- match a pattern against a string > P = lpeg.P -- match a string literally > S = lpeg.S -- match anything in a set > R = lpeg.R -- match anything in a range
如果你不想要手動建立捷徑,你可以這樣做
> setmetatable(_ENV or _G, { __index = lpeg or require"lpeg" })
我不建議在嚴肅的程式碼中這樣做,但對於探索 LPeg,這是很方便的。
比對會針對字串的開頭進行,而成功比對會傳回成功比對後的位置,或在不成功的情況下傳回 nil
。(在此,我使用Lua 中 f'x'
等於 f('x')
的事實;使用單引號與使用雙引號有相同的意思。)
> = match(P'a','aaa') 2 > = match(P'a','123') nil
它的作用類似於 string.find
,除了它只傳回一個索引。
你可以針對範圍或集合比對字元
> = match(R'09','123') 2 > = match(S'123','123') 2
比對多於一個項目是透過 ^
運算子來完成的。在這種情況下,比對等於 Lua 樣式 '^a+',表示一個或多個 'a' 出現。
> = match(P'a'^1,'aaa') 4
以順序組合樣式是透過 *
運算子來完成的。這等於 '^ab*',表示一個 'a' 接著零個或多個 'b'。
> = match(P'a'*P'b'^0,'abbc') 4
到目前為止,lpeg 為我們提供了一種更加詳細的方式來表示正規表示法,但這些樣式是可組合的,也就是說,它們可以輕鬆地從更簡單的樣式建構出來,而不用笨拙的字串操作。用這種方式,lpeg 樣式可以比其對應的正規表示法更易於閱讀。請注意,如果你建構樣式時,其中一個參數已經是樣式,則你經常可以略過明確的 P
呼叫
> maybe_a = P'a'^-1 -- one or zero matches of 'a' > match_ab = maybe_a * 'b' > = match(match_ab, 'ab') 3 > = match(match_ab, 'b') 2 > = match(match_ab, 'aaab') nil
+
運算子表示或者一個或另一個樣式
> either_ab = (P'a' + P'b')^1 -- sequence of either 'a' or 'b' > = either_ab:match 'aaa' 4 > = either_ab:match 'bbaa' 5
請注意,樣式物件有一個 match
方法!
當然,S'ab'^1
將是表示這一點的較短方式,但這裡的參數可以是任意的樣式。
取得比對後的位置非常好,然後你可以使用 string.sub
來擷取字串。但有許多方法可以明確要求擷取
> C = lpeg.C -- captures a match > Ct = lpeg.Ct -- a table with all captures from the pattern
第一種方法等同於 Lua 樣式中 '(...)' 的用途(或正規表示式中的 '\(...\)')
> digit = R'09' -- anything from '0' to '9' > digits = digit^1 -- a sequence of at least one digit > cdigits= C(digits) -- capture digits > = cdigits:match '123' 123
所以要取得字串值,請用 C
將樣式圈起來。
這個樣式沒有涵蓋一般的整數,一般整數可能前面有 '+' 或 '-'
> int = S'+-'^-1 * digits > = match(C(int),'+23') +23
與 Lua 樣式或正規表示法不同的是,你不必擔心跳脫『魔術』字元,字串中的每個字元都代表它自己:'(','+','*', 等等,它們只是其 ASCII 對應字元的寫法。
有 /
運算子提供一種特別的擷取,它將擷取到的字串傳遞到一個函式或表格。我加一到結果中,只是為了展示結果已經透過 tonumber
轉換為數字
> = match(int/tonumber,'+123') + 1 124
請注意,正如 string.match
一樣,一個匹配可以返回多個捕獲。這相當於 ‘^(a+)(b+)’
> = match(C(P'a'^1) * C(P'b'^1), 'aabbbb') aa bbbb
考慮一般浮點數
> function maybe(p) return p^-1 end > digits = R'09'^1 > mpm = maybe(S'+-') > dot = '.' > exp = S'eE' > float = mpm * digits * maybe(dot*digits) * maybe(exp*mpm*digits) > = match(C(float),'2.3') 2.3 > = match(C(float),'-2') -2 > = match(C(float),'2e-02') 2e-02
這個 lpeg 模式比等效的正規表達式 ‘[-+]?[0-9]+\.?[0-9]+([eE][+-]?[0-9]+)?’ 更容易閱讀;越短總是越好!一個原因在於我們可以將模式作為 表達式 來處理:提取出公共模式,為便利性和清晰度編寫函數等。請注意,以這種方式寫東西沒有缺點;lpeg 仍然是一種非常快速的文本解析方法!
可以從這些構建塊組合出更多複雜的結構。考慮一個任務,即解析浮點數列表。一個列表是一個數字,後跟零個或多個由逗號和數字組成的組
> listf = C(float) * (',' * C(float))^0 > = listf:match '2,3,4' 2 3 4
這很酷,但如果將其作為一個實際列表,那就更酷了。這就是 lpeg.Ct
發揮作用的地方;它將模式中所有的捕獲收集到一個表中。
= match(Ct(listf),'1,2,3') table: 0x84fe628
庫 Lua 沒有漂亮列印表,但你可以使用 [? Microlight] 來完成此工作
> tostring = require 'ml'.tstring > = match(Ct(listf),'1,2,3') {"1","2","3"}
這些值仍然是字串。最好寫成 listf
,以便它將其捕獲轉換
> floatc = float/tonumber > listf = floatc * (',' * floatc)^0
這種捕獲列表的方法非常通用,因為你可以在 floatc
的位置放置捕獲的 任何 表達式。但這個列表模式仍然過於局限,因為我們通常希望忽略空白
> sp = P' '^0 -- zero or more spaces (like '%s*') > function space(pat) return sp * pat * sp end -- surrond a pattern with optional space > floatc = space(float/tonumber) > listc = floatc * (',' * floatc)^0 > = match(Ct(listc),' 1,2, 3') {1,2,3}
這是一個品味問題,但我更喜歡在 項目 周圍允許選擇性的空間,而不是在 分隔符 ‘,’ 的周圍允許特定的空間。
借助 lpeg,我們可以使用模式匹配再次成為程式設計師,並重複使用模式
function list(pat) pat = space(pat) return pat * (',' * pat)^0 end
因此,一個識別符列表(根據通常的規則)
> idenchar = R('AZ','az')+P'_' > iden = idenchar * (idenchar+R'09')^0 > = list(C(iden)):match 'hello, dolly, _x, s23' "hello" "dolly" "_x" "s23"
使用顯式範圍似乎有點過時而且容易出錯。更便攜的解決方案是使用 lpeg 等效的 字元類別,它根據定義與區域設定無關
> l = {} > lpeg.locale(l) > for k in pairs(l) do print(k) end "punct" "alpha" "alnum" "digit" "graph" "xdigit" "upper" "space" "print" "cntrl" "lower" > iden = (l.alpha+P'_') * (l.alnum+P'_')^0
給定 list
的這個定義,很容易定義公共 CSV 格式的一個簡單子集,其中每個記錄都是一個由換行分隔的列表
> listf = list(float/tonumber) > csv = Ct( (Ct(listf)+'\n')^1 ) > = csv:match '1,2.3,3\n10,20, 30\n' {{1,2.3,3},{10,20,30}}
學習 lpeg 的一個很好的原因是它執行得非常好。這個模式遠遠快於使用 Lua 字串匹配來解析資料。
我將展示 lpeg 可以完成 string.gsub
所能完成的所有工作,而且更通用、更靈活。
我們還沒有使用過的一個運算符是 -
,它表示 ‘或’。考慮匹配雙引號字串的問題。在最簡單的情況下,它們是一個雙引號,後跟任何不是雙引號的字元,後再跟一個閉合雙引號。P(1)
匹配任何 單個字元,即它等於字串模式中的 ‘.’。一個字串可能為空,因此我們匹配零個或多個非引號字元
> Q = P'"' > str = Q * (P(1) - Q)^0 * Q > = C(str):match '"hello"' "\"hello\""
或者你可能想提取字串的內容,而沒有引號。在此文中,只需使用 1
代替 P(1)
並不是模棱兩可的,事實上,這正是你通常看到的這個 ‘任何不是 P 的 x’ 模式的寫法
> str2 = Q * C((1 - Q)^0) * Q > = str2:match '"hello"' "hello"
此樣本明顯地具有可概化性,通常終止樣本與最後樣本不會相同
function extract_quote(openp,endp) openp = P(openp) endp = endp and P(endp) or openp local upto_endp = (1 - endp)^1 return openp * C(upto_endp) * endp end
> return extract_quote('(',')'):match '(and more)' "and more" > = extract_quote('[[',']]'):match '[[long string]]' "long string"
現在考慮將 Markdown code
(反斜線封閉的文字)轉譯成 Lua wiki 了解的格式(雙大括弧封閉的文字)。幼稚的方式是摘取字串並串接結果,但這是笨拙的,而且(正如我們將會看到的)極大地限制了我們的選項。
function subst(openp,repl,endp) openp = P(openp) endp = endp and P(endp) or openp local upto_endp = (1 - endp)^1 return openp * C(upto_endp)/repl * endp end
> = subst('`','{{%1}}'):match '`code`' "{{code}}" > = subst('_',"''%1''"):match '_italics_' "''italics''"
我們在之前碰過擷取處理運算子 /
,使用 tonumber
轉換數字。它也能以與 string.gsub
非常相似的格式了解字串,在此 %n
表示第 n 個擷取。
此運算可精確地表示為
> = string.gsub('_italics_','^_([^_]+)_',"''%1''") "''italics''"
但是其優點是我們不必建立自訂字串樣本,而且不必擔心轉義「魔術」字元如 '(' 和 ')。
lpeg.Cs
是置換擷取,而且提供了全球字串置換的更通用的模組。在 lpeg 手冊中,這等於 string.gsub
function gsub (s, patt, repl) patt = P(patt) local p = Cs ((patt / repl + 1)^0) return p:match(s) end > = gsub('hello dog, dog!','dog','cat') "hello cat, cat!"
為了了解差異,以下是以純粹 C
使用的樣本
> p = C((P'dog'/'cat' + 1)^0) > = p:match 'hello dog, dog!' "hello dog, dog!" "cat" "cat"
此處的 C
僅擷取整個匹配,而且每個 '/' 都新增一項包含置換字串數值的擷取。
使用 Cs
,所有內容都將被擷取,而且所有擷取都建構為一個字串。其中一些擷取會透過 '/' 修改,因此我們會進行置換。
在 Markdown 中,區塊引號行以 '> ' 開頭。
lf = P'\n' rest_of_line_nl = C((1 - lf)^0*lf) -- capture chars upto \n quoted_line = '> '*rest_of_line_nl -- block quote lines start with '> ' -- collect the quoted lines and put inside [[[..]]] quote = Cs (quoted_line^1)/"[[[\n%1]]]\n" > = quote:match '> hello\n> dolly\n' "[[[ > hello > dolly ]]] "
這不太正確 - Cs
擷取所有內容,包括 '> '。但是我們可以強制一些擷取傳回空字串:}}}
function empty(p) return C(p)/'' end quoted_line = empty ('> ') * rest_of_line_nl ...
現在事情將會正確運作!
以下是用來將此文件從 Markdown 轉換為 Lua wiki 格式的程式
local lpeg = require 'lpeg' local P,S,C,Cs,Cg = lpeg.P,lpeg.S,lpeg.C,lpeg.Cs,lpeg.Cg local test = [[ ## A title here _we go_ and `a:bonzo()`: one line two line three line and `more_or_less_something` [A reference](http://bonzo.dog) > quoted > lines ]] function subst(openp,repl,endp) openp = P(openp) -- make sure it's a pattern endp = endp and P(endp) or openp -- pattern is 'bracket followed by any number of non-bracket followed by bracket' local contents = C((1 - endp)^1) local patt = openp * contents * endp if repl then patt = patt/repl end return patt end function empty(p) return C(p)/'' end lf = P'\n' rest_of_line = C((1 - lf)^1) rest_of_line_nl = C((1 - lf)^0*lf) -- indented code block indent = P'\t' + P' ' indented = empty(indent)*rest_of_line_nl -- which we'll assume are Lua code block = Cs(indented^1)/' [[[!Lua\n%1]]]\n' -- use > to get simple quoted block quoted_line = empty('> ')*rest_of_line_nl quote = Cs (quoted_line^1)/"[[[\n%1]]]\n" code = subst('`','{{%1}}') italic = subst('_',"''%1''") bold = subst('**',"'''%1'''") rest_of_line = C((1 - lf)^1) title1 = P'##' * rest_of_line/'=== %1 ===' title2 = P'###' * rest_of_line/'== %1 ==' url = (subst('[',nil,']')*subst('(',nil,')'))/'[%2 %1]' item = block + title1 + title2 + code + italic + bold + quote + url + 1 text = Cs(item^1) if arg[1] then local f = io.open(arg[1]) test = f:read '*a' f:close() end print(text:match(test))
由於這個 Wiki 有轉義問題,我必須在此來源中用 '[' 取代 '{' 等。請小心!
SteveDonovan,2012 年 6 月 12 日
本節將剖析群組和反向擷取(分別為 Cg()
和 Cb()
)的行為。
群組擷取(以下稱為「群組」)有兩種形式:命名和匿名。
Cg(C"baz" * C"qux", "name") -- named group. Cg(C"foo" * C"bar") -- anonymous group.
讓我們先解決簡單的一個:表格擷取中的命名群組。
Ct(Cc"foo" * Cg(Cc"bar" * Cc"baz", "TAG")* Cc"qux"):match"" --> { "foo", "qux", TAG = "bar" }在表格擷取中,群組中第一個擷取的數值(「bar」)會指定到表格中的對應鍵(「TAG」)。正如您所見,
Cc"baz"
在這個過程中遺失了。標籤必須是字串(或將自動轉換為字串的數字)。請注意,群組必須是表格的直接子元素,否則表格擷取將無法處理群組。
Ct(C(Cg(1,"foo"))):match"a" --> {"a"}
在深入探討群組之前,我們必須先了解擷取處理其子擷取的方式中的一個微妙之處。
一些擷取會針對其子擷取產生的數值執行操作,而其他擷取則會針對擷取物件執行操作。這有時會產生相反的直覺。
讓我們以下列樣本為例
(1 * C( C"b" * C"c" ) * 1):match"abcd" --> "bc", "b", "c"正如您所見,它在擷取串流中插入三個數值。
讓我們將其包裝在表格擷取中
Ct(1 * C( C"b" * C"c" ) * 1):match"abcd" --> { "bc", "b", "c" }
Ct()
執行值。在最後的範例中,有三項值依序插入表格中。現在,讓我們嘗試替換擷取
Cs(1 * C( C"b" * C"c" ) * 1):match"abcd" --> "abcd"
Cs()
執行擷取。它掃描嵌套擷取的第一層級,而且只取每一項的第一個值。在以上範例中,"b"
和 "c"
因此被捨棄。以下是另一個可能讓事情更清楚的範例function the_func (bcd) assert(bcd == "bcd") return "B", "C", "D" end Ct(1 * ( C"bcd" / the_func ) * 1):match"abcde" --> {"B", "C", "D"} -- All values are inserted. Cs(1 * ( C"bcd" / the_func ) * 1):match"abcde" --> "aBe" -- the "C" and "D" have been discarded.針對每一種擷取會以值 / 以擷取為基礎行為的更詳細說明將會是另一個區段的主題。
需要意識到的另一項重要事項是,大多數擷取會隱藏其次級擷取,但有些則不會。正如您在最後一個範例中所看到的,C"bcd"
的值被傳遞給 /function
擷取,但它並未出現在最終擷取清單中。在這方面,Ct()
和 Cs()
也是不透明的。它們只產生一個表格或一個字串,分別。
另一方面,C()
是透明的,正如我們在上面所看到的,C()
的次級擷取也會插入串流中。
C(C"b" * C"c"):match"bc" --> "bc", "b", "c"唯一的透明擷取是
C()
和匿名 Cg()
。
Cg()
將其次級擷取包裝在單一擷取物件中,但它本身並未產生任何內容。依據不同的情況,所有值都將會插入,或只有第一個值會插入。
以下是匿名群組的幾個範例
(1 * Cg(C"b" * C"c" * C"d") * 1):match"abcde" --> "b", "c", "d" Ct(1 * Cg(C"b" * C"c" * C"d") * 1):match"abcde" --> { "b", "c", "d" } Cs(1 * Cg(C"b" * C"c" * C"d") * 1):match"abcde" --> "abe" -- "c" and "d" are dropped.
這種行為在什麼時候有用?在摺疊擷取時。
讓我們寫一個非常基本的計算機,它可以加或減一數字的數字。
function calc(a, op, b) a, b = tonumber(a), tonumber(b) if op == "+" then return a + b else return a - b end end digit = R"09" calculate = Cf( C(digit) * Cg( C(S"+-") * C(digit) )^0 , calc ) calculate:match"1+2-3+4" --> 4擷取樹狀結構會看起來像這樣 [*]
{"Cf", func = calc, children = { {"C", val = "1"}, {"Cg", children = { {"C", val = "+"}, {"C", val = "2"} } }, {"Cg", children = { {"C", val = "-"}, {"C", val = "3"} } }, {"Cg", children = { {"C", val = "+"}, {"C", val = "4"} } } } }你可能會看出重點為何…就像
Cs()
,Cf()
會執行擷取物件。它會先萃取出第一個擷取的第一個值,並將其用作初始值。如果沒有更多擷取,這個值就會成為 Cf()
的值。但我們還有更多擷取。在我們的例子中,它將會將第二個擷取(群組)的所有值傳遞給 calc()
,緊接在第一個值之後。以下是上述 Cf()
的評估
first_arg = "1" next_ones: "+", "2" first_arg = calc("1", "+", "2") -- 3, calc() returns numbers next_ones: "-", "3" first_arg = calc(3, "-", "3") next_ones: "+", "4" first_arg = calc(0, "+", "4") return first_arg -- Tadaaaa.
[*] 實際上,在配對時間,擷取物件只會儲存其界線和輔助資料(例如 Cf()
的 calc()
)。實際的值會在配對完成後依序產生,但,正如上面所顯示的,它讓事情變得更清楚。在以上的範例中,嵌套 C()
和 Cg(C(),C())
的值實際上會一次產生一個,在摺疊處理的每個對應循環中。
(命名為 Cg()
/ Cb()
)組的行為類似匿名的 Cg()
,但捕捉在命名 Cg()
中的值不會在本地插入。它們會被傳送,並最終插入到 Cb()
所在位置的串流中。
以下是一個例子
( 1 * Cg(C"bc", "FOOO") * C"d" * 1 * Cb"FOOO" * Cb"FOOO"):match"abcde" -- > "d", "bc", "bc"如果有多個
Cb()
,會產生變形... 和複製。另一個例子( 1 * Cg(C"b" * C"c" * C"d", "FOOO") * C"e" * Ct(Cb"FOOO") ):match"abcde" --> "e", { "b", "c", "d" }
平常為了清楚起見,我在程式碼中將 Cg()
的別名設為 Tag()
。我將前者用於匿名群組,後者用於命名群組。
Cb"FOOO"
將回看前面是否有一個成功的對應 Cg()
。它會在樹狀結構中返回並向上尋找,並消耗捕捉。換句話說,它會搜尋自己的長輩手足,以及長輩手足的長輩手足,但不會搜尋自己的父母。它也不會測試手足/祖先手足的小孩。
它會以下列方式進行(從 [ #### ] <--- [[ START ]]
開始,然後跟著數字往上追蹤)
[ numbered ]
捕捉會按順序測試的捕捉。標記為 [ ** ]
的那一類則不會,原因詳列於各種原因清單中。這很棘手,但 AFAICT 是完整的。
Cg(-- [ ** ] ... This one would have been seen, -- if the search hadn't stopped at *the one*. "Too late, mate." , "~@~" ) * Cg( -- [ 3 ] The search ends here. <--------------[[ Stop ]] "This is *the one*!" , "~@~" ) * Cg(-- [ ** ] ... The great grand parent. -- Cg with the right tag, but direct ancestor, -- thus not checked. Cg( -- [ 2 ] ... Cg, but not the right tag. Skipped. Cg( -- [ ** ] good tag but masked by the parent (whatever its type) "Masked" , "~@~" ) , "BADTAG" ) * C( -- [ ** ] ... grand parent. Not even checked. ( Cg( -- [ ** ] ... This subpattern will fail after Cg() succeeds. -- The group is thus removed from the capture tree, and will -- not be found dureing the lookup. "FAIL" , "~@~" ) * false ) + Cmt( -- [ ** ] ... Direct parent. Not assessed. C(1) -- [ 1 ] ... Not a Cg. Skip. * Cb"~@~" -- [ #### ] <----------------- [[ START HERE ]] -- , function(subject, index, cap1, cap2) return assert(cap2 == "This is *the one*!") end ) ) , "~@~" -- [ ** ] This label goes with the great grand parent. )