通过 PyParsing 只解析块的相关部分

Parse only relevant parts of block via PyParsing

我正在使用 PyParsing 来解析如下所示的文件:

abc_module (
   .test_test1                       ( test_test_real1              ),
   .abc_test                         ( abc_test_real                ),
   .obs_test_one                     ( obs_test_one_real            ),
   .obs_test_two                     ( obs_test_two_real            )
);

cfg_module (
   .test_test2                       ( test_test_real2              ),
   .xyz_test                         ( xyz_test_real                )
);

xyz_module (
   .test_test2                       ( test_test_real2              ),
   .xyz_test                         ( xyz_test_real                ),
   .obs_test_three                   ( obs_test_three_real          ),
   .ahc_test                         ( ahc_test_real                ),
   .obs_test_four                    ( obs_test_four_real           )
);

我正在尝试提取每个代码块的模块名称以及以“.obs”开头的可选行,因此我希望此示例文件的 ParseResult 是:

ParseResult = [["abc_module", ".obs_test_one", ".obs_test_two"], ["cfg_module"], ["xyz_module", ".obs_test_three", ".obs_test_four"]] 

到目前为止我设法解析了代码块,但问题是我无法只提取 .obs 部分,我尝试使用 PyParsings“skipTo”方法,但我认为这不是正确的工具为此。

到目前为止我的代码:

keyword = Word(alphanums + '_' + ".")
sub_module_parser = keyword.setResultsName("module_name") + Suppress("(") + OneOrMore(
    keyword + Suppress("(" + keyword + ")" + Optional(","))).setResultsName("obs_param")

tl;dr - 最好不要尝试在一行中编写解析器,而是退后一步,先编写一个计划。

我通常会使用某种 semi-formal 符号来编写 BNF,但我会尝试一些不同的东西。 BNF 不必是 super-rigorous,只要它能阐明您对文本结构的想法即可。

这是一个非常干净的结构,没有递归嵌套,所以在实际编写任何代码之前,真的很适合花几分钟时间写下 pyparsing 代码的外观计划。它还将帮助您按逻辑组编写解析器 - 将整个解析器写在一行中没有实际好处,只会让以后更难理解和维护。

首先,在您的文本中寻找一些合乎逻辑的重复组。例如,您的子模块如下所示:

(a name starting with a dot) "(" (an identifier) ")"

每个模块都包含一个列表,用逗号分隔。

模块本身如下所示:

(an identifier) "(" (some submodules separated by commas) ")" ";" 

以这样的结构思考有助于避免一些无用的错误,例如将分隔逗号作为列表项的一部分。

这就是我们要使用的 BNF:

module IS (an identifier) "(" (some submodules separated by commas) ")" ";" 
submodule IS (a name starting with a dot) "(" (an identifier) ")" 

要转换为 pyparsing,我们需要从各个部分开始,并最终构建回整个模块。

首先是标点符号。它在解析时很有用,但之后会变得混乱,所以我们从解析结果中抑制它们:

LPAR = Suppress("(")
RPAR = Suppress(")")
SEMI = Suppress(";")

接下来是标识符。在定义诸如标识符之类的东西时,一个常见的错误是只定义一个包含所有可能找到的字母的单词,例如 Word(alphanums + '_')。这将匹配示例文本中的所有标识符,但也会匹配“___”、“123”和“2_3_4”,我认为您不想匹配这些。 Word 有一个 two-argument 形式来定义作为有效起始字符的字符,然后是有效的正文字符。假设您的标识符必须以字母开头,然后是任何其他字符 - 这至少会过滤掉没有字母的整数或标识符:

identifier = Word(alphas, alphanums+"_")

我要偷懒了,用这个技巧定义子模块标识符为:

submodule_identifier = Word(".", alphanums+"_")

看看这对子模块的“以点开头的名称”有何影响。

我们现在可以定义一个子模块表达式:

submodule = submodule_identifier + LPAR + identifier + RPAR

模块现在是:

module = identifier + LPAR + delimited_list(submodule) + RPAR

是否可以有一个空模块?如果是这样,那么子模块列表应该包含在一个 Optional 中。

module = identifier + LPAR + Optional(delimited_list(submodule)) + RPAR + SEMI

差不多就可以了。这是完整的解析器,添加了组和名称以帮助处理解析结果:

LPAR = Suppress("(")
RPAR = Suppress(")")
SEMI = Suppress(";")

identifier = Word(alphas, alphanums + "_").setName("identifier")
submodule_identifier = Word(".", alphanums + "_").setName("submodule_identifier")
submodule = Group(submodule_identifier("name") + LPAR + identifier("value") + RPAR).setName("submodule")
submodule_list = delimited_list(submodule).setName("submodule_list")
module = Group(identifier("name") + LPAR + Group(Optional(submodule_list))("submodules") + RPAR + SEMI).setName("module")

这是使用 pyparsing 3 中的新铁路图功能的此解析器的图表。

module.create_diagram("module_railroad_diag.html")

你的解析器将输出这个结构:

print(module[...].parseString(sample).dump())

[['abc_module', [['.test_test1', 'test_test_real1'], ['.abc_test', 'abc_test_real'], ...
[0]:
  ['abc_module', [['.test_test1', 'test_test_real1'], ['.abc_test', 'abc_test_real'], ...
  - name: 'abc_module'
  - submodules: [['.test_test1', 'test_test_real1'], ['.abc_test', 'abc_test_real'], ...
    [0]:
      ['.test_test1', 'test_test_real1']
      - name: '.test_test1'
      - value: 'test_test_real1'
    [1]:
      ['.abc_test', 'abc_test_real']
      - name: '.abc_test'
      - value: 'abc_test_real'
    [2]:
      ['.obs_test_one', 'obs_test_one_real']
      - name: '.obs_test_one'
      - value: 'obs_test_one_real'
    [3]:
      ['.obs_test_two', 'obs_test_two_real']
      - name: '.obs_test_two'
      - value: 'obs_test_two_real'
[1]:
  ['cfg_module', [['.test_test2', 'test_test_real2'], ['.xyz_test', 'xyz_test_real']]]
  - name: 'cfg_module'
  - submodules: [['.test_test2', 'test_test_real2'], ['.xyz_test', 'xyz_test_real']]
    [0]:
      ['.test_test2', 'test_test_real2']
      - name: '.test_test2'
      - value: 'test_test_real2'
    [1]:
      ['.xyz_test', 'xyz_test_real']
      - name: '.xyz_test'
      - value: 'xyz_test_real'
[2]:
  ['xyz_module', [['.test_test2', 'test_test_real2'], ['.xyz_test', 'xyz_test_real'], ...
  - name: 'xyz_module'
  - submodules: [['.test_test2', 'test_test_real2'], ['.xyz_test', 'xyz_test_real'], ...
    [0]:
      ['.test_test2', 'test_test_real2']
      - name: '.test_test2'
      - value: 'test_test_real2'
    [1]:
      ['.xyz_test', 'xyz_test_real']
      - name: '.xyz_test'
      - value: 'xyz_test_real'
    [2]:
      ['.obs_test_three', 'obs_test_three_real']
      - name: '.obs_test_three'
      - value: 'obs_test_three_real'
    [3]:
      ['.ahc_test', 'ahc_test_real']
      - name: '.ahc_test'
      - value: 'ahc_test_real'
    [4]:
      ['.obs_test_four', 'obs_test_four_real']
      - name: '.obs_test_four'
      - value: 'obs_test_four_real'

最后,您只想显示那些以“.obs”开头的子模块。这最好使用解析操作来完成,添加到子模块列表以过滤掉您想要的那些:

def obs_items_only(t):
    return ParseResults(item for item in t if item.name.startswith(".obs"))

submodule_list.addParseAction(obs_items_only)

添加此过滤解析操作后,结果 trim 下降为:

[['abc_module', [['.obs_test_one', 'obs_test_one_real'], ['.obs_test_two', 'obs_test_two_real']]], ...
[0]:
  ['abc_module', [['.obs_test_one', 'obs_test_one_real'], ['.obs_test_two', 'obs_test_two_real']]]
  - name: 'abc_module'
  - submodules: [['.obs_test_one', 'obs_test_one_real'], ['.obs_test_two', 'obs_test_two_real']]
    [0]:
      ['.obs_test_one', 'obs_test_one_real']
      - name: '.obs_test_one'
      - value: 'obs_test_one_real'
    [1]:
      ['.obs_test_two', 'obs_test_two_real']
      - name: '.obs_test_two'
      - value: 'obs_test_two_real'
[1]:
  ['cfg_module', []]
  - name: 'cfg_module'
  - submodules: []
[2]:
  ['xyz_module', [['.obs_test_three', 'obs_test_three_real'], ['.obs_test_four', 'obs_test_four_real']]]
  - name: 'xyz_module'
  - submodules: [['.obs_test_three', 'obs_test_three_real'], ['.obs_test_four', 'obs_test_four_real']]
    [0]:
      ['.obs_test_three', 'obs_test_three_real']
      - name: '.obs_test_three'
      - value: 'obs_test_three_real'
    [1]:
      ['.obs_test_four', 'obs_test_four_real']
      - name: '.obs_test_four'
      - value: 'obs_test_four_real'

OP 练习:使用此解析器,您将如何添加对也可以是整数的子模块值的支持,您可以将其定义为 Word(nums)?