使用 ruamel.yaml 将 YAML 转储到字符串的最佳方法(不是流式传输)

Best way to use ruamel.yaml to dump YAML to string (NOT to stream)

过去,我使用 ruamel.yaml 的向后兼容部分做了类似 some_fancy_printing_loggin_func(yaml.dump(...), ...) 的事情,但我想将我的代码转换为使用最新的 API 以便我可以利用一些新的格式设置。

但是,我讨厌我必须指定一个流 ruamel.yaml.YAML.dump() ...我不希望它直接写入一个流;我只是想让它 return 输出给调用者。我错过了什么?

PS:我知道我可以做下面这样的事情,当然我正在努力避免它。

f = io.StringIO()
yml.dump(myobj, f)
f.seek(0)
my_logging_func(f.read())

我不确定您是否真的遗漏了什么,如果有的话,可能是如果您正在使用流,您应该——最好——继续使用流。然而,ruamel.yaml 和 PyYAML 的许多用户似乎都忽略了这一点,因此他们这样做了:

print(dump(data))

而不是

dump(data, sys.stdout)

前者对于 (PyYAML) 文档中使用的非现实数据可能没问题,但它会导致对真实数据的坏习惯。

最好的解决方案是让您的 my_logging_func() 面向流。这可以例如做如下:

import sys
import ruamel.yaml

data = dict(user='rsaw', question=47614862)

class MyLogger:
    def write(self, s):
        sys.stdout.write(s.decode('utf-8'))

my_logging_func = MyLogger()
yml = ruamel.yaml.YAML()
yml.dump(data, my_logging_func)

给出:

user: rsaw
question: 47614862

但请注意 MyLogger.write() 被调用多次(在本例中为八次),如果您需要一次处理一行,则必须进行行缓冲。

如果您确实需要将 YAML 处理为 bytesstr,您可以安装适当的插件(ruamel.yaml.bytesruamel.yaml.string)并执行:

yaml = ruamel.yaml.YAML(typ=['rt', 'string'])
data  = dict(abc=42, help=['on', 'its', 'way'])
print('retval', yaml.dump_to_string(data))

或处理 yaml.dump_to_string(data) 的结果,它等价于 yaml.dumps(data)as you see necessary. Replacingstringwithbytesin the above doesn't decode the UTF-8 stream back tostr` 但将其保留为字节.

总有一些情况需要一些意想不到的东西(即使这与通常情况下的最佳实践相矛盾)。这是一个例子:

在这种情况下,我需要 yaml 作为字符串。不,使用文件而不是字符串不会在这里削减它,因为我会多次创建这个 input_yaml 因为我需要多次执行这个 pypandoc 转换。创建单个文件会更加混乱!

output = pypandoc.convert_text(input_yaml, to='markdown_strict', format='md', filters=filters)

input_yaml = """
---
bibliography: testing.bib
citation-style: ieee-with-url.csl
nocite: |

 @*
...
"""

正因为如此,我不得不回到 PyYAML。它让我

yaml_args = {'bibliography':'testing.bib', 'citation-style':'ieee-with-url.csl'}

test = yaml.dump(yaml_args, default_flow_style=False)
test = "---\n"+ test + "nocite: | \n\n @* \n...\n"
output = pypandoc.convert_text(test, to='markdown_strict', format='md', filters=filters)

笨拙但在这种情况下我能找到最好的。

这个答案(围绕 ruamel.yaml 的一个小包装),在我如此频繁地需要此功能后被放入 pip module here

TLDR

pip install ez_yaml

import ez_yaml

ez_yaml.to_string(obj=your_object    , options={})

ez_yaml.to_object(file_path=your_path, options={})
ez_yaml.to_object(string=your_string , options={})

ez_yaml.to_file(your_object, file_path=your_path)

原始问题的 Hacky/复制粘贴解决方案

def object_to_yaml_str(obj, options=None):
    # 
    # setup yaml part (customize this, probably move it outside this def)
    # 
    import ruamel.yaml
    yaml = ruamel.yaml.YAML()
    yaml.version = (1, 2)
    yaml.indent(mapping=3, sequence=2, offset=0)
    yaml.allow_duplicate_keys = True
    # show null
    def my_represent_none(self, data):
        return self.represent_scalar(u'tag:yaml.org,2002:null', u'null')
    yaml.representer.add_representer(type(None), my_represent_none)
    
    # 
    # the to-string part
    # 
    if options == None: options = {}
    from io import StringIO
    string_stream = StringIO()
    yaml.dump(obj, string_stream, **options)
    output_str = string_stream.getvalue()
    string_stream.close()
    return output_str

原始答案(如果你想自定义更多config/options)

import ruamel.yaml
from io import StringIO
from pathlib import Path

# setup loader (basically options)
yaml = ruamel.yaml.YAML()
yaml.version = (1, 2)
yaml.indent(mapping=3, sequence=2, offset=0)
yaml.allow_duplicate_keys = True
yaml.explicit_start = False
# show null
def my_represent_none(self, data):
    return self.represent_scalar(u'tag:yaml.org,2002:null', u'null')
yaml.representer.add_representer(type(None), my_represent_none)

# o->s
def object_to_yaml_str(obj, options=None):
    if options == None: options = {}
    string_stream = StringIO()
    yaml.dump(obj, string_stream, **options)
    output_str = string_stream.getvalue()
    string_stream.close()
    return output_str

# s->o
def yaml_string_to_object(string, options=None):
    if options == None: options = {}
    return yaml.load(string, **options)

# f->o
def yaml_file_to_object(file_path, options=None):
    if options == None: options = {}
    as_path_object = Path(file_path)
    return yaml.load(as_path_object, **options)

# o->f
def object_to_yaml_file(obj, file_path, options=None):
    if options == None: options = {}
    as_path_object = Path(Path(file_path))
    with as_path_object.open('w') as output_file:
        return yaml.dump(obj, output_file, **options)

# 
# string examples
# 
yaml_string = object_to_yaml_str({ (1,2): "hi" })
print("yaml string:", yaml_string)
obj = yaml_string_to_object(yaml_string)
print("obj from string:", obj)

# 
# file examples
# 
obj = yaml_file_to_object("./thingy.yaml")
print("obj from file:", obj)
object_to_yaml_file(obj, file_path="./thingy2.yaml")
print("saved that to a file")

咆哮

感谢 Mike Night 解决了原来的“我只想 return 输出给调用者”,并调用 Anthon 的 post 无法回答问题。我会做的更进一步: Anthon 你的模块很棒;往返旅行令人印象深刻,并且是有史以来为数不多的旅行之一。但是,(这种情况经常发生在 Stack Overflow 上)使其他人的代码运行时高效并不是作者的工作。明确的权衡很好,作者应该帮助人们理解他们选择的后果。添加警告,包括名称中的“慢”等,可能会非常有帮助。但是,ruamel.yaml 文档中的方法;创建一个完整的继承 class,不是“明确的”。它们是累赘和混淆的,使得其他人难以执行并且耗费时间来理解什么以及为什么存在这些额外的代码。

许多用户理所当然地不关心运行时性能。在没有 YAML 的情况下,我的程序的运行时间为 2 周。一个 500,000 行的 yaml 文件在几秒钟内被读取。这 2 周和几秒钟都与项目无关,因为它们是 CPU 时间,项目完全按工时计费。

YAML 代码已经是一个字符串对象,因为正在对它执行其他操作。强制将其放入流中实际上会导致更多开销。消除对 YAML 字符串形式的需求将涉及重写几个主要库和可能需要数月的努力;在这种情况下使流成为一个非常不切实际的选择。

假设甚至可以将其保持为流,并且该项目按 CPU 时间而不是工时计费;优化一个 500,000 行的 yaml 文件作为字符串将使效率提高≤0.0001%。花在找出这个问题的答案上的额外时间,以及其他人花在了解变通方法上的时间,本可以用来提高每秒调用 100 次的其中一个 c 函数的效率。因此,即使我们确实关心 CPU 时间,特定方法仍然不是一个有用的选择。

A post 忽略这个问题同时还建议用户可能花费大量时间重写他们的应用程序并不是一个答案。尊重他人,假设他们大体上知道自己在做什么并且知道其他选择。然后提供可能更有效的方法将得到赞赏而不是拒绝。

[吐槽完毕]