zoukankan      html  css  js  c++  java
  • [翻译]lpeg入门教程

    原文地址:http://lua-users.org/wiki/LpegTutorial 

    简单匹配

    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。(这里我应用了一个简写:f'x' 在Lua里等价于 f('x') ; 单引号和双引号等价)

    > = match(P'a','aaa')
    2
    > = match(P'a','123')
    nil
    

    看起来就像 string.find一样,除了只返回一个索引。

    你可以选择按 范围 匹配或者匹配一个集合内的字符:

    > = match(R'09','123')
    2
    > =  match(S'123','123')
    2
    

    要匹配超过1个子串,可以用 ^ 操作符。 在这种情况下,匹配与Lua的 '^a+' 等价- 匹配一个或多个'a':

    > = match(P'a'^1,'aaa')
    4
    

    要按顺序合并模式(pattern),可以通过 * 操作符。 这等价于 '^ab*' - 'a' 后跟着0个或多个 'b':

    > = match(P'a'*P'b'^0,'abbc')
    4
    

    到目前为止,lpeg只是为我们提供了一个更为灵活的方式去表达正则表达式,但是这些模式是可以 组合 - 他们可以由简单的原语组成,避免艰深的字符串操作。因此,lpeg匹配可以比等价的正则表达式表现得更为易读。注意,你可以在构造pattern的时候保留显式的 P 调用。尤其是其中一个参数已经是一个pattern的时候:

    > 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
    

    + 操作符表示 任意 一个pattern:

    > either_ab = (P'a' + P'b')^1 -- sequence of either 'a' or 'b'
    > = either_ab:match 'aaa'
    4
    > =  either_ab:match 'bbaa'
    5
    

    pattern对象还带有 match 方法!

    当然, S'ab'^1 会更简短, 但这里的参数可以是任意的模式。

    基本捕获

    获得match后的位置非常有用, 你可以随后用 string.sub 来取出匹配串。 不过,我们也有显式的方法直接获取子串捕获:

    > C = lpeg.C  -- captures a match
    > Ct = lpeg.Ct -- a table with all captures from the pattern
    

    第一个就好似 '(...)' 在Lua pattern里一样,(或者正则表达式里的 '(...)' )

    > 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码表达里的同类。

    有一类特殊的捕获,需要用到 / 操作符 - 他会将所有匹配的子串丢给一个函数或者table。 这里我会向结果加1,用来演示结果已被tonumber转换成number:

    > =  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依然可以高效的分析文本!

    这些基本构件可以组成更为复杂的结构。例如分析一个列表的浮点数。一个列表是指一个number接着0个或多个逗号和number:

    > listf = C(float) * (',' * C(float))^0
    > = listf:match '2,3,4'
    2	3	4
    

    这样很酷,其实还可以更酷一点,直接将其转换为真正的列表。这就是lpeg.Ct 长处所在; 它可以将所有的捕获归集到一个table里去。

    = match(Ct(listf),'1,2,3')
    table: 0x84fe628
    

    默认的Lua不能直接打印table,但你可以使用 [? Microlight] 来完成这个工作:

    > tostring = require 'ml'.tstring
    > = match(Ct(listf),'1,2,3')
    {"1","2","3"}
    

    这些值依然是字符串。最佳实践是定义一个listf,让它转换成number:

    > 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"
    

    使用显式的range好像很老土,并且充满错误。一个更可移植的方案,是使用lpeg里的 字符类, 字符类是独立于locale设置的:

    > 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
    

    给出列表的定义, 就可以定义一个常用CSV格式的子类, 其中每条纪录都是一个列表,列表之间用换行符分隔:

    > rlistf =  list(float/tonumber)
    > csv = Ct( (Ct(listf)+'
    ')^1 )
    > =  csv:match '1,2.3,3
    10,20, 30
    '
    {{1,2.3,3},{10,20,30}}
    

    学习lpeg的其中一个理由,是lpeg的表现十分令人满意。这种模式匹配 lot 比使用Lua的原始字符串匹配要快得多。

    字符串替换

    接下来,我会演示给大家看,所有string.gsub可以做的lpeg都可以做,并且更具有一般性,更加灵活。

    目前有一个操作符-是尚未展示过的, 它可以表示'任一/或'。 考虑双引号字符串的匹配。在最简单的情况下,一般是一个双引号接着其他不是双引号的字符,最后接上一个封闭的双引号。 P(1) 匹配 任一 单个字符, 就好比字符串模式里的 '.' 。 一个字符串可以是空的,于是我们应该匹配0个或多个非引号字符:

    > 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 代码块 (反斜杠包裹的文本) 转换为 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 函数来转换number。 它也有跟 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'
    '
    rest_of_line_nl = C((1 - lf)^0*lf)         -- capture chars upto 
    
    quoted_line = '> '*rest_of_line_nl       -- block quote lines start with '> '
    -- collect the quoted lines and put inside [[[..]]]
    quote = Cs (quoted_line^1)/"[[[
    %1]]]
    "
    
    > = quote:match '> hello
    > dolly
    '
    "[[[
    > hello
    > dolly
    ]]]
    "
    

    这不是那么正确 - Cs 捕获所有东西, 包括 '> '。 但我们可以强迫部分捕获返回空字符串: }}}

    function empty(p)
        return C(p)/''
    end
    
    quoted_line = empty ('> ') * rest_of_line_nl
    ...
    

    现在一切都可以正常工作了!

    这是最终用来转换Markdown文档到Lua维基格式的代码:

    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'
    '
    rest_of_line = C((1 - lf)^1)
    rest_of_line_nl = C((1 - lf)^0*lf)
    
    -- indented code block
    indent = P'	' + P'    '
    indented = empty(indent)*rest_of_line_nl
    -- which we'll assume are Lua code
    block = Cs(indented^1)/'    [[[!Lua
    %1]]]
    '
    
    -- use > to get simple quoted block
    quoted_line = empty('> ')*rest_of_line_nl 
    quote = Cs (quoted_line^1)/"[[[
    %1]]]
    "
     
    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))
    

    因为这篇维基的转义问题,我需要在这个文本里将'{' 替换为'[' 。请注意!

    SteveDonovan, 12 June 2012 --翻译: @spin6lock


    组捕获和反向捕获

    本节内容将剖析组捕获和反向捕获 (Cg()Cb() )。

    组捕获有两种: 命名式及匿名式。

        Cg(C"baz" * C"qux", "name") -- 命名组。
    
        Cg(C"foo" * C"bar")         -- 匿名组。
        

    我们先看看简单的部分:table捕获里的命名组。

        Ct(Cc"foo" * Cg(Cc"bar" * Cc"baz", "TAG")* Cc"qux"):match"" 
        --> { "foo", "qux", TAG = "bar" }
        

    在表捕获里,第一个捕获的值 ("bar") 会被分配到对应的key ("TAG") 里去。 就像你看到的那样, Cc"baz" 丢失了。 label必须是一个字符串 (如果是数字,则会被转换为字符串)。

    注意分组必须是table的一个直接孩子,否则table捕获不会处理:

        Ct(C(Cg(1,"foo"))):match"a"
        --> {"a"}
        

    捕获和值

    在深入研究分组之前,我们需要探索一下捕获如何处理他们的子捕获。

    有些捕获是操作它们子捕获的值,有些则是有时候这是反直觉的。

    我们看看以下这个例子:

        (1 * C( C"b" * C"c" ) * 1):match"abcd"
        --> "bc", "b", "c"
        

    就像你看到的一样,它把三个值插入了

    我们把它用table报装一下:

        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()在这种情形下也是透明的。他们只产生一个table或者一个字符串。

    另一方面, 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 ]是按顺序测试的捕获。 用 [ ** ] 标记的则不是,原因见下。有点千头万绪,但是在我能考虑到的范围里是完备的。

        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 翻译by:@spin6lock


    RecentChanges · preferences
    edit · history
    Last edited May 24, 2015 11:11 am GMT (diff)

  • 相关阅读:
    Leetcode 1489找到最小生成树李关键边和伪关键边
    Leetcode 113 路径总和 II
    hdu 1223 还是畅通工程
    hdu 1087 Super Jumping! Jumping! Jumping!
    hdu 1008 Elevator
    hdu 1037 Keep on Truckin'
    湖工oj 1241 畅通工程
    湖工oj 1162 大武汉局域网
    hdu 2057 A + B Again
    poj 2236 Wireless Network
  • 原文地址:https://www.cnblogs.com/Lifehacker/p/lpeg_tutorial.html
Copyright © 2011-2022 走看看