pyparsing 空格匹配问题
我尝试使用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 个回答
每当解析的内容中出现空格时,我总是感到不安。不过,考虑到你只允许单个空格,这样应该是可以处理的。我用以下表达式来定义那些可能包含单个空格的值:
# 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()