根据名称空间前缀删除nokogiri属性

时间:2018-11-06 21:15:35

标签: ruby xml xpath namespaces nokogiri

我正在使用nokogiri解析XML文件。文件中的某些节点具有特定于名称空间的属性:

<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
    <dc:identifier id="iden" opf:scheme="ISBN">xxxx</dc:identifier>
    <dc:creator opf:role="aut" opf:file-as="Name">xxxx</dc:creator>
    <dc:date opf:event="publication">xxxx</dc:date>
    <dc:publisher>xxxx</dc:publisher>
    <meta name="cover" content="x"/>
</metadata>

我正在尝试删除所有带有“ opf”前缀的属性。我在基于部分匹配找到属性 value 时遇到了xpath解决方案,但是当它是属性名称本身的部分匹配时呢?我尝试了很多没有用的东西。我做了一件简单的事情,只是试图至少提取属性名称,但是如果这样做:

elements = @doc.at_xpath('//xmlns:metadata').children
elements.each { |el|
    el.attributes.each { |attribute|
        if attribute[1].namespace_scopes[1].prefix == "opf"
            puts attribute[0]
        end
    }   
}

我最终得到:

id
scheme
role
file-as
event
name
content

但是我只想要带有“ opf”前缀的文件(“ opf:scheme”,“ opf:role,“ opf:file-as”,“ opf:event”),这样就可以删除它们而无需碰触其他任何属性。我什至尝试通过对我知道的属性进行硬编码来强制使用它:

opf_attributes = ["opf:file-as","opf:scheme","opf:role","opf:event"]
elements.each  { |el|
    opf_attributes.each { |x|
        el.remove_attribute(x) if el[x] != nil
    }
} 

这不是解决此问题的最明智的方法,但这仍然行不通。节点没有任何反应,并且属性保持原样。 (我不知道是否值得注意,但是如果我改用remove_attr(x)方法,则会出现此错误:undefined method 'remove_attr' for #<Nokogiri::XML::Element:0x...>

所以我的问题是:
有更清晰的方法

  1. 根据部分匹配和/或名称空间前缀找到属性,然后
  2. 从包含它们的节点中删除那些属性?

2 个答案:

答案 0 :(得分:1)

节点对象具有remove方法,可将其从树中删除,因此您可以编写如下内容:

require 'nokogiri'

doc  = Nokogiri::XML(DATA)
puts '--- Before'
puts doc.to_s

doc.traverse do |node|
  next unless node.respond_to? :attributes
  node.attributes.each do |key, val|
    val.remove if val&.namespace&.prefix == 'opf'
  end
end

puts
puts '--- After'
puts doc.to_s

__END__
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
    <dc:identifier id="iden" opf:scheme="ISBN">xxxx</dc:identifier>
    <dc:creator opf:role="aut" opf:file-as="Name">xxxx</dc:creator>
    <dc:date opf:event="publication">xxxx</dc:date>
    <dc:publisher>xxxx</dc:publisher>
    <meta name="cover" content="x"/>
</metadata>

并查看以下输出:

➜  ~ ruby test.rb
--- Before
<?xml version="1.0"?>
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
    <dc:identifier id="iden" opf:scheme="ISBN">xxxx</dc:identifier>
    <dc:creator opf:role="aut" opf:file-as="Name">xxxx</dc:creator>
    <dc:date opf:event="publication">xxxx</dc:date>
    <dc:publisher>xxxx</dc:publisher>
    <meta name="cover" content="x"/>
</metadata>

--- After
<?xml version="1.0"?>
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
    <dc:identifier id="iden">xxxx</dc:identifier>
    <dc:creator>xxxx</dc:creator>
    <dc:date>xxxx</dc:date>
    <dc:publisher>xxxx</dc:publisher>
    <meta name="cover" content="x"/>
</metadata>

注意:如果您使用的Ruby版本不支持&.,则需要处理可能为nil的命名空间。

答案 1 :(得分:1)

我相信这要简单得多

doc.xpath('//@opf:*', { opf: "http://www.idpf.org/2007/opf" }).each(&:remove)

//搜索任何后代节点,@指示它必须是一个属性节点,opf:与名称空间定义({ opf: "http://www.idpf.org/2007/opf" })一起说明其具有的名称空间属于,并且*与任何名称匹配。


请注意,opf:本身并不意味着什么。 "http://www.idpf.org/2007/opf"可以,而opf只是其范围的简写。 .xpath('//@foobar:*', { foobar: "http://www.idpf.org/2007/opf" })对于您的情况同样适用。

由于您在根目录上有名称空间定义,并且在文档中没有更改,因此可以简化为

doc.xpath('//@opf:*', doc.namespaces).each(&:remove)

,但请注意,这通常并不安全(例如,可以在子节点上定义名称空间)。 doc.collect_namespaces会更安全一些,但是即使那样您也不是完全安全的(例如,如果文档的不同部分中的两个不同的URI使用相同的前缀)。除非我亲眼看到XML,并且知道在哪里以及如何定义和使用前缀,否则我会选择第一个(显式URI)。

tl; dr:前缀无意义,请参考相关的URI。