Parse data from multiple XML files and output to csv file - ruby

I've got a dozen XML files which contain the results of some wcat web performance tests. Within each XML file there is a data node that contains the names of each page requested and the average time it took to load it. I want to extract that information from each XML file and output it to a csv file so I can create a nice pretty graph in excel.
I could do the task in my main working language of C# but in an attempt to improve my scripting skills I'd like to try and do it using unix/cygwin commands or a scripting language such as Ruby.
The format of the XML file is:
<report name="wcat" version="6.3.1" level="1" top="100">
<section name="header" key="90000">
... lots of other XML junk...
<item>
<data name="reportt" >Request Name I</data>
...
<data name="avgttlb" >628</data>
</item>
<item>
<data name="reportt" >Request Name II</data>
...
<data name="avgttlb" >793</data>
</item>
... lots of other XML junk...
</section
</report>
And the csv output I need is:
Request,File 1,File 2,...,File 12
Request Name I,628,123,...,789
Request Name II,793,456,...,987
Are there any good cygwin command line utilities that could parse the XML? Or failing that is there a nice way to do it in Ruby?

What you're describing could be done in XSLT, which supports text output method, multiple input files (using the document() function), and of course templates.
I know some people find XSLT gross, but I use it all the time for this kind of thing and rather like it. Plus it's pretty much platform-independent.

Ruby has a nice parser called Nokogiri, that I really like. It supports both XML and HTML, DOM and SAX, and can build XML if that's your fancy. It's built on libxml2.
#!/usr/bin/env ruby -w
xml = <<END_XML
<report name="wcat" version="6.3.1" level="1" top="100">
<section name="header" key="90000">
<item>
<data name="reportt" >Request Name I</data>
<data name="avgttlb" >628</data>
</item>
<item>
<data name="reportt" >Request Name II</data>
<data name="avgttlb" >793</data>
</item>
</section
</report>
END_XML
require 'nokogiri'
doc = Nokogiri::XML(xml)
content = doc.search('item').map { |i|
i.search('data').map { |d| d.text }
}
content.each do |c|
puts c.join(',')
end
# >> Request Name I,628
# >> Request Name II,793
Notice that Nokogiri allows use of CSS accessors, which I'm using here, in addition to the standard XPath accessors. The actual parsing took the middle four lines.
Ruby's got a built-in CSV generator/parser, but for this quick 'n dirty example I didn't use it.

in python...
import elementTree.ElementTree
import csv
result = []
tree = elementTree.ElemenTree.parse('test.xml')
section = tree.getroot().find('section')
items = section.findall('item')
for item in items:
records = item.findall('data')
row = [rec.text for rec in records]
result.append(row)
csv.writer(file('output.csv', 'w'))
csv.writerows(result)

Related

Get low level xpath from XML with Nokogiri

I'm trying to store in an array all the unique Xpaths of the low level elements in the XML below, but like I'm doing in array a is being stored all the XML, not only the Xpath themselves. The XML has different levels of Xpath. I mean, some child elements only have 2 ancestors and others more than one.
This is the code I have.
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>
<name>Cake</name>
<ppu>0.55</ppu>
<batters>
<batter>Regular</batter>
<batter>Chocolate</batter>
<batter>Blueberry</batter>
<batter>Devil's Food</batter>
</batters>
<topping>None</topping>
<topping>Glazed</topping>
<topping>Sugar</topping>
<topping>Powdered Sugar</topping>
<topping>Chocolate with Sprinkles</topping>
<topping>Chocolate</topping>
<topping>Maple</topping>
</item>
<item>
<name>Raised</name>
<ppu>0.55</ppu>
<batters>
<batter>Regular</batter>
</batters>
<topping>None</topping>
<topping>Glazed</topping>
<topping>Sugar</topping>
<topping>Chocolate</topping>
<topping>Maple</topping>
</item>
</items>
EOT
a = []
a = doc.xpath("//*")
puts a
I'd like to store in array "a" only the unique xpaths as below:
/items/item/name
/items/item/ppu
/items/item/batters/batter
/items/item/topping
Maybe somebody could help me in how to do this.
Thanks for the help.
What you want to select is the "leaf" nodes. You can do it like so:
doc.xpath("//*[not(*)]")
This means "select all elements that don't contain elements".
If you want the XPaths, you'll need to call .path on each node. But the paths provided by Nokogiri have explicit positions (e.g. /items/item[2]/topping[4]), so you'll have to apply a regex to remove them, then remove duplicates with uniq:
doc.xpath("//*[not(*)]").map {|leaf| leaf.path.gsub(/\[.*?\]/, '') }.uniq
Output:
/items/item/name
/items/item/ppu
/items/item/batters/batter
/items/item/topping

How do I parse XML with Nokogiri css selectors, using loops?

I am trying to parse this sample XML file:
<Collection version="2.0" id="74j5hc4je3b9">
<Name>A Funfair in Bangkok</Name>
<PermaLink>Funfair in Bangkok</PermaLink>
<PermaLinkIsName>True</PermaLinkIsName>
<Description>A small funfair near On Nut in Bangkok.</Description>
<Date>2009-08-03T00:00:00</Date>
<IsHidden>False</IsHidden>
<Items>
<Item filename="AGC_1998.jpg">
<Title>Funfair in Bangkok</Title>
<Caption>A small funfair near On Nut in Bangkok.</Caption>
<Authors>Anthony Bouch</Authors>
<Copyright>Copyright © Anthony Bouch</Copyright>
<CreatedDate>2009-08-07T19:22:08</CreatedDate>
<Keywords>
<Keyword>Funfair</Keyword>
<Keyword>Bangkok</Keyword>
<Keyword>Thailand</Keyword>
</Keywords>
<ThumbnailSize width="133" height="200" />
<PreviewSize width="532" height="800" />
<OriginalSize width="2279" height="3425" />
</Item>
<Item filename="AGC_1164.jpg" iscover="True">
<Title>Bumper Cars at a Funfair in Bangkok</Title>
<Caption>Bumper cars at a small funfair near On Nut in Bangkok.</Caption>
<Authors>Anthony Bouch</Authors>
<Copyright>Copyright © Anthony Bouch</Copyright>
<CreatedDate>2009-08-03T22:08:24</CreatedDate>
<Keywords>
<Keyword>Bumper Cars</Keyword>
<Keyword>Funfair</Keyword>
<Keyword>Bangkok</Keyword>
<Keyword>Thailand</Keyword>
</Keywords>
<ThumbnailSize width="200" height="133" />
<PreviewSize width="800" height="532" />
<OriginalSize width="3725" height="2479" />
</Item>
</Items>
</Collection>
Here is my current code:
require 'nokogiri'
doc = Nokogiri::XML(File.open("sample.xml"))
somevar = doc.css("collection")
#create loop
somevar.each do |item|
puts "Item "
puts item['Title']
puts "\n"
end#items
Starting at the root of the XML document, I'm trying to go from the root "Collections" down to each new level.
I start in the node sets, and get information from the nodes, and the nodes contain elements. How do I assign the node to a variable, and extract every single layer underneath that and the text?
I can do something like the code below, but I want to know how to systematically move through each nested element of XML using loops, and output the data for each line. When finished showing text, how do I move back up to the previous element/node, whatever it may be (traversing a node in the tree)?
puts somevar.css("Keyworks Keyword").text
Nokogiri's NodeSet and Node support very similar APIs, with the key semantic difference that NodeSet's methods tend to operate on all the contained nodes in turn. For example, while a single node's children gets that node's children, a NodeSet's children gets all contained nodes' children (ordered as they occur in the document). So, to print all the titles and authors of all your items, you could do this:
require 'nokogiri'
doc = Nokogiri::XML(File.open("sample.xml"))
coll = doc.css("Collection")
coll.css("Items").children.each do |item|
title = item.css("Title")[0]
authors = item.css("Authors")[0]
puts title.content if title
puts authors.content if authors
end
You can get at any level of the tree in this way. Another example -- depth-first search printing every node in the tree (NB. the printed representation of a node includes the printed representations of its children, so the output will be quite long):
def rec(node)
puts node
node.children.each do |child|
rec child
end
end
Since you ask about this specifically, if you want to get at the parent of a given node, you can use the parent method. You may never need to though, if you can put your processing in blocks passed to each and the like on NodeSets containing subtrees of interest.

Get attribute value from XML

I have this chunk of XML:
<show name="Are We There Yet?">
<sid>24588</sid>
<network>TBS</network>
<title>The Kwandanegaba Children's Fund Episode</title>
<ep>03x31</ep>
<link>
http://www.tvrage.com/shows/id-24588/episodes/1065228407
</link>
</show>
I am trying to get "Are we there yet?" via Nokogiri. It is effectively the 'name' attribute of 'show'. I'm struggling to figure out how to parse this.
xml.at_css('show').value was my best guess but doesn't work.
You can use the following:
xml.at('//show/#name').text
which is XPath expression that returns the name attribute from the show element.
Use:
require 'nokogiri'
xml =<<EOT
<show name="Are We There Yet?">
<sid>24588</sid>
<network>TBS</network>
<title>The Kwandanegaba Children's Fund Episode</title>
<ep>03x31</ep>
<link>
http://www.tvrage.com/shows/id-24588/episodes/1065228407
</link>
</show>
EOT
xml = Nokogiri::XML(xml)
puts xml.at('show')['name']
=> Are We There Yet?
at accepts either CSS or XPath expressions, so feel free to use it for both. Use at_css or at_xpath if you know you need to declare the expression as CSS or XPath, respectively. at returns a Node, so you can simply reference the parameters of the node like you would a hash.

Using ruby/nokogiri to transform xml to another xml

I've never encountered task of transforming XML from one form to another. I hear that XSLT is just for that, but I don't want to go there. So, using only ruby and nokogiri, how can I:
remove all item elements but time from initial XML and also rename element time to HammerTime?
Initial XML:
...
<item>
<time>05.04.2011 9:53:23</time>
<iddqd>42</iddqd>
<idkfa>woot</idkfa>
</item>
<item>
...
Desired result:
...
<item>
<HammerTime>05.04.2011 9:53:23</HammerTime>
</item>
<item>
...
I figured out how to put data from XML to array using nokogiri's .xpath, but is there a way to make the desired transformation into another XML without manually having to write something like puts "<HammerTime>#{array['time']}</HammerTime>"?
Here you go:
require 'rubygems'
require 'nokogiri'
doc = Nokogiri::HTML <<-EOHTML
<html>
<body>
<item>
<time>05.04.2011 9:53:23</time>
<iddqd>42</iddqd>
<idkfa>woot</idkfa>
</item>
</body>
</html>
EOHTML
hammer = doc.at_css "time"
hammer.name = 'hammertime'
doc.css("iddqd").remove
doc.css("idkfa").remove
outfile = File.new("output.html", "w")
outfile.puts doc.to_html
outfile.close
What do you mean with
into another XML without manually having to write something like puts "<HammerTime>#{array['time']}</HammerTime>"?
If you want to transform an XML element into another in a language-independent way, you can use XSLT transformations (or stylesheet). Once you have your XSLT file you can apply it with Nokogiri's Nokogiri::XSLT::Stylesheet#apply_to.

How can I use Ruby to parse through XML easily to query and find certain tag values?

I am working with an API and want to know how I can easily search and display/format the output based on the tags.
For example, here is the page with the API and examples of the XML OUtput:
http://developer.linkedin.com/docs/DOC-1191
I want to be able to treat each record as an object, such as User.first-name User.last-name so that I can display and store information, and do searches.
Is there perhaps a gem that makes this easier to do?
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<people-search>
<people total="108" count="10" start="0">
<person>
<id>tePXJ3SX1o</id>
<first-name>Bill</first-name>
<last-name>Doe</last-name>
<headline>Marketing Professional and Matchmaker</headline>
<picture-url>http://media.linkedin.com:/....</picture-url>
</person>
<person>
<id>pcfBxmL_Vv</id>
<first-name>Ed</first-name>
<last-name>Harris</last-name>
<headline>Chief Executive Officer</headline>
</person>
...
</people>
<num-results>108</num-results>
</people-search>
This might give you a jump start:
#!/usr/bin/env ruby
require 'nokogiri'
XML = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<people-search>
<people total="108" count="10" start="0">
<person>
<id>tePXJ3SX1o</id>
<first-name>Bill</first-name>
<last-name>Doe</last-name>
<headline>Marketing Professional and Matchmaker</headline>
<picture-url>http://media.linkedin.com:/foo.png</picture-url>
</person>
<person>
<id>pcfBxmL_Vv</id>
<first-name>Ed</first-name>
<last-name>Harris</last-name>
<headline>Chief Executive Officer</headline>
</person>
</people>
<num-results>108</num-results>
</people-search>}
doc = Nokogiri::XML(XML)
doc.search('//person').each do |person|
firstname = person.at('first-name').text
puts "firstname: #{firstname}"
end
# >> firstname: Bill
# >> firstname: Ed
The idea is you're looping over the section that repeats, "person", in this case. Then you pick out the sections you want and extract the text. I'm using Nokogiri's .at() to get the first occurrence, but there are other ways to do it.
The Nokogiri site has good examples and well written documentation so be sure to spend a bit of time going over it. You should find it easy going.
nokogiri is a really nice xml parser for ruby that allows you to use xpath or css3 selectors to access your xml, but its not an xml to object mapper
there is a project called xml-mapping that does exactly this, by defining xpath expressions that should be mapped to object properties - and vice versa.
This is how I did it for the Ruby Challenge using the built-in REXML.
This is basicaly the parsing code for the whole document:
doc = REXML::Document.new File.new cia_file
doc.elements.each('cia/continent') { |e| #continents.push Continent.new(e) }
doc.elements.each('cia/country') { |e| #countries.push Country.new(self, e) }
http://nokogiri.org/ is an option you should investigate

Resources