如何解析包含多个文档的 YAML 文件?

Posted

技术标签:

【中文标题】如何解析包含多个文档的 YAML 文件?【英文标题】:How to parse a YAML file with multiple documents? 【发布时间】:2017-07-20 05:44:34 【问题描述】:

这是我的解析代码:

import yaml

def yaml_as_python(val):
    """Convert YAML to dict"""
    try:
        return yaml.load_all(val)
    except yaml.YAMLError as exc:
        return exc

with open('circuits-small.yaml','r') as input_file:
    results = yaml_as_python(input_file)
    print results
    for value in results:
         print value

这是一个文件示例:

ingests:
  - timestamp: 1970-01-01T00:00:00.000Z
    id: SwitchBank_35496721
    attrs:
      Feeder: Line_928
      Switch.normalOpen: 'true'
      IdentifiedObject.description: SwitchBank
      IdentifiedObject.mRID: SwitchBank_35496721
      PowerSystemResource.circuit: '928'
      IdentifiedObject.name: SwitchBank_35496721
      IdentifiedObject.aliasName: SwitchBank_35496721
    loc: vector [43.05292, -76.126800000000003, 0.0]
    kind: SwitchBank
  - timestamp: 1970-01-01T00:00:00.000Z
    id: UndergroundDistributionLineSegment_34862802
    attrs:
      Feeder: Line_928
      status: de-energized
      IdentifiedObject.description: UndergroundDistributionLineSegment
      IdentifiedObject.mRID: UndergroundDistributionLineSegment_34862802
      PowerSystemResource.circuit: '928'
      IdentifiedObject.name: UndergroundDistributionLineSegment_34862802
    path:
    - vector [43.052942000000002, -76.126716000000002, 0.0]
    - vector [43.052585000000001, -76.126515999999995, 0.0]
    kind: UndergroundDistributionLineSegment
  - timestamp: 1970-01-01T00:00:00.000Z
    id: UndergroundDistributionLineSegment_34806014
    attrs:
      Feeder: Line_928
      status: de-energized
      IdentifiedObject.description: UndergroundDistributionLineSegment
      IdentifiedObject.mRID: UndergroundDistributionLineSegment_34806014
      PowerSystemResource.circuit: '928'
      IdentifiedObject.name: UndergroundDistributionLineSegment_34806014
    path:
    - vector [43.05292, -76.126800000000003, 0.0]
    - vector [43.052928999999999, -76.126766000000003, 0.0]
    - vector [43.052942000000002, -76.126716000000002, 0.0]
    kind: UndergroundDistributionLineSegment
... 
ingests:
  - timestamp: 1970-01-01T00:00:00.000Z
    id: OverheadDistributionLineSegment_31168454

在回溯中,注意它在...开始出现问题

Traceback (most recent call last):
  File "convert.py", line 29, in <module>
    for value in results:
  File "/Users/conduce-laptop/anaconda2/lib/python2.7/site-packages/yaml/__init__.py", line 82, in load_all
    while loader.check_data():
  File "/Users/conduce-laptop/anaconda2/lib/python2.7/site-packages/yaml/constructor.py", line 28, in check_data
    return self.check_node()
  File "/Users/conduce-laptop/anaconda2/lib/python2.7/site-packages/yaml/composer.py", line 18, in check_node
    if self.check_event(StreamStartEvent):
  File "/Users/conduce-laptop/anaconda2/lib/python2.7/site-packages/yaml/parser.py", line 98, in check_event
    self.current_event = self.state()
  File "/Users/conduce-laptop/anaconda2/lib/python2.7/site-packages/yaml/parser.py", line 174, in parse_document_start
    self.peek_token().start_mark)
yaml.parser.ParserError: expected '<document start>', but found '<block mapping start>'
  in "circuits-small.yaml", line 42, column 1

我希望它能够将这些文档中的每一个都解析为一个单独的对象,也许它们都在同一个列表中,或者几乎任何其他可以与 PyYAML 模块一起使用的对象。我相信... 实际上是有效的 YAML,所以我很惊讶它不会自动处理它。

【问题讨论】:

【参考方案1】:

错误消息非常明确,文档需要以document start marker 开头。您的第一个文档没有这样的标记,尽管它有一个文档结束标记。在以... 明确结束第一个文档后,您将无法再在 PyYAML 中使用没有文档边界标记的文档,您必须明确以 --- 开头:

文件的结尾应如下所示:

    kind: UndergroundDistributionLineSegment
...
---
ingests:
  - timestamp: 1970-01-01T00:00:00.000Z
    id: OverheadDistributionLineSegment_31168454

您可以从第一个文档中省略显式文档开始标记,但您需要为每个后续文档添加一个开始标记。文档结束标记是可选的。

如果您无法完全控制输入,则使用.load_all() 是不安全的。通常没有理由冒险,您应该使用.safe_load_all() 并扩展SafeLoader 来处理您的YAML 可能包含的任何特定标签。

除此之外,您应该在文档开始指示符之前使用明确的version directive 开始您的 YAML 文档(您还应该将其添加到第一个文档中):

%YAML 1.1
---

这是为了您的 YAML 文件的未来编辑者的利益,因为您使用的是 PyYAML,它仅支持(大部分)YAML 1.1,而不支持 YAML 1.2 规范(2009 年表格)。另一种方法当然是将您的 YAML 解析器升级到例如 ruamel.yaml,这也会警告您使用不安全的 load_all()(免责声明:我是该解析器的作者)。 ruamel.yaml 不允许您在明确的文档结束标记(@flyx 指出的那样)之后有一个裸文档,这是一个 bug。

【讨论】:

您应该更改链接以指向 YAML 1.1 规范,因为在 YAML 1.2 中,在文档后缀之后有一个隐式文档是完全有效的。您链接的示例 9.3 在 1.2 规范中直接显示了这一点。 @flyx 感谢您指出这一点。更新了答案,修复 ruamel.yaml 以使其符合要求需要更多时间。我认为您可以争辩说,在 YAML 1.1 中也不需要这样做('以“---”开头的行可以用于明确表示新 YAML 文档的开头'(强调我的) ')。 YAML 1.1 中的相关产生式是l-yaml-stream,它将第一个文档之后的所有文档捕获为l-next-document,解析为l-explicit-document,并且必须---开头。【参考方案2】:

我认为你的 yaml 无效

查看示例中的第二个文档,它以 ... 开头,而不是 ---

... 
ingests:
  - timestamp: 1970-01-01T00:00:00.000Z
    id: OverheadDistributionLineSegment_31168454

【讨论】:

... 结束上一个文档。标量 ingests 然后隐式启动一个新文档。改用--- 也可以,因为它显式地启动了一个新文档,而隐式地结束了前一个文档。 附录:这仅对 YAML 1.2 有效。在 YAML 1.1 中,您确实需要 ---

以上是关于如何解析包含多个文档的 YAML 文件?的主要内容,如果未能解决你的问题,请参考以下文章

在Jackson的单个文件中反序列化来自多个YAML文档的POJO

多个 Spring 配置文件的属性解析(yaml 配置)

如何使用 spark/scala 解析 YAML

如何在不知道终端标量的映射和类型中的键的情况下使用 yaml-cpp 库解析任意 yaml 文件?

在 bash 中解析 YAML 文件中的嵌套变量

SnakeYAML:解析时如何禁用下划线剥离?