使用 pyparsing 解析字节字符串

Parsing a byte string with pyparsing

我有来自网络的数据包,我想使用 pyparsing 来检测消息并提取不同数据包类型中的特定数据。当然,来自网络的是字节串的形式,如下例所示。

b'\x03\xff*******************************************************************'

其中 * 代表任意字符。请注意,没有使用特定的编码,例如 Unicode.

我可以将 pyparsing 与字节字符串一起使用,当我明确指定要查找的内容时似乎工作正常,例如:

expr = Suppress(b'\x03\xff')

现在我想让它找到一个 20 字节的序列,例如放在 Suppress(b'\x03\xff') 之后,它们可以是任何东西。如果可以使用 DOTALL 标志,我想使用像 Regex('.{20}') 这样的表达式,但无论如何我得到了错误: TypeError: cannot use a string pattern on a bytes-like object

那么我怎样才能检测到这个 20 字节的序列呢?

附录

尝试@FMc 提议:

from pyparsing import *
expr = Suppress(b'\x03\xff') + Regex(b'.{20}')
line = b'\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff--4353425352FGDSGSFDGBFSDBGfdeGRES'
print(expr.parseString(line, parseAll=False).dump())

给予

Traceback (most recent call last):
  File "<input>", line 2, in <module>
  File "lib\site-packages\pyparsing\core.py", line 2384, in __init__
    self.mayReturnEmpty = self.re_match("") is not None
TypeError: cannot use a bytes pattern on a string-like object

我不确定,但我怀疑 pyparsing 是 文本解析器,而不是字节解析器。我在文档中没有提到“二进制”或“字节”。 docs中的第一段:

This document provides how-to instructions for the pyparsing library, an easy-to-use Python module for constructing and executing basic text parsers.

当我在 pyparsing 代码库的失败行之前添加一个 print([expr]) 时,打印了很多内容,给我的印象是 pyparsing 的代码库中包含 text-based 假设。这是全部爆炸之前的输出片段:

[<SP><TAB>]
[{{{~{","} ~{LineEnd}} W:(0123...)} [<SP><TAB>]}]
[{{{{~{","} ~{LineEnd}} W:(0123...)} [<SP><TAB>]}}...]
[{quotedString using single or double quotes | commaItem}]
[","]
[{Suppress:(",") [{quotedString using single or double quotes | commaItem}]}]
[b'\x03\xff']
Traceback (most recent call last):
  File "x.py", line 3, in <module>
    expr = Suppress(b'\x03\xff') + Regex(b'.{20}')
  File "/Users/.../lib/python3.7/site-packages/pyparsing.py", line 5100, in __init__
    super(TokenConverter, self).__init__(expr)  # , savelist)
  File "/Users/.../lib/python3.7/site-packages/pyparsing.py", line 4453, in __init__
    self.mayIndexError = expr.mayIndexError
AttributeError: 'bytes' object has no attribute 'mayIndexError'

最初,我有一个想法使用编码方案(例如,十六进制)来解析字节 与pyparsing。但经过一些初步实验后,我意识到这条路似乎 复杂——而且可能行不通。 pyparsing 库与 正则表达式,在其 API 及其底层实现中。例如,这个 无法按预期工作:

from pyparsing import Regex, ParseException

# Your input converted to hex.
line = b'\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff--4353425352FGDSGSFDGBFSDBGfdeGRES'
hex_line = line.hex()

# An easy regex, naively converted to hex.
pattern = r'.{20}'
hex_pattern = pattern.encode().hex() # 2e7b32307d
rgx = Regex(hex_pattern)

# It doesn't work: the regex syntax has been lost. We end up searching for the
# literal 2e7b32307d, which isn't found.
try:
    print(rgx.parseString(hex_line, parseAll = False).dump())
except ParseException as e:
    print(e)

# A hex-minded regex for 20 arbitrary ACSII characters would be this:
hpattern = r'.{40}'
rgx = Regex(hpattern)

# This works.
print(rgx.parseString(hex_line, parseAll = False).dump())

但是那个例子太简单了。许多正则表达式概念不太方便 以十六进制表示:

\d+            # Regex
(?:3[0-9])+    # Hex-centric regex? No thanks.

除非你的需求很简单(而且,如果是这样,为什么还要使用 pyparsing),这个 道路似乎很艰难。再一次,不要忘记许多 pyparsing 的 API 元素在引擎盖下作为普通正则表达式实现。

看来 pyparsing 只适用于文本 (str) 而不是 bytes

一个想法是使用 latin-1(普通编码)将字节转换为“”“文本”””,然后使用它解析为文本:

from pyparsing import *
expr = Suppress('\x03\xff') + Regex('.{20}')
line = b'\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff--4353425352FGDSGSFDGBFSDBGfdeGRES'
for tok in expr.parseString(line.decode('latin-1'), parseAll=False):
    print(tok.encode('latin-1'))

输出:

$ python3 t.py 
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff--435342535'

为了在执行之前进行测试,我尝试了 Anthony Sottile 的想法,在要测试的行中使用完整的字节谱,但出现以下错误:

from pyparsing import *
expr = Suppress('\x03\xff') + Regex('.{20}')
line = b'\x03\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
for tok in expr.parseString(line.decode('latin-1'), parseAll=False):
    print(tok.encode('latin-1'))

Traceback (most recent call last):
  File "<input>", line 4, in <module>
  File "\lib\site-packages\pyparsing.py", line 1955, in parseString
    raise exc
  File "\lib\site-packages\pyparsing.py", line 3342, in parseImpl
    raise ParseException(instring, loc, self.errmsg, self)
pyparsing.ParseException: Expected Re:('.{20}'), found '\x00'  (at char 2), (line:1, col:3)

要使其正常工作,我需要以下内容:

from pyparsing import *
import re
expr = Suppress('\x03\xff') + Regex(re.compile(r'.{256}', re.DOTALL))
line = b'\x03\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
for tok in expr.parseString(line.decode('latin-1'), parseAll=False):
    print(tok.encode('latin-1'))
    print(len(tok), len(tok.encode('latin-1')))

输出:

b'\x00\x01\x02\x03\x04\x05\x06\x07\x08     \n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb'
256 256

但是打印的字符串以 \xfb 结尾,我想知道为什么长度正好是 256。看起来 TAB=\x09 被转换为 5 个空格,所以转换在 4 个字符之前停止\xff。 我发现 parseStringTAB 转换为空格。所以通过添加 parseWithTabs() 一切正常:

from pyparsing import *                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
import re                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
expr = Suppress('\x03\xff') + Regex(re.compile(r'.{256}', re.DOTALL))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
line = b'\x03\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'    
expr.parseWithTabs()                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
for tok in expr.parseString(line.decode('latin-1'), parseAll=False):                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
    print(tok.encode('latin-1'))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      
    print(len(tok), len(tok.encode('latin-1')))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      

输出:

b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
256 256

附录

实际上,为了确保它在每种情况下都能正常工作,需要更新默认的空白字符,以便它们在某些情况下不会被跳过。在这种情况下,我选择使用 setDefaultWhitespaceChars()

ParserElement.setDefaultWhitespaceChars("")