How to add a new node to XML

10,723

Solution 1

It would be helpful to distinguish between a Node (a particular piece of structured XML data at a particular place in a tree), and a "node template" which is the structure of the data.

Nokogiri (and most other XML libraries) only allow you to specify Nodes, not node templates. So when you created price = Nokogiri::XML::Node.new "price", @items, you had a particular piece of data that belongs in a particular place, but hadn't defined the place yet.

When you added it to the first <item>, you defined its place. When you added it to the second <item>, you uprooted it from its place and put it in a new place. At that point this Node appeared only in the second <item>. This continues when you add the same Node to each item, until you reach the last <item>, which is where the node stays.

Nokogiri doesn't have any way to specify a node template. What you need to do is:

@items.xpath('//items/item/manufacturer').each do |node|
  price = Nokogiri::XML::Node.new "price", @items
  price.content = "10"
  node.add_next_sibling(price)
end

Solution 2

I'd start with this:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item>
    <name>mouse</name>
    <manufacturer>Logitech</manufacturer>
  </item>
  <item>
    <name>keyboard</name>
    <manufacturer>Logitech - Inc.</manufacturer>
  </item>
</items>
EOT

doc.search('manufacturer').each { |n| n.after('<price>10</price>') }

Which results in:

puts doc.to_xml
# >> <?xml version="1.0" encoding="UTF-8"?>
# >> <items>
# >>   <item>
# >>     <name>mouse</name>
# >>     <manufacturer>Logitech</manufacturer><price>10</price>
# >>   </item>
# >>   <item>
# >>     <name>keyboard</name>
# >>     <manufacturer>Logitech - Inc.</manufacturer><price>10</price>
# >>   </item>
# >> </items>

It's easy to build upon this to insert different values for the price.

Share:
10,723
Mr. L
Author by

Mr. L

I ♡ perl

Updated on June 05, 2022

Comments

  • Mr. L
    Mr. L about 2 years

    I have a simple XML file, items.xml:

     <?xml version="1.0" encoding="UTF-8" ?>
    
    <items>
      <item>
        <name>mouse</name>
        <manufacturer>Logicteh</manufacturer>
      </item>
      <item>
        <name>keyboard</name>
        <manufacturer>Logitech - Inc.</manufacturer>
      </item>
      <item>
        <name>webcam</name>
        <manufacturer>Logistech</manufacturer>
      </item>
    </items>
    

    I am trying to insert a new node with the following code:

    require 'rubygems'
    require 'nokogiri'
    
    f = File.open('items.xml')
    @items = Nokogiri::XML(f)
    f.close
    
    price = Nokogiri::XML::Node.new "price", @items
    price.content = "10"
    
    @items.xpath('//items/item/manufacturer').each do |node|
      node.add_next_sibling(price)
    end
    
    file = File.open("items_fixed.xml",'w')
    file.puts @items.to_xml
    file.close
    

    However this code adds a new node only after the last <manufacturer> node, items_fixed.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <items>
      <item>
        <name>mouse</name>
        <manufacturer>Logitech</manufacturer>
      </item>
      <item>
        <name>keyboard</name>
        <manufacturer>Logitech</manufacturer>
      </item>
      <item>
        <name>webcam</name>
        <manufacturer>Logitech</manufacturer><price>10</price>
      </item>
    </items>
    

    Why?

  • Mr. L
    Mr. L over 13 years
    I saw the exact behavour you explained in debugger but couldn't understand it. Thank you for your explanation and your answer!
  • Phrogz
    Phrogz over 13 years
    Alternatively, you may want to simply use Node#dup to clone the node before inserting it in a new location.
  • Arup Rakshit
    Arup Rakshit over 9 years
    Nice explanation.. I was having the same issue since last 3-4 days back. Now I found out why it was happening. Thanks for the OP and you.
  • PJP
    PJP almost 8 years
    It's easier than this. See my example.