Lpeg 教程

lua-users home
wiki

簡單比對

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.
    )
    

-- PierreYvesGerardy
最新變更 · 喜好設定
編輯 · 歷史
最後編輯於全球標準時間 2019 年 2 月 18 日晚上 7:39 (diff)