Parsing data from XML using Ruby and Nokogiri

I have a method that parses XML into an array of hashes.

Here is the original XML:

<rowset name="skillqueue" key="queuePosition" columns="queuePosition,typeID,level,startSP,endSP,startTime,endTime">
<row queuePosition="0" typeID="20495" level="3" startSP="2829" endSP="16000" startTime="2013-03-04 16:25:50" endTime="2013-03-05 01:02:20"/>
<row queuePosition="1" typeID="19767" level="4" startSP="40000" endSP="226275" startTime="2013-03-05 01:02:20" endTime="2013-03-07 06:40:31"/>
</rowset>


Here is the final array of hashes:

[{:queuePosition=>"0", :typeID=>"20495", :level=>"3", :startSP=>"2829", :endSP=>"16000", :startTime=>"2013-03-04 16:25:50", :endTime=>"2013-03-05 01:02:20"}, {:queuePosition=>"1", :typeID=>"19767", :level=>"4", :startSP=>"40000", :endSP=>"226275", :startTime=>"2013-03-05 01:02:20", :endTime=>"2013-03-07 06:40:31"}]


Here is the method, which uses Nokogiri for parsing:

def get_training_queue(xml_data)
queue = []
xml_data.xpath("//row").each do |skill_in_queue|
skill = {}
skill_in_queue.attributes.each do |details|
skill[details[0].to_s.to_sym] = details[1].to_s
end
queue << skill
end
queue
end


This works great, but it feels a bit inelegant.

• I'm curious if there is a better way to create the hashes within the internal loop?
• I am calling to_s because I couldn't figure out a way to pull out just the key/value without doing so, and it 'feels' like I'm missing some more elegant way of doing that.

Start by using map instead of each plus <<; creating an array and then adding to it isn't very Ruby-like.

Second, Node#attributes is a hash already, so one way to go is to modify a duplicate of it, rather than "manually" copying each key/value to a new hash. Also, the keys are strings so to_s.to_sym can be replaced by just to_sym.

To do the hash-conversion, I've used the same keys.each approach as what Rails uses in its symbolize_keys! method.

def get_training_queue(xml_data)
xml_data.xpath("//row").map do |row|
skill = row.attributes.dup
skill.keys.each do |key|
skill[key.to_sym] = skill.delete(key).to_s
end
skill
end
end


Here's a different, super-brief, approach that uses the Hash[ [key, value] , ... ] syntax

def get_training_queue(xml_data)
xml_data.xpath("//row").map do |row|
Hash[ row.attributes.map { |k, v| [k.to_sym, v.to_s] } ]
end
end


Either one should give you the right result, though.

• you can safely remove that to_a :-) – tokland Mar 5 '13 at 23:54
• @tokland Good call. Wonder why I even put that in there... :P – Flambino Mar 6 '13 at 0:03

Notes:

• Use functional Enumerable#map instead of imperative pattern obj = [] + Enumerable#each + Array#<<.
• Use Kernel#Hash to build a hash from its pairs. Note that this method is very ugly and some prefer a more OOP approach, in that case check Enumerable#mash from Facets.
• Use nokogiri_node.text.
• Arguments in blocks can be unpacked.

I'd write:

require 'nokogiri'
require 'facets'

def get_training_queue(xml_data)
xml_data.xpath("//row").map do |skill_in_queue|
skill_in_queue.attributes.mash do |name, attribute|
[name.to_sym, attribute.text]
end
end
end


All those eachs and in-place updates show that you think in imperative terms instead of functional, check this wiki page.

• I had failed to get around to messing with facets up to this point - so interesting solution. Both answers presented in this thread were helpful, but Flambino offered two helpful solutions so I'm going to go ahead and issue them the answer. Upvote though :) – Ecnalyr Mar 5 '13 at 15:44
• @Ecnalyr: no problem, the solutions are almost identical. Make sure you read a bit about FP though, great stuff! – tokland Mar 6 '13 at 9:30