我可以在 yaml/pyyaml 中转储空白而不是 null 吗?

Posted

技术标签:

【中文标题】我可以在 yaml/pyyaml 中转储空白而不是 null 吗?【英文标题】:Can I dump blank instead of null in yaml/pyyaml? 【发布时间】:2016-09-09 01:39:33 【问题描述】:

使用 PyYAML,如果我在 dict 中读取包含空白值的文件:

test_str = '''
attrs:
  first:
  second: value2
'''

这会为键 first 返回 None

>>> data = yaml.load(test_str)
>>> data
'attrs': 'second': 'value2', 'first': None

但是在写的时候,None的值被null替换了:

>>> print(yaml.dump(data, default_flow_style=False))
attrs:
  first: null
  second: value2

有没有办法格式化转储输出以打印空白标量而不是null

【问题讨论】:

是否有特定原因需要 null (None) 成为转储 yaml 中的空行? 我们正在为非技术用户创建一个数据输入系统,因此希望界面超级简单。我们决定输入空白而不是 null,让输出尽可能多地复制输入会很棒。 【参考方案1】:

你得到null是因为dump()使用了Representer(),它是SafeRepresenter()的子类并代表None,调用了以下方法:

def represent_none(self, data):
    return self.represent_scalar(u'tag:yaml.org,2002:null',
                                 u'null')

由于字符串null 是硬编码的,因此dump() 无法更改它。

在 PyYAML 中解决此问题的正确方法是创建自己的 Dumper 子类,它具有 dump() 使用的标准 Dumper 中的 EmitterSerializerResolver,但带有子类Representer 代表 None 您想要的方式:

import sys
import yaml

from yaml.representer import Representer
from yaml.dumper import Dumper
from yaml.emitter import Emitter
from yaml.serializer import Serializer
from yaml.resolver import Resolver


yaml_str = """\
attrs:
  first:
  second: value2
"""

class MyRepresenter(Representer):
    def represent_none(self, data):
        return self.represent_scalar(u'tag:yaml.org,2002:null',
                                 u'')

class MyDumper(Emitter, Serializer, MyRepresenter, Resolver):
    def __init__(self, stream,
            default_style=None, default_flow_style=None,
            canonical=None, indent=None, width=None,
            allow_unicode=None, line_break=None,
            encoding=None, explicit_start=None, explicit_end=None,
            version=None, tags=None):
        Emitter.__init__(self, stream, canonical=canonical,
                indent=indent, width=width,
                allow_unicode=allow_unicode, line_break=line_break)
        Serializer.__init__(self, encoding=encoding,
                explicit_start=explicit_start, explicit_end=explicit_end,
                version=version, tags=tags)
        MyRepresenter.__init__(self, default_style=default_style,
                default_flow_style=default_flow_style)
        Resolver.__init__(self)

MyRepresenter.add_representer(type(None),
                              MyRepresenter.represent_none)

data = yaml.load(yaml_str)
yaml.dump(data, stream=sys.stdout, Dumper=MyDumper, default_flow_style=False)

给你:

attrs:
  first:
  second: value2

如果仅仅为了摆脱 null 而这听起来像是很多开销,那就是。您可以采取一些捷径,甚至可以尝试将替代函数移植到现有的Representer,但由于实际使用的函数在查找表(由add_representer 填充)中引用,您至少需要处理参考。

更简单的解决方案是将 PyYAML 替换为 ruamel.yaml 并使用它的往返功能(免责声明:我是该软件包的作者):

import ruamel.yaml

yaml_str = """\
# trying to round-trip preserve empty scalar
attrs:
  first:
  second: value2
"""

data = ruamel.yaml.round_trip_load(yaml_str)
assert ruamel.yaml.round_trip_dump(data) == yaml_str

除了将None 作为空标量发出之外,它还保留了映射键、cmets 和标签名称的顺序,PyYAML 都没有。 ruamel.yaml 也遵循 YAML 1.2 规范(从 2009 年开始),其中 PyYAML 使用较旧的 YAML 1.1。


ruamel.yaml 软件包可以使用 PyPI 中的 pip 安装,也可以使用基于现代 Debian 的发行版安装,也可以使用 apt-get python-ruamel.yaml

【讨论】:

【参考方案2】:

基于@Anthon 的excellent answer,我能够设计出这个解决方案:

def represent_none(self, _):
    return self.represent_scalar('tag:yaml.org,2002:null', '')

yaml.add_representer(type(None), represent_none)

根据我对PyYAML code 的理解,为现有类型添加表示器应该简单地替换现有表示器。

这是一个全局更改,这意味着所有后续转储都使用空白。如果您的程序中一些不相关的其他代码依赖None 以“正常”方式表示,例如您导入的库也使用 PyYAML,该库将不再以预期的方式/正确地工作,在这种情况下,子类化是正确的方法。

【讨论】:

哇。疯狂的技能。这完全有效!感谢 Jace,也感谢 @Anthon【参考方案3】:

只用字符串替换

print(yaml.dump(data).replace("null", ""))

【讨论】:

欢迎来到Stack Overflow。这是一个非常糟糕的建议:首先,不能保证四个字符 null 不是更大标量的一部分(例如“annulling: 42”),因此如果这四个字符是标签。第二:您添加了一个额外的换行符。第三:PyYAML 有流接口。如果您不指定流(如您所做的那样),则会创建一个缓冲区,将其流式传输,然后检索缓冲区内容。在此运行替换,然后再次将其流式传输。那很慢,内存效率低下。如果你使用类似流的东西进行后处理。【参考方案4】:

在解决@Anthon 的问题的同时扩展@Jace Browning 的答案,我们可以使用一个上下文管理器来记住None 的先前实现:

class BlankNone(Representer):
    """Print None as blank when used as context manager"""
    def represent_none(self, *_):
        return self.represent_scalar(u'tag:yaml.org,2002:null', u'')

def __enter__(self):
    self.prior = Dumper.yaml_representers[type(None)]
    yaml.add_representer(type(None), self.represent_none)

def __exit__(self, exc_type, exc_val, exc_tb):
    Dumper.yaml_representers[type(None)] = self.prior

可以这样使用:

 with BlankNone(), open(file, 'wt') as f:
        yaml.dump(hosts, f)

【讨论】:

以上是关于我可以在 yaml/pyyaml 中转储空白而不是 null 吗?的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit - 转储变量

PyYAML文档

我可以从通知中启动对话活动,使其显示在我的应用程序窗口上而不是空白背景上吗?

空白日期参数而不是使用“允许空”复选框

单个模型的 Django 转储数据?

Codeigniter 显示空白页而不是错误消息