从“strings / that / are / paths”数组构建XML树(在Ruby中)

时间:2009-10-01 00:42:36

标签: xml ruby parsing tree

如果你有一个字符串路径数组,在Ruby中构建XML树的最佳方法是什么?


paths = [
  "nodeA1",
  "nodeA1/nodeB1/nodeC1",
  "nodeA1/nodeB1/nodeC1/nodeD1/nodeE1",
  "nodeA1/nodeB1/nodeC2",
  "nodeA1/nodeB2/nodeC2",
  "nodeA3/nodeB2/nodeC3"
]
xml = 
<nodeA1>
    <nodeB1>
        <nodeC1>
            <nodeD1>
                <nodeE1/>
            </nodeD1>
        </nodeC1>
        <nodeC2/>
    </nodeB1>
    <nodeB2>
        <nodeC2/>
        <nodeC3/>
    </nodeB2>
</nodeA1>

我的第一个想法是将路径字符串拆分为一个数组,并将其深度和内容与前一个数组进行比较,但是如果我到达路径“nodeA1 / nodeB1 / nodeC1 / nodeD1 / nodeE1”,那么我去回到“nodeA1 / nodeB1 / nodeC2”,[1]节点是共同的祖先,但跟踪它是混乱的,至少我一直这样做。

我也想让它递归,所以我可以在自己的函数中处理每个嵌套级别,但还没有达到任何半通用的解决方案。

当你遇到这个问题时,你们常常做的任何想法或事情?

谢谢! 兰斯

3 个答案:

答案 0 :(得分:5)

REXML是你的朋友!你得到了XPath,所以请使用'em!

require 'rexml/document'

paths = [
  "nodeA1",
  "nodeA1/nodeB1/nodeC1",
  "nodeA1/nodeB1/nodeC1/nodeD1/nodeE1",
  "nodeA1/nodeB1/nodeC2",
  "nodeA1/nodeB2/nodeC2",
  "nodeA3/nodeB2/nodeC3"
]

x = REXML::Document.new
x.elements << "xml"

paths.each do |p|
  steps = p.split(/\//)
  steps.each_index do |i|
    unless REXML::XPath.first(x,"/xml/" + steps[0..i]*"/")
      REXML::XPath.first(x,"/xml/" + steps[0...i]*"/").elements << steps[i]
    end
  end
end
puts x.to_s

请注意,您的示例数据在顶层都有nodeA1和nodeA3,所以我在这里开始使用名为“xml”的根。如果“3”是拼写错误,并且nodeA1确实是您的根(正如您的示例XML输出所示),您可以删除'x.elements&lt;&lt; “xml”'行并将所有“/ xml /”更改为“/".

答案 1 :(得分:4)

这与this question非常相似。这是基于sris's answer的修改版本:

paths = [
  "nodeA1",
  "nodeA1/nodeB1/nodeC1",
  "nodeA1/nodeB1/nodeC1/nodeD1/nodeE1",
  "nodeA1/nodeB1/nodeC2",
  "nodeA1/nodeB2/nodeC2",
  "nodeA3/nodeB2/nodeC3"
]

tree = {}

paths.each do |path|
  current  = tree
  path.split("/").inject("") do |sub_path,dir|
    sub_path = File.join(sub_path, dir)
    current[sub_path] ||= {}
    current  = current[sub_path]
    sub_path
  end
end

def make_tree(prefix, node)
  tree = ""
  node.each_pair do |path, subtree| 
    tree += "#{prefix}<#{File.basename(path)}"
    if subtree.empty?
      tree += "/>\n"
    else
      tree += ">\n"
      tree += make_tree(prefix + "\t", subtree) unless subtree.empty?
      tree += "#{prefix}</#{File.basename(path)}>\n"
    end
  end
  tree
end

xml = make_tree "", tree
print xml

编辑:

这是一个使用Nokogiri构建实际XML文档的修改版本。我认为它比字符串版本更容易理解。我还删除了File的使用,因为您实际上并不需要它来满足您的需求:

require 'nokogiri'

paths = [
  "nodeA1",
  "nodeA1/nodeB1/nodeC1",
  "nodeA1/nodeB1/nodeC1/nodeD1/nodeE1",
  "nodeA1/nodeB1/nodeC2",
  "nodeA1/nodeB2/nodeC2",
  "nodeA3/nodeB2/nodeC3"
]

tree = {}

paths.each do |path|
  current  = tree
  path.split("/").each do |name|
    current[name] ||= {}
    current  = current[name]
  end
end

def make_tree(node, curr = nil, doc = Nokogiri::XML::Document.new)
  #You need a root node for the XML.  Feel free to rename it.
  curr ||= doc.root = Nokogiri::XML::Node.new('root', doc)
  node.each_pair do |name, subtree|
      child = curr << Nokogiri::XML::Node.new(name, doc)
      make_tree(subtree, child, doc) unless subtree.empty?
  end
  doc
end

xml = make_tree tree
print xml

编辑2:

是的,确实在Ruby 1.8哈希值不能保证维持插入顺序。如果这是一个问题,有办法解决它。这是一个保留顺序但不会对递归感到困扰的解决方案,并且更加简单:

require 'nokogiri'

paths = [
  "nodeA1",
  "nodeA1/nodeB1/nodeC1",
  "nodeA1/nodeB1/nodeC1/nodeD1/nodeE1",
  "nodeA1/nodeB1/nodeC2",
  "nodeA1/nodeB2/nodeC2",
  "nodeA3/nodeB2/nodeC3"
]

doc = Nokogiri::XML::Document.new
doc.root = Nokogiri::XML::Node.new('root', doc)

paths.each do |path|
  curr = doc.root
  path.split("/").each do |name|
    curr = curr.xpath(name).first || curr << Nokogiri::XML::Node.new(name, doc)
  end
end

print doc

答案 2 :(得分:1)

看起来像this question的另一个版本。

因此,您可以定义树结构并为列表中的每个字符串创建节点。然后编写一个输出方法,将树打印为xml。

如果您不想定义树结构,则必须确保列表按照示例中的顺序排序。然后遍历列表并将每一行与前一行进行比较:

  • 对于上一行中不属于当前节点的所有节点,写一个结束标记(反向顺序)
  • 对于当前行中不属于上一行的所有节点,请写一个开始标记。

此解决方案无法生成自动关闭标记(“&lt; nodeE1 /&gt;”),因为这需要与上一行和下一行进行比较。

这个解决方案不是递归的,但我认为这个问题不是一个递归的问题......(或者我只是不完全理解,为什么你想要一个递归函数)