How to use XPath with Nokogiri to select a single element from a Nodeset based on Tagname

Ashley Raiteri Source

Given the following XML,

<Set >
<RecommendedCoverSong>Hurt by NiN - Johnny Cash</RecommendedCoverSong>
<RecommendedOriginalSong>She Like Electric by Smoosh</RecommendedOriginalSong>
<RecommendedDuetSong>Portland by Jack White and Loretta Lynn</RecommendedDuetSong>
<RecommendedGroupSong>SoS by Abba</RecommendedGroupSong>
<CoverSong>Kangaroo  by Big Star  - This Mortal Coil</CoverSong>
<OriginalSong>Pick up the Change by Wilco</OriginalSong>
<DuetSong>I am the Cosmos by Pete Yorn and Scarlett Johansen</DuetSong>
<GroupSong>Kitties Never Rest by Rex or Regina</GroupSong>

I'd like to grab two elements that include "Cover" in the tag, and then operate on each of them.

Nokogiri's use of Xpath easily allows the first query expression like so:

price_xml = doc_xml.xpath('Container/Set/*[contains(name(), "Cover")]')

I've selected all the elements (using *) in Set, and then used an Xpath Expression function:

contains, in order to specify that Adult must be in the name. This returns two Nokogiri XML Nodes in Nodeset.

What I wanted to do was then select one of these elements based on a pattern in the tagname use my favorite tool, Xpath.

But I just couldn't get Nokogiri to give it to me, and several solutions ending up selecting way more than the 1 element I wanted. (Because the nodes in the Nodeset still contain relationships with their parents)

songtypes = ['Cover', 'Original', 'Duet', 'Group']
songtypes.each do |song|

node_xml = doc.xpath('Container/Set/*[contains(name(), "Cover")]')
#I wanted to be able to do the following
FavoriteCover =  node_xml.xpath('./*[contains(name(), "Recommended")]')
RegularCover  =  node_xml.xpath('./*[not(contains(name(), "Recommended"))]')

FavoriteCover =  node_xml.xpath('*[contains(name(), "Recommended")]')
RegularCover  =  node_xml.xpath('*[not(contains(name(), "Recommended"))]')
#But instead I had to resort to a Rails solution

RegularCover  =  node_xml.find{ |node| !~ /Recommended/ }
FavoriteCover =  node_xml.find{ |node| =~ /Recommended/ }

#Do something with the songs here




answered 7 years ago taro #1

Try something like:

node_xml.at_xpath('./self::*[not(contains(name(), "Recommended"))]')
node_xml.at_xpath('./self::*[contains(name(), "Recommended")]')

And consider using variables instead of constants inside iteration.

Or you can generate node name:

songtypes = ['Cover', 'Original', 'Duet', 'Group']
songtypes.each do |st|
  regular = doc.at_xpath("Container/Set/#{st}Song")
  recommended = doc.at_xpath("Container/Set/Recommended#{st}Song")

comments powered by Disqus