4

之前有人问过这个问题:在不破坏锚点和别名的情况下读写YAML文件?

我想知道如何用许多锚点和别名来解决这个问题?

谢谢

4

3 回答 3

11

这里的问题是, Yaml 中的锚点和别名是序列化细节,因此在解析后不是数据的一部分,因此在将数据写回 Yaml 时不知道原始锚点名称。为了在往返时保留锚名称,您需要在解析时将它们存储在某处,以便稍后在序列化时可用。在 Ruby 中,任何对象都可以有与之关联的实例变量,因此实现这一点的一种简单方法是将锚名称存储在相关对象的实例变量中。

继续前面问题中的示例,对于散列,我们可以更改重新定义的revive_hash方法,以便如果散列是锚,那么除了在@st变量中记录锚名称以便以后可以识别别名之外,我们将它添加为实例散列上的变量。

class ToRubyNoMerge < Psych::Visitors::ToRuby
  def revive_hash hash, o
    if o.anchor
      @st[o.anchor] = hash
      hash.instance_variable_set "@_yaml_anchor_name", o.anchor
    end

    o.children.each_slice(2) { |k,v|
      key = accept(k)
      hash[key] = accept(v)
    }
    hash
  end
end

请注意,这只影响作为锚点的 yaml 映射。如果您想让其他类型保留它们的锚名称,您需要查看psych/visitors/to_ruby.rb并确保在所有情况下都添加了名称。大多数类型都可以通过覆盖来包含,register但还有一些其他类型;搜索@st.

现在散列具有与其关联的所需锚名称,您需要让 Psych 在序列化它时使用它而不是对象 id。这可以通过子类化来完成YAMLTree。当YAMLTree处理一个对象时,它首先检查该对象是否已经被看到,如果有,则为其发出一个别名。对于任何新对象,它都会记录它已经看到该对象,以防以后需要创建别名object_id用作其中的键,因此您需要覆盖这两个方法来检查实例变量,并在它存在时使用它:

class MyYAMLTree < Psych::Visitors::YAMLTree

  # check to see if this object has been seen before
  def accept target
    if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
      if @st.key? anchor_name
        oid         = anchor_name
        node        = @st[oid]
        anchor      = oid.to_s
        node.anchor = anchor
        return @emitter.alias anchor
      end
    end

    # accept is a pretty big method, call super to avoid copying
    # it all here. super will handle the cases when it's an object
    # that's been seen but doesn't have '@_yaml_anchor_name' set
    super
  end

  # record object for future, using '@_yaml_anchor_name' rather
  # than object_id if it exists
  def register target, yaml_obj
    anchor_name = target.instance_variable_get('@_yaml_anchor_name') || target.object_id
    @st[anchor_name] = yaml_obj
    yaml_obj
  end
end

现在您可以像这样使用它(与上一个问题不同,在这种情况下您不需要创建自定义发射器):

builder = MyYAMLTree.new
builder << data

tree = builder.tree

puts tree.yaml # returns a string

# alternativelty write direct to file:
File.open('a_file.yml', 'r+') do |f|
  tree.yaml f
end
于 2012-11-27T16:08:42.757 回答
1

这是一个稍作修改的版本,用于更新版本的 psych gem。在它给我以下错误之前:

NoMethodError - undefined method `[]=' for #<Psych::Visitors::YAMLTree::Registrar:0x007fa0db6ba4d0>

register方法移入 的子类YAMLTree,因此现在对于马特在回答中所说的所有内容都有效:

class ToRubyNoMerge < Psych::Visitors::ToRuby
  def revive_hash hash, o
    if o.anchor
      @st[o.anchor] = hash
      hash.instance_variable_set "@_yaml_anchor_name", o.anchor
    end

    o.children.each_slice(2) { |k,v|
      key = accept(k)
      hash[key] = accept(v)
    }
    hash
  end
end

class MyYAMLTree < Psych::Visitors::YAMLTree
  class Registrar
    # record object for future, using '@_yaml_anchor_name' rather
    # than object_id if it exists
    def register target, node
      anchor_name = target.instance_variable_get('@_yaml_anchor_name') || target.object_id
      @obj_to_node[anchor_name] = node
    end
  end

  # check to see if this object has been seen before
  def accept target
    if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
      if @st.key? anchor_name
        oid         = anchor_name
        node        = @st[oid]
        anchor      = oid.to_s
        node.anchor = anchor
        return @emitter.alias anchor
      end
    end

    # accept is a pretty big method, call super to avoid copying
    # it all here. super will handle the cases when it's an object
    # that's been seen but doesn't have '@_yaml_anchor_name' set
    super
  end

end
于 2013-03-11T16:10:58.950 回答
1

我不得不进一步修改@markus 发布的代码以与 Psych v2.0.17 一起使用。

这就是我最终的结果。我希望它可以帮助其他人节省相当多的时间。:-)

class ToRubyNoMerge < Psych::Visitors::ToRuby
  def revive_hash hash, o
    if o.anchor
      @st[o.anchor] = hash
      hash.instance_variable_set "@_yaml_anchor_name", o.anchor
    end

    o.children.each_slice(2) do |k,v|
      key = accept(k)
      hash[key] = accept(v)
    end
    hash
  end
end

class Psych::Visitors::YAMLTree::Registrar
  # record object for future, using '@_yaml_anchor_name' rather
  # than object_id if it exists
  def register target, node
    @targets << target
    @obj_to_node[_anchor_name(target)] = node
  end

  def key? target
    @obj_to_node.key? _anchor_name(target)
  rescue NoMethodError
    false
  end

  def node_for target
    @obj_to_node[_anchor_name(target)]
  end

  private

  def _anchor_name(target)
    target.instance_variable_get('@_yaml_anchor_name') || target.object_id
  end
end

class MyYAMLTree < Psych::Visitors::YAMLTree
  # check to see if this object has been seen before
  def accept target
    if anchor_name = target.instance_variable_get('@_yaml_anchor_name')
      if @st.key? target
        node        = @st.node_for target
        node.anchor = anchor_name
        return @emitter.alias anchor_name
      end
    end

    # accept is a pretty big method, call super to avoid copying
    # it all here. super will handle the cases when it's an object
    # that's been seen but doesn't have '@_yaml_anchor_name' set
    super
  end

  def visit_String o
    if o == '<<'
      style = Psych::Nodes::Scalar::PLAIN
      tag   = 'tag:yaml.org,2002:str'
      plain = true
      quote = false

      return @emitter.scalar o, nil, tag, plain, quote, style
    end

    # visit_String is a pretty big method, call super to avoid copying it all
    # here. super will handle the cases when it's a string other than '<<'
    super
  end
end
于 2016-06-14T22:28:56.810 回答