Lua解析神器Lpeg

Lua Lpeg

  • 用基本匹配函数,组合匹配表达式
  • 所有匹配函数返回userdata类型,是一个匹配模式(以下用pattern代替),可相互组合

lpeg.P

lpeg.P(value)

将给定的value,根据规则返回适当的pattern,规则如下:

  • value是pattern,原封不动的返回这个pattern
  • value是string, 返回整个该字符串的pattern
  • value是非负整数,只有在输入的字符串长度大于等于value个字符串时返回成功。
  • value是负整数,只有在输入的字符串长度还剩下不到value个字符才返回成功,lpeg.P(-n)等价于-lpeg.P(n)
  • value是boolean,总是返回成功或者失败的pattern
  • value是table,将它理解为一个语法(Grammar)
  • value是function,返回pattern,它等价于一个match-time capture用一个空字符串的匹配模式(lpeg.P(function)等价于lpeg.Cmt("", function))。

示例:

  • 都是从第一个字符串开始匹配,若没匹配到就返回nil
  • 如果匹配到pattern,则返回匹配到的字符串长度加1
local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

-- value是string的情况
local ps = P'ab'
print(match(ps, "a"))  ---> nil
print(match(ps, "abcd"))  ---> 3  注:只匹配"ab"

-- value是非负整数
-- 注:完全匹配任意value个字符
local pn1 = P(3)  
-- local pn1 = P(3.0)  -- 这样也是可以的
print(match(pn1, "12"))  ---> nil  注:只有2个字符
print(match(pn1, "abcd"))  ---> 4

-- 注:value不能是小数,但是小数部分为0是可以的
-- 如果是小数,总是返回1
local pn2 = P(3.1)
print(match(pn2, ""))  ---> 1

-- value是负整数
-- 被匹配的字符串长度要小于value
local pn3 = P(-3)  -- 等价于  local pn3 = - P(3)
print(match(pn3, "abcd"))  ---> nil  注:字符串长度>=3了
print(match(pn3, "ab"))  ---> 1
print(match(pn3, ""))  ---> 1

-- value是boolean
local pb1 = P(true)
local str1 = "aaa"  -- str1为任意字符串
print(match(pb1, str1))  ---> 1  注:当value为true时,不管str1是什么字符串,始终返回1

local pb2 = P(false)
local str2 = "aaa"  -- str2为任意字符串
print(match(pb2, str2))  ---> nil  注:当value为false时,不管str2是什么字符串,始终返回nil

-- value是table
local m1 = Cs((P(1) / { a = 1, b = 2, c = 3 }) ^ 0)
print(match(m1, "abc"))  ---> 123

-- value是function
-- 请跳到match-time capture(lpeg.Cmt)

lpeg.B

  • 返回一个匹配模式,只有在输入的字符串在当前位置是patt的谓语时,才被匹配到。
  • patt在有调整长度的字符串中匹配,并且不包含捕获

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local B = lpeg.B

local m1 = B"a"
local m2 = 1 * m1
local m3 = 1 * B"b"

print(match(m1, "a"))  ---> nil
print(match(m2, "a"))  ---> 2
print(match(m3, "a"))  ---> nil

-- 注:这里的m3和m2的区别就是一个是a,一个是b,但得到的结果不一样

local m4 = B(1)
local m5 = 1 * m4

print(match(m4, "a"))  ---> nil
print(match(m5, "a"))  ---> 2
print(match(-m4, "a"))  ---> nil

local m6 = B(250)
local m7 = 250 * m6

print(match(m6, string.rep("a", 250)))  ---> nil
print(match(m7, string.rep("a", 250)))  ---> 251

lpeg.R

  • 给定一个2字符的字符串(可以传多个这样的字符串),返回两个字符之间范围的pattern
  • 字符可以为任意字符编码(闭区间)

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local R = lpeg.R

local r1 = R"09" -- 匹配任意数字
print(match(r1, "12345"))  ---> 2 注:只要匹配到1个字符就返回

local r2 = R("az", "AZ")  -- 可以传多个2个字符组成的字符串
print(match(r2, "abcd"))  ---> 2
print(match(r2, "Abcd"))  ---> 2

-- 特别注意
local r3 = R()
local str = "aaa"
print(match(r3, str))  ---> nil
-- 当R没有参数时,不管str为什么字符串,总返回nil

lpeg.S

  • 给定一个字符串(字符串等价于字符的集合(set)),返回一个匹配该字符串里任意一个字符的pattern

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local S = lpeg.S

local s1 = S"+-*/"
print(match(s1, "+"))  ---> 2
print(match(s1, "-"))  ---> 2
print(match(s1, "*"))  ---> 2
print(match(s1, "/"))  ---> 2

-- 特别注意
local s2 = S""
local str = "aaa"
print(match(s2, str))  ---> nil
-- 注:当S的参数为空字符串的时候,不管str是什么字符串,总返回nil

lpeg.V

  • 这个操作为语法(Grammar)生成一个变量(non-terminal),变量规则是闭包语法中的v
  • 个人理解:因为语法是通过table来定义的,调用这个函数告诉lpeg在table里定义了哪些变量名
  • 剩下的后面说哇~~

lpeg.locale

  • 返回一个table,table里面的元素是lpeg提供的常用pattern,有以下匹配模式:alnum,alpha,cntrl,digit,graph,lower,print,punct,space,upper,xdigit
  • 如果参数是table,则会把这些pattern放到参数的table里面

示例:

local lpeg = require "lpeg"

local lpegHelp = {}
lpeg.locale(lpegHelp)

local help = lpeg.locale()

Lpeg操作符

#patt

  • 返回一个pattern。如果输入的字符串被patt匹配到,这个pattern才会被匹配。
  • 这个pattern被叫做,and谓语,等价于原生PEG中的&patt
  • 这个pattern从不产生任何捕获。

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P
local C = lpeg.C
local V= lpeg.V

local p1 = P"aa"
local p2 = #p1

print(match(p1, "a"))  ---> nil
print(match(p2, "a"))  ---> nil

print(match(p1, "aaaa"))  ---> 3
print(match(p2, "aaaa"))  ---> 1

-- 进阶
local pat = P{
    "S";
    S1 = C("abc") + 3,
    S = #V("S1")     -- 这里捕获到了结果,但是#必须忽略这个结果
}

print(match(pat, "abc"))  ---> 1

-- 调整长度(fixed length)
local m1 = #("a" * (P"bd" + "cd"))
local m2 = C(m1 * 2)
local m3 = C(m1 * 3)
local m4 = C(m1 * 4)
print(match(m1, "acd"))  ---> 1
print(match(m2, "acd"))  ---> ac
print(match(m3, "acd"))  ---> acd
print(match(m4, "acd"))  ---> nil

-- 解析:
-- 1. #patt后面跟着 * number时,是对捕获到的值的长度进行调整
-- 2. number的值不能超过捕获到的值的长度

-patt

  • 返回一个pattern,只有patt没有被匹配到的时候始终返回1,否则返回nil
  • 这个pattern从不产生任何捕获
  • 这个pattern等价于patt的补集
  • 个人理解:这里始终返回1是根据结果猜的,如果有其他情况后面再补上

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

local p1 = P"aa"
local p2 = - p1

print(match(p1, "aa"))  ---> 3
print(match(p2, "aa"))  ---> nil

print(match(p1, "a"))  ---> nil
print(match(p2, "a"))  ---> 1

patt1 + patt2

  • 返回一个pattern,匹配patt1或者patt2
  • 如果patt1和patt2都是字符集合,这个操作,返回结果的并集。

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

local p1 = P"aaa"
local p2 = P"aa"
local p3 = p1 + p2

print(match(p3, "a"))  ---> nil
print(match(p3, "aa"))  ---> 3
print(match(p3, "aaa"))  ---> 4

patt1 - patt2

  • 返回一个pattern,不捕获patt2并且捕获到patt1
  • 当捕获成功后,这个pattern产生patt1的所有捕获,并且从不捕获patt2
  • 如果patt1和patt2都是字符集合,这个操作相当于两个集合的差集。- patt2 = "" - patt2 = 0 - patt2。(如果patt2是字符集合,那么1 - patt2是补集)
local lpeg = require "lpeg"

local match = lpeg.match
local S = lpeg.S

local s1 = S"\1\0\2"
local s2 = S"\0"
local s3 = s1 - s2

print(match(s1, "\0"))  ---> 2
print(match(s2, "\0"))  ---> 2
print(match(s3, "\0"))  ---> 0
print(match(s3, "\1"))  ---> 2

-- 注:这里的s3是s1与s2的差集,也就是不配s2并且匹配s1

patt1 * patt2

  • 返回一个pattern,它会先匹配patt1,如果匹配成功继续匹配patt2

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

local p1 = P"aa"
local p2 = P"bb"
local p3 = p1 * p2

print(match(p3, "aabb"))  ---> 5
print(match(p3, "aaaa"))  ---> nil

patt^n

  • 如果n是非负整数,这个pattern将匹配n个或n个以上patt
  • 如果n是负整数,那么这个pattern讲最多匹配n个patt,也就是最少匹配0个,最多匹配n个
  • patt ^ 0类似于lua里面正则表达式的*
  • patt ^ 1类似于lua里面正则表达式的+
  • patt ^ -1类似于lua里面正则表达式的?
  • 在所有情况下,此pattern的结果是没有回溯的贪婪模式,也就是说匹配最长的可能匹配到的序列
local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

local p1 = P"aa"
local p2 = p1 ^ 0
local p3 = p1 ^ 3
local p4 = p1 ^ -1

print(match(p1, "aaaa"))  ---> 3
print(match(p2, "aaaa"))  ---> 5
print(match(p3, "aaaa"))  ---> nil
print(match(p3, "aaaaaa"))  ---> 7
print(match(p4, "aaaa"))  ---> 3
print(match(p4, "a"))  ---> 1  注:这里没有匹配到也返回1

Lpeg语法(Grammars)

  • 在lua的环境下,可以自定义一些patterns,让新定义的pattern可以使用已经定义过的旧的pattern。然而,这种技术不允许定义递归匹配模式。对于递归匹配模式,我们需要真正的语法。
  • Lpeg用tables来描述语法,每个条目就是一个语法规则。
  • 通过调用lpeg.V(v)来创建一个pattren,它表示在语法中的一个引索V。
  • 当table被转换成了pattern(通过调用lpeg.P或者在一个pattern中使用),它被固定了。
  • 当table已经固定,结果是一个匹配它初始规则的pattern。第一个引索的条目就是她初始规则。如果这个条目是字符串,那么它被认为是初始规则的名字。否则lpeg假定第一个条目就是table的初始规则。

捕获(Captures)

  • 捕获是一种模式,该模式会根据匹配到的数据返回值(语义信息)。
  • lpeg有几种捕获方式,产生的值会基于匹配,并且组合这个值产生新的值。
  • 每个捕获可能产生0个或多个值。

lpeg.C (patt)

  • 创建一个简单的捕获,捕获匹配到patt的子字符串。
  • 捕获的值是一个字符串。
  • 如果patt有其他捕获,他们的值将在这个值之后返回

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P
local R = lpeg.R
local S = lpeg.S

local C = lpeg.C

local p1 = P"aa" ^ 3
local cp1 = C(p1)

print(match(cp1, "aaaaaa"))  ---> aaaaaa
print(match(cp1, "aa"))  ---> nil

local r1 = R"09" ^ 0
local cr1 = C(r1)

print(match(cr1, "1234aaaa"))  ---> 1234
print(#match(cr1, "aaaa"))  ---> 0  注:这里返回的是空字符串,跟lpeg.P不同

local s1 = S"+-*/" ^ 0
local cs1 = C(s1)

print(match(cs1, "++++aaaa"))  ---> ++++
print(#match(cs1, "aaaa"))  ---> 0  注:这里返回的是空字符串,跟lpeg.P不同 

-- 进阶
local m1 = { [1] = C(C(1) * V(1) + -1) }

print(match(m1, "abc"))  ---> "abc" "a" "bc" "c" "c" ""  注:这里的值添加了引号,最后一个是空字符串

-- 解析:
-- 过程:
-- ① (1)匹配到a,然后匹配V(1),由于C(1),产生结果a
-- ② (1)匹配到b,然后匹配V(1),由于C(1),产生结果b
-- ③ (1)匹配到c,然后匹配V(1),由于C(1),产生结果c
-- ④ (1)C(1) * V(1)没有匹配到,匹配到-1,返回到③,由于-1,产生""
-- ③ (2)匹配到C(1) * V(1),返回到②,由于C(1) * V(1),产生c和④的组合,就是c
-- ② (2)匹配到C(1) * V(1),返回到①,由于C(1) * V(1),产生b和③的组合,就是bc
-- ① (2)匹配到C(1) * V(1),返回结果,由于C(1) * V(1),产生a和②的组合,就是abc
-- 这个过程是个递归的过程,看似有点复杂,根据结果,仔细分析一下就能得出结论啦

lpeg.Carg (n)

  • 创建一个参数捕获。
  • 未完待续

lpeg.Cb (name)

  • 创建一个回退捕获
  • 这个pattern匹配空字符串,并且通过最近的group cpature(lpeg.Cg)被命名的名字(名字可以是任何Lua类型),产生返回值
  • 最近的group cpature(lpeg.Cg)被命名的名字的意思是,最近一次完整的给定名字的组捕获
  • 完整的捕获意思是,整个模式对应的捕获匹配。
  • 外层的意思是,这个捕获不是在另一个完整的捕获

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local Cg = lpeg.Cg
local Cb = lpeg.Cb

local t = {}
local foo = function(s)
    t[#t + 1] = s
    return s .. "x"
end

local m1 = Cg(C(2) / foo, "y") * Cb"y"
             * Cg(Cb"y" / foo, "y") * Cb"y"
             * Cg(Cb"y" / foo, "y") * Cb"y"

print(match(m1, "ab"))  ---> abx abxx abxxx
print(table.concat(t, " "))  ---> ab ab abx ab abx abxx

-- 解析:
-- ① C(2)捕获到"ab",通过foo函数,向t插入"ab",并返回"abx",再给"abx"打上"y"的标记
-- ② 接下来的Cb"y",捕获到打了"y"标记的"abx",这里产生了"abx"
-- ③ 第一个Cg(Cb"y" / foo, "y"),这里的Cb"y",从Cg(C(2) / foo, "y")开始,走①②流程
-- ④ 然后再匹配Cg(③的结果 / foo, "y") * Cb"y",向t插入"abx",此时t里面的内容是"ab" "ab" "abx",并通过foo函数返回"abxx",并打上"y"标记
-- ⑤ 接下来的Cb"y",捕获到打了"y"标记的"abxx",这里产生了"abxx"
-- ⑥ 第二个Cg(Cb"y" / foo, "y"),重复③的流程

-- 最后m1捕获到的结果是:"abx" "abxx" "abxxx"
-- t里面的结果是
-- {
--      "ab",                       -- 这个是Cg(C(2) / foo, "y") * Cb"y"的结果
--      "ab", "abx",                -- 这个是第一个* Cg(Cb"y" / foo, "y") * Cb"y"的结果
--      "ab", "abx", "abxx"         -- 这个是第二个* Cg(Cb"y" / foo, "y") * Cb"y"的结果
-- }

lpeg.Cg (patt [, name])

  • 创建一个组捕获。这个组捕获的所有返回值都是通过patt捕获到的简单返回值
  • 如果没有给组取名字,那么这个组就是匿名的,或者这个组的名字是给定的名字(nil不能作为名字)
  • 在大多数情况下,一个被命名的组,是没有任何捕获值返回的,只有在back capture(lpeg.Cb)或者table capture(lpeg.Ct)才会有捕获值返回

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local Cg = lpeg.Cg
local Cc = lpeg.Cc
local C = lpeg.C

local m1 = Cg(1)
print(match(m1, "x"))  ---> x

local m2 = Cg(Cg(Cg(1)))
print(match(m2, "x"))  ---> x

local m3 = Cg(Cg(Cg(C(1)) ^ 0) * Cg(Cc(1) * Cc(2)))
print(match(m3, "abc"))  ---> a b c 1 2

local m4 = Ct(Cg(Cc(10), "hi") * C(1) ^ 0 * Cg(Cc(20), "ho"))
print(match(m4, "abc"))  ---> { hi = 10, ho = 20, "a", "b", "c" }

-- 非字符串的组名
local print = print
local tab = {}
local m5 = Ct(Cg(1, print) * Cg(1, 23.5) * Cg(1, tab))

local result = match(m5, "abcdefghij")
print(result)  ---> { [function: 0x10322db70] = "a", [23.5] = "b", [table: 0x7faaa2408660] = "c" }

print(result[print])  ---> a
print(result[tab])  ---> c

lpeg.Cp ()

  • 创建一个位置捕获。这个pattern匹配空字符串,并且返回捕获的位置
  • 这个捕获值是数字类型

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local R = lpeg.R
local Cp = lpeg.Cp

local letter = R"az" + R"AZ"

local m1 = letter ^ 1
local m2 = Cp() * m1
local m3 = m2 * Cp()
local m4 = C(m3)
local m5 = Cp() * letter ^ 5 * Cp()

print(match(m1, "abcd"))  ---> 5
print(match(m2, "abcd"))  ---> 1
print(match(m3, "abcd"))  ---> 1 5
print(match(m4, "abcd"))  ---> abcd 1 5
print(match(m5, "abcd"))  ---> nil

-- 解析:
-- 1. 通过m1和m2,推出:如果有Cp的pattern只返回Cp的结果
-- 2. 通过m3,推出:存在Cp时,并匹配成功,只返回Cp()相关结果
-- 3. 通过m4,推出:会优先捕获其他pattern的结果,再返回Cp的结果

lpeg.Cc ([value, ...])

  • 创建一个常量捕获,这个pattern匹配到一个空字符串并且产生所有给定的值作为捕获的值

示例

local lpeg = require "lpeg"

local match = lpeg.match
local R = lpeg.R
local Cc = lpeg.Cc

local m1 = C(R"09" ^ 1 * Cc("a"))
local m2 = C(R"09" ^ 1 * Cc("a", "b"))
local m3 = Cc(nil)
local m4 = Cc()

print(match(m1, "123"))  ---> 123 a
print(match(m2, "123"))  ---> 123 a b
print(match(m3, "123"))  ---> nil
print(match(m4, "123"))  ---> 1

----- 分割线 -----

local m5 = Cc() * Cc() * Cc(1) * Cc(20, 30, 40) * "a" * Cp()
print(match(m5, "aaa"))  ---> 1 20 30 40 2

local m6 = Cc() * Cc() * Cc(nil) * Cc(1) * Cc() * Cc(20, 30, 40) * "a" * Cp()
local m7 = Cc(1) * Cc(2) * Cc(nil) * Cc(3) * Cc()
print(match(m6, "aaa"))  ---> nil 1 20 30 40 2
print(match(m7, "aaa"))  ---> 1 2 nil 3  注:这里只返回了4个结果
-- 根据结果,推出以下结论:
-- 1. Cc()单独匹配时返回1,例如m4
-- 2. Cc()和其他太参数的Cc函数或其他pattern组合时,Cc()会被忽略,即Cc()什么都不会返回

lpeg.Cf (patt, func)

  • 创建一个迭代捕获。
  • 如果这个patt产生了一个C1, C2, ..., Cn的捕获列表,那么这个捕获将产生一个值,这个值由func(...func(func(C1, C2), C3)..., Cn)产生
  • 也就是说,它会使用func对产生的捕获值进行迭代
  • 这个patt至少捕获到一个值(任何类型的值),这个值来作为累积器的初始值。至少要这个条件成立
  • 如果patt要一个特殊的初始值,应该在这个patt前面加一个constant capture(lpeg.Cc)
  • 对于后面的每一个捕获,LPeg调用func,这个累积值作为func的第一个参数,这个捕获产生的所有值作为这个func的额外参数,func的结果成为新的累积值。这个累积的最终值就是这个patt的结果

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local R = lpeg.R
local Cf = lpeg.Cf
local Cc = lpeg.Cc
local C = lpeg.C

local number = R"09" ^ 1 /tonumber
local list = number * ("," * number) ^ 0

function add(acc, cpatureNewValue)
    return acc + cpatureNewValue
end

local sum = Cf(list, add)
print(match(sum, "10,30,43"))  ---> 83

local f = function(x)
    return x + 1
end

local m1 = Cc(0) * C(1) ^ 0
local m2 = Cf(m1, f)
print(match(m1, "alo alo"))  ---> 0 "a" "l" "o" "" "a" "l" "o"
print(match(m2, "alo alo"))  ---> 7
-- 注:通过m1的结果,推出:Cc(0)是第一个捕获值,最后迭代出7

local m3 = Cf(Cc(1, 2, 3), error)
print(match(m3, ""))  ---> 1  注:这里调用error直接返回1(个人理解)

local m4 = Ct(true) * Cg(C(R"az" ^ 1) * "=" * C(R"az" ^ 1) * ";") ^ 0
local m5 = Cf(m4, rawset)
print(match(m4, "a=b;c=du;xux=yuy;"))  ---> {} a b c du xux yuy
print(match(m5, "a=b;c=du;xux=yuy;"))  ---> { a= "b", c = "du", xux = "yuy" }
-- 注:这里的Ct(true)捕获到一个空table,m5通过rawset向空table设置key-value

lpeg.Cs (patt)

  • 创建一个替换捕获

lpeg.Ct (patt)

  • 创建一个table捕获。这个捕获返回一个table
  • 返回所有patt的匿名捕获的值,key从1开始
  • 此外,每个被命名的捕获组,第一个值是组名

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local R = lpeg.R
local Ct = lpeg.Ct
local C = lpeg.C
local Cc = lpeg.Cc

local letter = R"az" + R"AZ"

local m1 = letter ^ 1
local m2 = Ct(m1)

print(match(m1, "alo"))  ---> 4
print(match(m2, "alo"))  ---> {}   注:m1没有返回捕获的值

local m3 = C(m1)
local m4 = Ct(m3) * Cc("t")

print(match(m4, "alo"))  ---> { "alo" }  "t"

local m5 = Ct(C(C(letter) ^ 1))
print(match(m5, "alo"))  ---> { "alo", "a", "l", "o" }

local m6 = Ct((Cp() * letter * Cp()) ^ 1)
local m7 = Ct(m6)

print(match(m6, "alo"))  ---> { 1, 2, 2, 3, 3, 4 }
print(match(m7, "alo"))  ---> { { 1, 2, 2, 3, 3, 4 } }

patt / string

  • 创建一个字符串捕获。这个操作会创建一个基于string的字符串捕获
  • 返回的捕获的值是string的一个副本,string中的%是转义字符,如果要表示%,则用%%来表示
  • 字符串中的任何序列中有%n(n在1到9之间),在patt中匹配第n个捕获。%0表示匹配到的整个序列

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P
local Cp = lpeg.Cp

local pi = "3.14159 26535 89793 23846 26433 83279 50288 41971 69399 37510"

local m1 = (P"1" / "a" + P"5" / "b" + P"9" / "c" + 1) ^ 0
print(match(m1, pi))  ---> a a b c b b c c c b a c a c c c b a
-- 注:这里返回的是捕获到值后被替换的值

print(match(m1, "222"))  ---> 4  注:这里只匹配到了1,所以返回4

local m2 = Cp() * P(3) * Cp() / "%2%1%1 - %0"
print(match(m2, "abcde"))  ---> "411 - abc"
-- 解析:
-- 1. 先匹配Cp() * P(3) * Cp(),匹配到 1 4
-- 2. 然后通过 / "%2%1%1 - %0" 格式化结果
-- 3. 这里的%1对应1,%2对应4,%0对应"abc"

local m3 = C"a" / "%1%%%0"
print(match(m3, "a"))  ---> a%a

local m4 = P(1) / "%0"
print(match(m4, "abc"))  ---> a

local m5 = C(1) ^ 0 / "%2-%9-%0-%9"
print(match(m5, "0123456789"))  ---> "1-8-0123456789-8"

local m6 = C(1) ^ 0 / "9-%1-%0-%3"
print(match(m6, "12345678901234567890"))  ---> "9-1-12345678901234567890-3"
-- 注:%1 - %9 是所有捕获的结果,%0 - %9 的值都只能是数字或者字符串,其他类型报错

patt / number

  • 创建一个有编号的捕获
  • 对于非0的number,返回的是被捕获的第n个值
  • 当number为0时,不会返回捕获的值

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local C = lpeg.C

local m1 = C(1)
local m2 = C(C(m1 * C(2)) * C(3))
local m3 = m2 / 3
local m4 = m2 / 0

print(match(m2, "aaabcdefgh"))  ---> aaabcd aaa a aa bcd
-- 注:这里返回值的顺序是什么规律?按匹配的顺序也不像哇~~~~

print(match(m3, "aaabcdefgh"))  ---> a
print(match(m4, "aaabcdefgh"))  ---> 7
-- 注:
-- 1. 如果是负数会报错
-- 2. 如果number超出捕获的长度,也会报错

local m5 = m1 * (C(m1 * C(2)) * C(3) / 4) * m1
print(match(m5, "abcdefgh"))  ---> a efg h
-- 解析:
-- ① 匹配m1,捕获到a
-- ② (1)先匹配m1,捕获到b
-- ② (2)再匹配C(2),捕获到cd
-- ② (3)再匹配m1 * C(2),捕获到bcd
-- ② (4)再匹配C(3),捕获到efg
-- ② (5)再取②捕获到的值的第4个捕获值,最终得到efg
-- ③ (6)最后匹配m1,捕获到h

patt / table

  • 创建一个查询捕获。patt捕获到的值作为table中的key,来引索table中的value
  • table中的值是捕获的最终值。如果table没有key,将不会产生捕获

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P

local pi = "3.14159 26535 89793 23846 26433 83279 50288 41971 69399 37510"
local m1 = (P(1) / { ["1"] = "a", ["5"] = "b", ["9"] = "c" }) ^ 0

print(match(m1, pi))  ---> a a b c b b c c c b a c a c c c b a
print(match(m1, "222"))  ---> 4

patt / function

  • 创建一个函数捕获
  • 这个pattern将调用给定的function,并把捕获到的所有值当参数传个这个function
  • function的返回值是这个捕获的最终值,如果function没有返回值,也就没有捕获值

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local C = lpeg.C
local Cg = lpeg.Cg
local Cb = lpeg.Cb

local foo = function(s)
    return s .. "0"
end

local m1 = Cg(C(3) / foo, "name") * Cb("name")
print(match(m1, "abc"))  ---> abc0

lpeg.Cmt(patt, function)

  • 创建一个匹配时的捕获
  • 不像其他捕获,当有一个匹配时,会立即执行(甚至在一个很复杂的匹配模式中失败)
  • 它会强制执行所有嵌入的捕获,并调用function
  • 整个待匹配的字符串,当前匹配到的坐标,还有任何捕获到的值是这个function的参数
  • function返回的第一个值定义了匹配过程,如果返回的是一个数字,则表示匹配成功,返回的数字成为新的匹配位置
  • 假设一个被匹配的字符串s,和当前的匹配位置i,返回的数字必须在[i, len(s+1)]
  • 如果返回true,匹配成功时,不消耗任何输入(所以返回true,等价于返回i)
  • 如果返回falsenil,或者无返回值,匹配失败
  • function额外的返回值会成为捕获的值

示例:

local lpeg = require "lpeg"

local match = lpeg.match

local S = lpeg.S
local P = lpeg.P
local R = lpeg.R
local V = lpeg.V
local C = lpeg.C

local Cs = lpeg.Cs
local Ct = lpeg.Ct
local Cg = lpeg.Cg
local Cmt = lpeg.Cmt

local space = S" \t\n" ^ 0

local id = function(s, i, ...)
    return true, ...
end

local m1 = P{
    "S",
    S = V"atom" * space + Cmt(Ct("(" * space * (Cmt(V"S" ^ 1, id) + P(true)) * ")" * space), id),
    atom = Cmt(C(R("AZ", "az", "09") ^ 1), id)
}

print(match(m1, "(a g () ((b) c) (d (e)))"))  ---> { "a", "g", {}, { { "b" }, "c" }, { "d", { "e" } } }

local m2 = Cmt(1, id) ^ 0
print(match(m2, string.rep("a", 5)))  ---> a a a a a

local id = function(s, i, x)
    if x == "a" then
        return i, 1, 3, 7
    end
    
    return nil, 2, 4, 6, 8
end

local m3 = (P(id) * 1 + Cmt(2, id) * 1 + Cmt(1, id) * 1) ^ 0
print(match(m3, "abababab"))  ---> 1 3 7 1 3 7 1 3 7 1 3 7
-- 问题:这里每个Cmt后面要 * 1呢?不加的话会报错

local ref = function(s, i, x)
    -- print(s, i, x)
    return match(x, s, i - #x)
end

local m4 = Cmt(P(1) ^ 0)
local m5 = P(1) * m4
local m6 = P(1) * Cmt(C(1) ^ 0)

print(match(m4, "alo"))  ---> 4
print(match(m5, "alo"))  ---> 4
print(match(m6, "alo"))  ---> nil
-- 注:这里的m5和m6的区别,一个P,一个是C;
-- P后面跟了^0,所以要等P匹配完才产生捕获;
-- 虽然C后面也跟了^0,但是C立马产生捕获,
-- 所以会立马调用ref,就会匹配不到,然后直接返回nil

ref = function(s, i, a, b)
    if a == b then
        return i, a:upper()
    end
end

local any = P(1)

local m7 = Cmt(C(R"az" ^ 1) * "-" * C(R"az" ^ 1), ref)
local m8 = (any - m7) ^ 0 * m7 * any ^ 0 * -1

print(match(m8, "abbbc-bc ddaa"))  ---> BC
-- 解析:
-- ① (1)先匹配(any - m7) ^ 0,这里是不匹配m1,并且匹配any,匹配至少0次
-- ① (2)先匹配m7,这里会捕获到"abbbc"和"bc"当做参数传给ref,也就是ref的a和b两个参数,然后"abbbc"不等于"bc",没有返回值,所以也就没有匹配到m7,且匹配到any
-- ② 重复①的过程,这次m7匹配到"bbbc"和"bc",也没匹配到m7
-- ③ 重复①的过程,这次m7匹配到"bbc"和"bc",也没匹配到m7
-- ④ 重复①的过程,这次m7匹配到"bc"和"bc",这回匹配到m7了,所以(any - m7) ^ 0的匹配终止,产生结果4
-- ⑤ 接着匹配m7,从位置4开始匹配,然后捕获到"bc"和"bc",返回了一个捕获值"BC"

local check = function(_, _, s1, s2)
    return s1 == s2
end

local m9 = "[" * Cg(P"=" ^ 0, "init") * "[" * {
    Cmt("]" * C(P"=" ^ 0) * "]" * Cb("init"), check) + 1 * V(1) 
} / 0
print(match(m9, "[==[]]====]]]]==]===[]"))  ---> 18
-- 解析:
-- ① 首先匹配"[" * Cg(P"=" ^ 0, "init") * "[",这里捕获到"==",并命名为"init",返回位置5
-- ② (1)再匹配{ Cmt("]" * C(P"=" ^ 0) * "]" * Cb("init"), check) + 1 * V(1) } / 0,这里有两个捕获,一个是C(P"=" ^ 0),另一个是Cb("init")
-- ② (2)先匹配Cmt("]" * C(P"=" ^ 0) * "]" * Cb("init"), check),从位置5开始匹配,匹配到]],产生捕获"",和前面捕获的"=="比较,不相等,返回false,匹配失败
-- ② (3)然后匹配1 * V(1),重复过程②(1)
-- ③ 直到匹配到]==]

Lpeg 小技巧

示例:

local lpeg = require "lpeg"

local match = lpeg.match
local P = lpeg.P
local R = lpeg.R

local V = lpeg.V
local C = lpeg.C

print(match(3, "aaaa"))  ---> 4
print(match(P(3), "aaaa"))  ---> 4

-- lpeg 对lua的数据类型默认使用lpeg.P来处理

local m1 = R"09" * -1
local m2 = R"09" * P(-1)
local m3 = R"09" * -P(1)

-- 这里的m1、m2、m3等价

print(match(m1, "9"))  ---> 2
print(match(m1, "99"))  ---> nil
print(match(m2, "9"))  ---> 2
print(match(m2, "99"))  ---> nil
print(match(m3, "9"))  ---> 2
print(match(m3, "99"))  ---> nil

-- lpeg.V、#patt
local tab = {
    [1] = "(" * (((1 - S"()") + #P"(" * V(1)) ^ 0)
}

-- 这里的V(1)指的是tab[1]的规则
-- 这里的#P"("表示这条规则匹配到的"("的个数(个人结论,不太确定)

local digit = R"09"
local upper = R"AZ"
local lower = R"az"
local letter = S"" + upper + lower
local alpha = letter + digit + R()

local m1 = letter ^ 1
local m2 = m1:C()
local m3 = C(m1)  -- 注:这里的m3等价于m2
local m4 = P{ [1] = m2 + (1 * V(1)) }
local m5 = m4 ^ 0

print(match(m1, "   4achou123..."))  ---> nil
print(match(m4, "   4achou123..."))  ---> achou
print(match(m5, " two words, one more  "))  ---> two  words  one  more

-- 解析:
-- m1是匹配所有字母,通过m4的语法,就可以匹配不以字母开头的字符串
-- 关键点是  1 * V(1)  中的 1 *,这里的1等价于P(1),也就是至少匹配任意一个字符
-- 匹配过程:
-- (1) 第一个字符是空白符,m2是肯定没匹配到,那么匹配(1 * V(1))
-- (2) 匹配到1的时候,符合匹配条件,再匹配V(1),此时又回到过程(1)
-- 直到都不符合匹配。整个过程是个递归的过程,最后把匹配到的结果返回

-- 语法例子
local m6 = P{ P"x" * V(1) + P"y" }
-- 这里的m6可以改为 local m6 = P{ P"x" * V(1) + -1 } 看看是什么结果
local m7 = -m6
local m8 = C(m7)

print(match(m6, "xxx"))  ---> nil
print(match(m7, "xxx"))  ---> 1
print(match(m8, "xxx"))  ---> ""  注:这里是空字符串

-- 解析:
-- ① m6的匹配过程:一直匹配P"x" * V(1),知道最后一个x,然后匹配P"y",但是没有没匹配到,直接返回nil
-- ② m7根据①的返回结果,返回①的补集,这里nil的补集就是1
-- ③ m8根据②的返回结果,捕获到一个空字符串
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容