pyparsing:自定义一个属于你的语法解析器(更新中)

楔子

pyparsing是用于解析字符串的模块,可以从大量日志中解析出指定的内容、也可以自定义一个解析器,从而判断相应的文本是不是符合指定的规则。对,它类似于正则。如果使用正则的话,其实完全可以实现pyparsing的所有功能,但是pyparsing最大特点就是它的可读性更强一些。下面我们一起来看看:

从一个简单的例子开始

from pyparsing import Word, nums, alphas

match = Word(nums) + Word(alphas)
print(match.parseString("250abc"))  # ['250', 'abc']

首先说一点,在pyparsing里面有这么几个东西:

  • nums:"0123456789",即所有的数字
  • alphas:"abcdef...z" + "ABCDEF...Z",即所有的英文字符
  • hexnums:nums + "ABCDEFabcdef",也就是16进制数字
  • alphanums:alphas + nums

然后回到上面的例子,一个Word对象可以理解为一个单词,Word(nums)就表示一个由数字组成的单词,个数不限。所以上面的match就表示由两部分、或者说由两个词组成,一个全是数字,一个全是英文字符,那么匹配的字符串应该由数字和字符组成。然后调用parseString,会自动将每一个Word对应的部分解析出来,放在列表里面。

from pyparsing import Word, nums, alphas

match = Word(nums) + Word(alphas)
# 我们看到匹配的时候,默认是忽略空格的
# 第一个Word匹配到了"250",因为遇到空格结束了,然后下一个Word会从"250"之后不是空格的地方开始匹配
# 所以第二个Word匹配到satori,然后后面又遇到空格,于是匹配结束。
print(match.parseString("250    satori abc"))  # ['250', 'satori']
# match是"数字" + "字符",所以会匹配250和aaa,因为aaa后面是数字,就不匹配了
print(match.parseString("250aaa250bbb"))  # ['250', 'aaa']

# 可以加上一个parseAll参数,默认为False,如果指定为True,则类似于严格模式,意思就是必须和match严格匹配
# 这里的match是"数字" + "字符"
try:
    print(match.parseString("250aaa250", parseAll=True))
except Exception as e:
    print(e)  # Expected end of text, found '2'  (at char 6), (line:1, col:7)
# 我们看到报错了,因为"250aaa250"中的"aaa"后面又出现了数字,等于是"数字" + "字符" + "数字"
# 而match要求是"数字" + "字符",所以不符合。
# 所以parseAll=True,则表示解析的字符串的组成必须整体和match匹配
# 如果parseAll=False,那么只要从头开始能匹配到"数字" + "字符"的组合即可

try:
    match.parseString("a250aaa")
except Exception as e:
    print(e)  # Expected W:(0123...), found 'a'  (at char 0), (line:1, col:1)
# 而解析"a250aaa"即使不加parseAll=True,也报错了
# 因为match是"数字" + "字符",而解析的字符串的开头是"字符",不是"数字",所以不符合
# 而根据报错信息我们也知道,"希望的是0123...,但是却发现了字符a"
# 并且,我们也发现了,parseString是从头开始匹配的

目前你应该对pyparsing有一个基本的认识

Word对象

Word对象是用来定义要匹配的字符串的模式的,我们上面已经见识过了。不过它除了接收一个字符串之外,还可以接收其它参数。

from pyparsing import Word, nums, alphas

# 我们说Word(nums)等价于Word("0123456789")
# 那么我们也可以输入自己定义的字符串,下面表示匹配的字符串的开头必须全部由123456组成
# 因为不是全局匹配,所以哪怕只有开头一个字符符合条件也是可以的
match = Word("123456")
print(match.parseString("1aa"))  # ['1']
# 当然如果开头是空格的话,是个例外。会忽略空格
print(match.parseString("    14223abc"))  # ['14223']

但是我们发现在匹配的时候,是按照最大个数匹配的,那么可不可以匹配指定的数量呢?

from pyparsing import Word, nums, alphas

"""
Word里面有一个min参数和一个max参数
但是这个min和max和你想象的有些不一样,至少我是在一开始的时候想错了
"""
match = Word(nums, min=1, max=3)
try:
    print(match.parseString("123456"))
except Exception as e:
    print(e)  # Expected W:(0123...), found '4'  (at char 3), (line:1, col:4)
"""
但是它报错了,估计有人以为min=1,max=3表示至少匹配一个,至多匹配三个,所以应该得到123才对
但是不是的,Word里面的参数min和max指的是匹配的字符串里面至少出现的字符个数和至多出现的字符个数
比如:Word(nums, min=1, max=3),它表示的是parseString里面的字符串必须由数字组成,并且至少有1个,至多有3个
但是我们这里出现了6个,所以报错了
"""
print(match.parseString("1"))  # ['1']
print(match.parseString("12"))  # ['12']
print(match.parseString("123"))  # ['123']
# 以上都是符合条件的,依旧是按照最大个数匹配
# 至于我们不指定的话,min默认为1,并且这个min要大于等于1
# max默认为0,表示没有限制,如果大于0,那么匹配的字符串中对应部分的字符的个数不能超过max的值

# 如果在max不为0的情况下,记得max一定要大于等于min,否则不会匹配出结果的
# 另外,如果想匹配指定个数的字符怎么办呢?比如必须是4个字符,那么只需要min和max相等、都为4即可
match = Word(nums, min=4, max=4)
print(match.parseString("1234"))  # ['1234']
try:
    # 必须是4个,其它个数不可以
    print(match.parseString("123"))  # Expected W:(0123...), found end of text  (at char 3), (line:1, col:4)
except Exception as e:
    print(e)

再来想一下python中的变量名,要求必须由字母、数字、下划线组成,但是开头不能是数字。那么我们也来实现一下

from pyparsing import Word, alphanums

match = Word(alphanums + "_")
print(match.parseString("name"))  # ['name']
print(match.parseString("var1"))  # ['var1']
print(match.parseString("_var1"))  # ['_var1']
# 问题来了
print(match.parseString("222"))  # ['222']

我们看到,虽然由数字、字母、下划线组成的字符串确实能够正常匹配,但是很明显这个match它没有考虑到开头不能为数字的条件。因此我们需要改善这一点,于是就引出了Word里面的另一个参数:

"""
首先Word是一个类,里面的__init__函数,除了self,前4个参数如下
initChars, 
bodyChars=None, 
min=1, 
max=0

里面的min和max我们都见过了,而initChars显然是我们一开始就输入的字符串
重点是bodyChars,注意它和initChars之间的关系
从名字上也能看出initChars指的是匹配部分的第一个字符
而bodyChars指的是匹配部分的第二个、以及之后的字符

如果bodyChars为None,那么所有字符都以initChars为准
否则第一个字符以initChars为准,后面的字符以bodyChars为准
"""

光说不练假把式,我们举几个例子:

from pyparsing import Word, alphanums, alphas

# 表示开头只能是字母加下划线,后面的部分则是字母、数字、下划线均可
match = Word(alphas + "_", alphanums + "_")
print(match.parseString("_abc"))  # ['_abc']
print(match.parseString("_22"))  # ['_22']
print(match.parseString("a_bc_"))  # ['a_bc_']
try:
    print(match.parseString("2ac"))
except Exception as e:
    print(e)  # Expected W:(ABCD..., ABCD...), found '2'  (at char 0), (line:1, col:1)
    

以上我们就实现了变量名的合法检测,为了更好的理解,我们再来看几个复杂点的栗子

from pyparsing import Word, nums, alphas

# 显然match由两部分组成,"字母(开头) + 数字(开头之后)" + "下划线(开头) + 字母(开头之后)"
match = Word(alphas, nums) + Word("_", alphas)

# a22的开头为字母,开头之后为数组,然后是_abc,开头为下划线,开头之后为字母
print(match.parseString("a22_abc"))  # ['a22', '_abc']

try:
    print(match.parseString("abc22_abc"))
except Exception as e:
    print(e)  # Expected W:(_, ABCD...), found 'b'  (at char 1), (line:1, col:2)
# 这个报错了,因为match要求第一部分的开头是字母、开头之后的部分全是数字
# 而先不管match各自对应字符串"abc22_abc"的哪一部分,我们就从头看。
# 显然"a"是符合的,但是"a"后面是"b",它是一个字符,显然不符合match的Word(alphas, nums)这部分


# 估计这个可能有点出乎意料
print(match.parseString("a_bc_"))  # ['a', '_bc']
# "a"后面的明明是下划线,不是数字,为什么就能成功呢?
# 因为match的第二部分要求开头是下划线
# "a"后面是下划线,显然它不符合match的第一部分、或者说第一个Word要求的格式,那么第一部分的匹配就此结束,所以结果就匹配了个"a"
# 而第二部分是符合要求的,要求开头是下划线,开头之后是字母,显然匹配到"_bc"结束。
"""
再回过头来看看解析上面的"abc22_abc"的例子,因为"b"不是数字,所以第一部分匹配结束,同样匹配的"a",那么会从"b"开始匹配第二部分
而第二部分的开头要求是下划线,但是看到的确实字符"b",所以报错了,我们看一下上面的报错信息
Expected W:(_, ABCD...), found 'b'  (at char 1), (line:1, col:2)
信息表明:期待一个开头是_,开头之后是ABCD...,但是却看到了字符"b"
"""

# 如果我们指定一下个数的话
match = Word(alphas, nums, min=2) + Word("_", alphas)
try:
    print(match.parseString("a_bc_"))
except Exception as e:
    print(e)  # Expected W:(ABCD..., 0123...), found '_'  (at char 1), (line:1, col:2)
# 报错了,虽然第一个字符"a"是可以的,但是第二个字符是"_"
# 如果是默认的min=1的话,"_"虽然不符合bodyChars指定的nums,但是已经达到最少数量要求了
# 那么该部分就会结束,会进行下一部分的匹配
# 但是这里我们改成了min=2,就要求第一个Word至少能匹配到两个字符。那么意味着第二个字符必须要是第一个Word里面的的bodyChars所指定nums
# 但是"_"不在nums里面,所以报错了
# 看一下报错信息,告诉我们:期待0123...,但是却发现了"_"

# 下面就没有问题了,因为第一个Word的开头之后是能够匹配到nums的
print(match.parseString("a1_bc_"))  # ['a1', '_bc']
print(match.parseString("a1222_bc_"))  # ['a1222', '_bc']

还有一个关键的问题,就是匹配的长度。

from pyparsing import Word, nums, alphas

match = Word(alphas + nums) + Word(nums)
try:
    print(match.parseString("abcdef123456"))
except Exception as e:
    print(e)  # Expected W:(0123...), found end of text  (at char 12), (line:1, col:13)
# 我们看到报错了,告诉我们期待0123...,但是却发现字符串到头了
# 很好理解,因为我们指定的是Word(alphas + nums) + Word(nums),这样本来就是不合理的
# 因为Word(alphas + nums)默认是类似正则的贪婪匹配,它把"abcdef0123456"都给匹配完了,剩余的就没了
# 第二个Word(nums)期望得到数字,但是已经没有东西能留给它了
"""
所以记住一个原则:多个Word对象组合,那么"后一个Word里面的所有字符"不可以是"前一个Word里面的所有字符"的子集
怎么理解呢?比如第一个Word里面是"字符+数字",后一个Word里面是"数字"
如果解析的字符串全部是字符+数字,那么会被第一个Word全部匹配到
那么显然到第二个Word就没有东西能让它匹配了,于是报错,提示:字符串到头了

如果字符串中出现了第一个Word无法匹配的字符,好,那么第一个Word结束,从第一个Word无法匹配的字符开始进行第二个Word的匹配
然而事实上,如果第一个Word无法匹配,那么第二Word肯定更无法匹配
就拿这里的例子,第一个"字母+数字",第二个是"数字",第一个都匹配不上,更不要说第二个了

所以要记住上面说的原则:"后一个Word里面的所有字符"不可以是"前一个Word里面的所有字符"的子集
否则任何字符串都是无法解析的
"""

# 这里可能有人想了,那指定max不就行了吗?我们来试试
match = Word(nums, max=3) + Word(nums)
try:
    print(match.parseString("123123123"))
except Exception as e:
    print(e)  # Expected W:(0123...), found '1'  (at char 3), (line:1, col:4)
# 直接看报错信息会让人很懵逼,估计有人认为结果应该是['123', '123123']
# 我们之前也指定了max,但当时只有一个Word,我们说Word(nums, max=3)要求解析的字符串的个数是1到3,并且全是数字
# 但这里是两个Word,那么第一个Word(nums, max=3)要求开头必须是数字,范围是1到3个。如果前三个是数字,那么第四个就不能是数字了
"""
Word(nums, max=3):解析的字符串全是数字,最多3个。不可以可以出现第四个字符,因为只有一个Word
Word(nums, max=3) + Word(nums):解析的字符串开头最多3个是数字,可以出现第四个字符,因为有两个Word
                               但是第四个字符不能再是数字了,因为第一个Word里面是数字
                               
因此即便指定了max,同样要求后一个的Word里面的所有字符不能是前一个Word里面的所有字符的子集
比如这里的例子:第一个Word要求是第4个字符不可以是数字,但是第二个Word要求匹配数字,所以出现了矛盾,因此无论什么字符都是无法解析的    
"""
# 注意:我们上面说的数字指的是nums,也就是0123456789、即所有的数字
# 但如果是部分数字就不一定了
match = Word("12345", max=3) + Word("612345")
print(match.parseString("1236123123"))  # ['123', '6123123']
"""
我们看到这里就可以匹配了,因为Word("12345", max=3)要求开头最多三个字符必须是数字,并且都是"12345"中的一个
第四个字符则不能是"12345"中的任意一个,因此这里的6是符合要求的。而第二个Word里面也存在着"6"这个字符,因此它不是上一个Word的子集也侧面说明了上面的那个结论。

形如:Word(..., max=n) + Word(...),那么第n+1个字符不可以在第一个Word里面指定的所有字符里面
比如:
    如果第一个Word指定的是nums,那么第n+1个字符就不能是数字,如果是"012345678",那么第n+1个字符可以是数字,但如果是数字的话,只能是9
    如果第一个Word指定的是nums + alphas,那么第n+1个字符则不能是"字母"或者"数字"
总之就是:
    第n+1个字符不可以在第一个Word里面指定的所有字符里面
"""
# 同理下面两个则无需解释了,只要是1到3个都是可以的
print(match.parseString("126123123"))  # ['12', '6123123']
print(match.parseString("16123123"))  # ['1', '6123123']

# 再来看个栗子
# 第二个Word的开头是字母,所以它是正确可以解析某些字符串的
match = Word(nums, max=3) + Word(alphas, nums)
print(match.parseString("1a123"))  # ['1', 'a123']
print(match.parseString("12a123"))  # ['12', 'a123']
print(match.parseString("123a123"))  # ['123', 'a123']
try:
    print(match.parseString("1234a123"))
except Exception as e:
    print(e)  # Expected W:(0123...), found '4'  (at char 3), (line:1, col:4)
"""
前三个很好理解,但是第四个"1234a123"报错了,只是它有两种理解方式,一种是正确的,一种是不正确,尽管都能解释的通。
第一种理解:
    Word(nums, max=3)匹配到了123,那么Word(alphas, nums)应该匹配4a123,但是开头不是字母,所以报错
    尽管这么理解可以解释的通,但是它的理解方式是错误的
第二种理解:
    Word(nums, max=3)匹配到了123,但是后面的字符还是nums,这是不允许的,所以报错
    这种理解方式是对的
"""

# 搭配parseAll
print(match.parseString("123a456a"))  # ['123', 'a456']
try:
    print(match.parseString("123a456a", parseAll=True))
except Exception as e:
    print(e)  # Expected end of text, found 'a'  (at char 7), (line:1, col:8)
"""
parseAll=True要求整体全部匹配,那么第二个Word就必须和123后面的所有内容完全匹配,也就是"a456a"
但是结尾的"a"显然不是nums,那么就报错了。提示我们:期待字符串到头,但是看到了字符"a"
但是不加parseAll=True是没有问题的,结尾的"a"不符合,就舍弃不管了
"""

Word的其它参数:exact

from pyparsing import Word, alphas

# exact表示去固定长度的字符串,exact=4,略微等价于min=4, max=4
match = Word(alphas, exact=4)
print(match.parseString("abcdef"))  # ['abcd']
"""
但是我们看到exact=4和min=4, max=4还是有区别的
exact=4,表示匹配4个,但是解析的字符串多于4个也可以
但是min=4, max=4要求解析的字符串则必须是4个,否则报错
"""

# 但是对于exact来说,因为是固定取3个,所以两个Word里面的所有字符相同也是可以的
# 此时不需要满足"不能是子集"的关系
match = Word(alphas, exact=3) + Word(alphas, exact=3)
print(match.parseString("abcdefg"))  # ['abc', 'def']

Word的其它参数:asKeyword

from pyparsing import Word


# 这个参数非常的方便,它表示是否作为关键字,默认是False
# 如果为True表示作为关键字
match = Word("while", asKeyword=True)
# 作为关键字,那么就不能有字符和它挨着,必须要存在一个或多个空格
print(match.parseString("while"))  # ['while']
print(match.parseString("while aaa"))  # ['while']
try:
    match.parseString("while1")
except Exception as e:
    print(e)  # Expected W:(whil...), found 'w'  (at char 0), (line:1, col:1)
"""
这里要求匹配的while是一个关键字,那么就不能有字符和它挨着
"""
try:
    match.parseString("whilE")
except Exception as e:
    print(e)  # Expected W:(whil...), found 'w'  (at char 0), (line:1, col:1)
# 所以我们看到,作为关键字之后,就必须叫while,某个字符也不可以大写

# 但是我们看到它有一个局限性,那就是个数不够也是能匹配的
# 只是顺序不能错,比如可以是:w、wh、whi、whil
print(match.parseString("w"))  # ['w']
print(match.parseString("wh"))  # ['wh']
print(match.parseString("whi"))  # ['whi']
print(match.parseString("whil"))  # ['whil']

Word的最后一个参数:excludeChars

# 这个很好理解,就是不要的字符
# Word("abcdef", excludeChars="ab") <==> Word("cdef")
# 这个参数最大的作用就是方便,比如:我们匹配数字,但是不能有9
# 我们可以Word("012345678"),也可以Word(nums, excludeChars="9")
# 这个excludeChars会同时作用于initChars和bodyChars
# Word("abcdef", "aaaac", excludeChars="ab") <==> Word("cdef", "c")

Literal对象

问题来了,我希望实现这么一个功能,要求解析的字符串必须是"select xxx"这种格式该怎么做呢?

from pyparsing import Word, alphas, alphanums

match = Word("select") + Word(alphas + "_", alphanums + "_")
print(match.parseString("select abc"))  # ['select', 'abc']
print(match.parseString("selectttt abc"))  # ['selectttt', 'abc']
print(match.parseString("selectAbc"))  # ['select', 'Abc']

但是我们看到这样显然是不行的,因为Word("select")要求字符只要在"select"里面即可,所以select、seletc、seleeeect都是合法的,并且它也没有考虑到空格的问题。可能有人觉得可以通过asKeyword来实现,但是我们说它有局限性。

于是Literal就登场了,Literal("select"),它要求的是字符串必须是"select",不能多也不能少,等于就是普通的字面量。所以不像Word,Literal这个类的__init__函数里面除了self,只接收一个参数,就是其对应的字符。

from pyparsing import Word, alphas, alphanums,Literal

# Literal("select ")表示开头必须是"select "
match = Literal("select ") + Word(alphas + "_", alphanums + "_")

print(match.parseString("select name"))  # ['select ', 'name']
try:
    print(match.parseString("selectt name"))
except Exception as e:
    print(e)  # Expected "select ", found 's'  (at char 0), (line:1, col:1)

try:
    print(match.parseString("selectName"))
except Exception as e:
    print(e)  # Expected "select ", found 's'  (at char 0), (line:1, col:1)

所以开头必须出现"select ",因此"select         name"也是成立的,只要select后面至少有一个空格即可, 因为Literal里面的"select"后面是有一个空格的。至于"selecttt name"、"selectName"之类的都是不合法的。

下面我们尝试解析一个稍微复杂点的字符串,比如解析: "select xxx from xxx"这样的字符串

from pyparsing import Word, alphanums, alphas, Literal


# 首先Literal("select ")里面的"select"后面必须有空格,否则selectname这种也是合法的
# 但selectname这种显然不合法,应该select name
match = (
    Literal("select ") + Word(alphas + "_", alphanums + "_")
    # 关键是这里的Literal("from"),它的两边是否需要空格呢?
    # 我们先都不加空格
    + Literal("from") + Word(alphas + "_", alphanums + "_")
)

print(match.parseString("select name from table"))  # ['select ', 'name', 'from', 'table']
print(match.parseString("select name fromtable"))  # ['select ', 'name', 'from', 'table']
"""
我们看到显然"from"的右边是需要一个空格的,否则fromtable也是合法的
但是左边需不需要空格呢?事实上是不需要的,而且也不能要
因为无论是Word还是Literal都会从不是空格的字符开始匹配,如果Literal里面的第一个参数是空格不就矛盾了吗

但是可能有人会好奇,既然左边不能有空格
那么是不是意味着:select namefrom table这种也是合法的呢?
我们说由于Word是贪婪匹配,那么第一个Word不仅会匹配name还会把name后面的from也一起匹配
所以等到第二个Literal的时候,就没有东西了
"""

match = (
    Literal("select ") + Word(alphas + "_", alphanums + "_")
    # 右边加上空格
    + Literal("from ") + Word(alphas + "_", alphanums + "_")
)
try:
    print(match.parseString("select namefrom table"))
except Exception as e:
    # namefrom都被第一个Word匹配上了,所以第二个Literal找不到了
    print(e)  # Expected "from ", found 't'  (at char 16), (line:1, col:17)

# 但这样是可以的,不过这样的SQL语句是不合法的
# 当然这涉及到SQL的语法解析部分了,我们只要格式正确就行,至于关键字是否冲突我们先不管
print(match.parseString("select namefrom from from"))  # ['select ', 'namefrom', 'from ', 'from']

Combine对象

Combine用于将多个Word或者Literal组合起来

from pyparsing import Word, Literal, Combine, nums


ip_part = Word(nums, max=3)
match1 = ip_part + Literal(".") + ip_part + Literal(".") + ip_part + Literal(".") + ip_part
match2 = Combine(ip_part + Literal(".") + ip_part + Literal(".") + ip_part + Literal(".") + ip_part)
print(match1.parseString("10.1.255.105"))  # ['10', '.', '1', '.', '255', '.', '105']
print(match2.parseString("10.1.255.105"))  # ['10.1.255.105']

我们看到如果使用Combine,那么会将整体作为一个部分进行匹配,其实就是将匹配的各个部分自动帮你用空字符串拼接了起来。

from pyparsing import Word, Combine, nums, alphas

# a1b2cc3ddd4
part = Word(alphas, max=3) + Word(nums, max=3)
match = Combine(part * 3, "--")  # "--"表示指定连接符为"--"
# part * 3 == part + part + part
# Combine(part * 3) = Combine(
#           Word(alphas, max=3) + Word(nums, max=3) +
#           Word(alphas, max=3) + Word(nums, max=3) +
#           Word(alphas, max=3) + Word(nums, max=3)
#           )
print(match.parseString("aaa123bb123cc12"))  # ['aaa--123--bb--123--cc--12']

但是我们上面还有不完美的地方,那就是每个ip的部分最大是255,但我们这里999.999.999.999也是能匹配成功的。如果是正则会使用|分情况讨论,同理对于pyparsing也是如此。

from pyparsing import Word, nums, alphas

# 这个|和正则是类似的,|是针对两边的全部部分,如果想要限定范围需要使用()括起来
match = (Word(alphas)|Word(nums) + Word("+-*/"))
# 表示要么匹配Word(alphas),要么匹配Word(nums) + Word("+-*/")
print(match.parseString("abc+"))  # ['abc']
print(match.parseString("abc123+"))  # ['abc']
print(match.parseString("123+"))  # ['123', '+']
原文地址:https://www.cnblogs.com/traditional/p/11870163.html