【问题标题】:JSON to YAML: convert fields with embedded newlines "\n" to literal block "|"JSON 到 YAML:将嵌入换行符“\n”的字段转换为文字块“|”
【发布时间】:2018-04-21 06:54:08
【问题描述】:

如何将 JSON 文件(其中一些字段值是多行字符串,嵌入换行符(如“\n”)转换为 YAML,其中嵌入换行符的值且仅使用文字块表示法写入这些值。

例如给定以下 JSON:

{
   "01ea672a": {
        "summary": "A short one-line summary",
        "description": "first line\nsecond line",
        "content": "1st line\n2nd line\n"
   }
}

应该生成类似于以下 YAML 的内容(细节可能不同):

---
01ea672a:
  summary: A short one-line summary
  description: |-
    first line
    second line
  content: |
    1st line
    2nd line

我更喜欢使用脚本语言的解决方案,无论是 Python、Perl、Ruby 还是其他语言,或者使用像 Catmandu 这样的命令行转换工具。

json2yaml.com在线可以做到这一点,但我宁愿不尝试将它用于 40 MB 文件。

【问题讨论】:

    标签: json yaml file-conversion


    【解决方案1】:

    ruamel.yaml(免责声明:我是那个库的作者),已经可以了 在不丢失任何信息的情况下往返您的预期输出 (包括键顺序):

    import sys
    import ruamel.yaml
    
    yaml_str = """---
    01ea672a:
      summary: A short one-line summary
      description: |-
        first line
        second line
      content: |
        1st line
        2nd line
    """
    
    yaml = ruamel.yaml.YAML()
    yaml.explicit_start = True
    data = yaml.load(yaml_str)
    yaml.dump(data, sys.stdout)
    

    给予:

    ---
    01ea672a:
      summary: A short one-line summary
      description: |-
        first line
        second line
      content: |
        1st line
        2nd line
    

    如果您要添加:

        print(type(data['01ea672a']['description']), type(data['01ea672a']))
    

    您会看到这些分别是来自ruamel.yaml.scalarstringLiteralStringScalarCommentedMap 来自 ruamel.yaml.comments。后者您可以即时创建 通过将类型交给 JSON 加载器,它将保留键顺序,因为它的行为类似于有序字典。

    前者 加载后必须“强制”,因为json.loads 没有 'parse_string' 选项,在加载期间执行此操作的选项。 ruamel.yaml 有一个实用函数 walk_tree 可以做到这一点。

    有了这些知识,从 JSON 到 YAML 的干净转换是微不足道的:

    import sys
    import ruamel.yaml
    import json
    
    json_str = r"""
    {
        "01ea672a": {
            "summary": "A short one-line summary",
            "description": "first line\nsecond line",
            "content": "1st line\n2nd line\n"
        }
    }
    """
    
    yaml = ruamel.yaml.YAML()
    yaml.explicit_start = True
    
    data = json.loads(json_str, object_pairs_hook=ruamel.yaml.comments.CommentedMap)
    ruamel.yaml.scalarstring.walk_tree(data)  
    
    yaml.dump(data, sys.stdout)
    

    再次准确给出您期望的输出。

    【讨论】:

      【解决方案2】:

      您可以使用低级事件 API 来执行此操作。只需将 JSON 解析为 YAML 以获取事件流(YAML 作为 JSON 的超集允许这样做),然后按以下方式修改事件:

      • 使其成为 block-style 事件(JSON 风格在 YAML 中称为 flow-style)。
      • 如果是标量键,则设为普通样式。
      • 如果它是一个标量值,如果该值包含换行符,则将其设为文字样式,否则设为普通样式。

      最后,发出修改后的事件。以下是 PyYaml 的解决方案:

      import yaml, types
      from yaml.events import *
      
      events = []
      
      class Level:
        def __init__(self, is_mapping):
          self.is_mapping = is_mapping
          self.is_value = True
      
      levels = []
      
      with open("in.json", 'r') as stream:
        for event in yaml.parse(stream):
          if len(levels) > 0 and levels[-1].is_mapping:
            levels[-1].is_value = not levels[-1].is_value
          if isinstance(event, yaml.CollectionStartEvent):
            levels.append(Level(isinstance(event, MappingStartEvent)))
            event.flow_style = False
          elif isinstance(event, CollectionEndEvent):
            levels.pop()
          elif isinstance(event, ScalarEvent):
            if len(levels) > 0 and levels[-1].is_value:
              event.style = '|' if "\n" in event.value else ''
            else:
              event.style = ''
            event.implicit = (True, True)
          events.append(event)
      
      with open("out.yaml", 'w') as stream:
        yaml.emit(events, stream)
      

      注意: PyYaml 支持 YAML 1.1,在某些边缘情况下,它不是 JSON 的超集。可以肯定的是,您可以使用 ruamel 代替它,它确实实现了 YAML 1.2,但我不熟悉它的代码,这就是我提供 PyYaml 解决方案的原因。

      【讨论】:

      • 是否可以在事件到来时发出事件,而不是将整个文件存储在内存中(作为事件列表)?
      • 事实证明,它无法处理带有 TAB ("\t") 字符缩进的行的漂亮打印的多行 JSON,因为它在我的 in.json 中是偶然的...不是单行的真实 JSON 数据文件存在问题。
      【解决方案3】:

      事实证明,我能够将dnozay answer 修改为“Any yaml libraries in Python that support dumping of long strings as block literals or folded blocks?”问题。

      结果证明它比flyx answer 快一点,但您需要一些额外的技巧(借用drbild/json2yaml 的修改)来保持键的顺序。

      主要是使用Representer.add_representer

      class maybe_literal_str(str): pass
      class maybe_literal_unicode(unicode): pass
      
      def change_maybe_style(representer):
          def new_maybe_representer(dumper, data):
              scalar = representer(dumper, data)
              if isinstance(data, basestring) and "\n" in data:
                  scalar.style = '|'
              else:
                  scalar.style = None
              return scalar
          return new_maybe_representer
      
      from yaml.representer import SafeRepresenter
      
      # represent_str does handle some corner cases, so use that
      # instead of calling represent_scalar directly 
      represent_maybe_literal_str     = change_maybe_style(SafeRepresenter.represent_str)
      represent_maybe_literal_unicode = change_maybe_style(SafeRepresenter.represent_unicode)
      
      # I needed to use it in yaml.safe_dump() with older PyYAML,
      # hence explicit Dumper=yaml=SafeDumper
      yaml.add_representer(maybe_literal_str, represent_maybe_literal_str,
                           Dumper=yaml.SafeDumper)
      yaml.add_representer(maybe_literal_unicode, represent_maybe_literal_unicode,
                           Dumper=yaml.SafeDumper)
      

      为了让它工作,我必须用这两个类之一来包装字符串:

      def wrap_strings(arg):
          """Wrap {str,unicode} arguments in maybe_literal_{str,unicode}"""
          if isinstance(arg, str):
              return maybe_literal_str(arg)
          elif isinstance(arg, unicode):
              return maybe_literal_unicode(arg)
          else:
              return arg
      

      我用这个hacky函数来修改结构

      def transform(obj, leaf_callback):
          try:
              # is it dict or something like it?
              enum = obj.iteritems()
          except AttributeError:
              # if not dict-like, it is list-like object
              enum = enumerate(obj)
          for k, v in enum:
              # is value 'v' collection or scalar (leaf value)?
              if isinstance(v, (dict, list)):
                  transform(v, leaf_callback)
              else:
                  newval = leaf_callback(v)
                  if newval is not None:
                      obj[k] = newval
      

      从 JSON 到 YAML 的转换是通过以下方式完成的:

      def convert_dom(json_file, yaml_file):
          loaded_json = json.load(json_file)
          transform(loaded_json, wrap_strings)
          yaml.safe_dump(loaded_json, yaml_file,
                         explicit_start=True, # start with "---\n"
                         default_flow_style=False)
      
      
      with open('in.json', 'r') as json_file:
          with open('out.yaml', 'w') as yaml_file:
              convert_events(json_file, yaml_file)
      

      【讨论】:

      • 您指的是在 JSON 中保留键的顺序,但我无法弄清楚您在代码中的哪个位置实际执行此操作。我希望在您的最后一个示例中为json.load() 添加一个额外的参数。
      • @Anthon: 啊,真的,在我的代码中,我有 loaded_json = json.load(json_file, object_pairs_hook=MyOrderedDict),其中 MyOrderedDictcollections.OrderedDict "monkey-patched" 的子类,因此 .items() 方法返回类似列表带有.sort() 方法的对象,它什么都不做。这是必需的,因为 json.load 出于某种原因对键进行排序,据我所知,您无法关闭此行为。
      • yaml.(safe_)dump() 在转储 dict 之前对 dict 的键进行排序。我知道 Stack Overflow 上有代码可以通过更改 dicts 的表示器来防止这种情况发生,但是您的 .sort() 方法的“补丁”也可以正常工作,我从来没有想过这样做。
      猜你喜欢
      • 1970-01-01
      • 2020-01-20
      • 2018-04-12
      • 1970-01-01
      • 2020-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多