pyparsing 空格匹配问题

7 投票
1 回答
6270 浏览
提问于 2025-04-30 01:42

我尝试使用pyparsing来解析robotframework,这是一种基于文本的领域特定语言(DSL)。它的语法大致如下(抱歉,我觉得用BNF描述有点困难)。在robotframework中,一行代码可能看起来像这样:

Library\tSSHClient    with name\tnode

\t代表制表符,在robotframework中,它会被透明地转换为两个空格(实际上,它只是调用了str.replace('\t', ' ')来替换制表符,但这会改变每行的实际长度,len('\t')是1,而len(' ')是2)。在robot中,两个或更多的空格和'\t'用于分隔标记,如果单词之间只有一个空格,那么这些单词就会被视为一个标记组。

Library\tSSHClient    with name\tnode

如果正确解析,实际上会被分割成以下标记:

 ['Library', 'SSHClient', 'with name', 'node']

由于“with”和“name”之间只有一个空格,所以解析器认为它们属于一个语法标记组。

这是我的代码:

ParserElement.setDefaultWhitespaceChars('\r\n\t ')
source = "Library\tSSHClient    with name\tnode"
EACH_LINE = Optional(Word(" ")).leaveWhitespace().suppress() + \
            CaselessKeyword("library").suppress() + \
            OneOrMore((Word(alphas)) + White(max=1).setResultName('myValue')) +\
            SkipTo(LineEnd())

res = EACH_LINE.parseString(source)
print res.myValue

问题:

1) 我已经设置了空格。如果我想精确匹配两个或更多的空格,或者一个或多个制表符,我认为代码应该像这样: White(ws=' ', min=2)| White(ws='\t', min=1) 但这会失败,所以我无法指定空格的值吗?

2) 有没有办法获取匹配结果的索引?我尝试了setParseAction,但似乎通过这个回调我无法获取索引。我需要开始和结束的索引来高亮显示单词。

3) LineStart和LineEnd是什么意思?我打印这些值,似乎它们只是普通字符串,我是否需要在行的前面写一些东西,比如: LineStart() + balabala... + LineEnd()?

谢谢,不过有个限制,我无法将'\t'替换为' '

from pyparsing import *

source = "Library\tsshclient\t\t\twith name    s1"

value = Combine(OneOrMore(Word(printables) | White(' ', max=1) + ~White()))  #here it seems the whitespace has already been set to ' ', why the result still match '\t'?

linedefn = OneOrMore(value)

res = linedefn.parseString(source)

print res

我得到了

['Library sshclient', 'with name', 's1']

但我期望的是 ['Library', 'sshclient', 'with name', 's1']

暂无标签

1 个回答

7

每当解析的内容中出现空格时,我总是感到不安。不过,考虑到你只允许单个空格,这样应该是可以处理的。我用以下表达式来定义那些可能包含单个空格的值:

# each value consists of printable words separated by at most a 
# single space (a space that is not followed by another space)
value = Combine(OneOrMore(Word(printables) | White(' ',max=1) + ~White()))

完成这个后,一行就可以是一个或多个这些值:

linedefn = OneOrMore(value)

按照你的例子,包括调用 str.replace 将制表符替换为两个空格,代码看起来是这样的:

data = "Library\tSSHClient    with name\tnode"

# replace tabs with 2 spaces
data = data.replace('\t', '  ')

print linedefn.parseString(data)

结果是:

['Library', 'SSHClient', 'with name', 'node']

为了获取原始字符串中任何值的起始和结束位置,可以把这个表达式放在新的 pyparsing 辅助方法 locatedExpr 中:

# use new locatedExpr to get the value, start, and end location 
# for each value
linedefn = OneOrMore(locatedExpr(value))('values')

如果我们解析并输出结果:

print linedefn.parseString(data).dump()

我们得到:

- values: 
  [0]:
    [0, 'Library', 7]
    - locn_end: 7
    - locn_start: 0
    - value: Library
  [1]:
    [9, 'SSHClient', 18]
    - locn_end: 18
    - locn_start: 9
    - value: SSHClient
  [2]:
    [22, 'with name', 31]
    - locn_end: 31
    - locn_start: 22
    - value: with name
  [3]:
    [33, 'node', 37]
    - locn_end: 37
    - locn_start: 33
    - value: node

LineStart 和 LineEnd 是 pyparsing 的表达式类,它们的实例应该匹配行的开始和结束。LineStart 一直比较难处理,但 LineEnd 相对比较可预测。在你的情况下,如果你一次只读取和解析一行,那么你不需要它们——只需定义你期望的行内容。如果你想确保解析器处理了整个字符串(而不是因为遇到不匹配的字符而提前停止),可以在解析器的末尾加上 + LineEnd()+ StringEnd(),或者在调用 parseString() 时添加参数 parseAll=True

编辑:

很容易忘记 pyparsing 默认会调用 str.expandtabs——你需要通过调用 parseWithTabs 来禁用这个。这样做,以及明确禁止在值之间使用制表符,可以解决你的问题,并保持值的字符计数正确。请看下面的更改:

from pyparsing import *
TAB = White('\t')

# each value consists of printable words separated by at most a 
# single space (a space that is not followed by another space)
value = Combine(OneOrMore(~TAB + (Word(printables) | White(' ',max=1) + ~White())))

# each line has one or more of these values
linedefn = OneOrMore(value)
# do not expand tabs before parsing
linedefn.parseWithTabs()


data = "Library\tSSHClient    with name\tnode"

# replace tabs with 2 spaces
#data = data.replace('\t', '  ')

print linedefn.parseString(data)


linedefn = OneOrMore(locatedExpr(value))('values')
# do not expand tabs before parsing
linedefn.parseWithTabs()
print linedefn.parseString(data).dump()

撰写回答