Skip to content

Ruby Ruby/XML, XSLT

The Extensible Markup Language (XML) is a markup language designed to carry data, not to display data (unlike HTML). It’s a W3C recommendation and an open standard, enabling structured data exchange between different applications and systems.

XML is valuable for data storage, configuration files, and as a format for data interchange over networks.

There are two main approaches to parsing XML documents:

  • DOM (Document Object Model): The parser reads the entire XML document into memory and builds a tree structure representing the document. This allows for easy navigation and modification of the XML. However, it can be memory-intensive for very large documents. Nokogiri primarily uses a DOM-style approach for parsing full documents.
  • SAX (Simple API for XML): This is an event-based approach. The parser reads the document sequentially and triggers events (callbacks) as it encounters elements, attributes, text, etc. It’s more memory-efficient for large documents as the entire document isn’t stored in memory. It’s typically read-only during parsing.

Choosing between DOM and SAX depends on the task: DOM is often easier for random access and modification, while SAX is better for processing large files or when memory is a concern.

XML Processing in Ruby: Nokogiri and REXML

Section titled “XML Processing in Ruby: Nokogiri and REXML”

While Ruby’s standard library includes REXML (a pure-Ruby XML processor), the de facto standard for XML (and HTML) processing in the Ruby community is the Nokogiri gem. Nokogiri is significantly faster and more feature-rich, leveraging native C libraries (like libxml2) for performance.

gem install nokogiri

REXML: REXML is part of the standard library, so no installation is needed. It’s suitable for simpler tasks or when external gem dependencies are undesirable.

For this tutorial, we will primarily focus on Nokogiri due to its prevalence and capabilities. We’ll use the following sample XML file (movies.xml):

<!-- movies.xml -->
<collection shelf="New Arrivals">
<movie title="Enemy Behind">
<type>War, Thriller</type>
<format>DVD</format>
<year>2003</year>
<rating>PG</rating>
<stars>8</stars>
<description>A gripping war drama.</description>
</movie>
<movie title="Cosmic Odyssey">
<type>Science Fiction</type>
<format>Blu-ray</format>
<year>2021</year>
<rating>PG-13</rating>
<stars>9</stars>
<description>An epic journey through space.</description>
</movie>
<movie title="The Silent City">
<type>Mystery</type>
<format>Streaming</format>
<episodes>10</episodes> <!-- Note: some movies might have episodes instead of year -->
<rating>R</rating>
<stars>7</stars>
<description>A detective uncovers secrets.</description>
</movie>
</collection>

Nokogiri can parse XML from a file, a string, or an IO object.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'nokogiri'
# Assuming movies.xml is in the same directory
begin
file = File.open("movies.xml")
doc = Nokogiri::XML(file)
file.close
# Get the root element and its attributes
root = doc.root
puts "Root element: #{root.name}, Shelf: #{root['shelf']}"
puts "\n--- Movie Titles ---"
doc.xpath("//movie").each do |movie_node|
puts "Title: #{movie_node['title']}"
end
puts "\n--- Movie Types ---"
doc.xpath("//movie/type").each do |type_node|
puts "Type: #{type_node.content}" # .text or .content gets the text content
end
puts "\n--- Movie Descriptions ---"
# Using CSS selectors as an alternative to XPath
doc.css("collection movie description").each do |desc_node|
puts "Description: #{desc_node.text}"
end
# Find a specific movie and its details
puts "\n--- Details for 'Cosmic Odyssey' ---"
cosmic_odyssey = doc.at_xpath("//movie[@title='Cosmic Odyssey']") # .at_xpath finds the first match
if cosmic_odyssey
puts "Format: #{cosmic_odyssey.at_xpath('format')&.text}" # &. safe navigation operator
puts "Year: #{cosmic_odyssey.at_css('year')&.text}"
puts "Rating: #{cosmic_odyssey.css('rating').first&.text}"
else
puts "'Cosmic Odyssey' not found."
end
rescue Errno::ENOENT
puts "Error: movies.xml not found."
rescue StandardError => e
puts "An XML parsing error occurred: #{e.message}"
end

This example demonstrates reading the XML, accessing elements and attributes using XPath and CSS selectors, and iterating through nodes.

For SAX parsing, you create a custom handler class that inherits from Nokogiri::XML::SAX::Document and override its event methods.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'nokogiri'
class MovieSAXHandler < Nokogiri::XML::SAX::Document
def initialize
@current_element = nil
@in_movie_title = false
end
# Called at the beginning of an element
def start_element(name, attrs = [])
@current_element = name
# attrs is an array of [localname, prefix, URI, value] or [localname, value]
# For simplicity, convert to a hash if needed: Hash[attrs]
puts "Start Element: #{name}, Attributes: #{Hash[attrs]}"
if name == "movie"
title = Hash[attrs]["title"]
puts " -> Found movie: #{title}" if title
end
@in_movie_title = true if name == "title" && @current_element_path&.end_with?("/movie/title")
end
# Called with character data inside an element
def characters(string)
# Avoid printing excessive whitespace, trim for readability
text_content = string.strip
if text_content.length > 0
puts " Characters: '#{text_content}' (inside #{@current_element})"
end
end
# Called at the end of an element
def end_element(name)
puts "End Element: #{name}"
@current_element = nil
@in_movie_title = false if name == "title"
end
def error(message)
puts "SAX Error: #{message}"
end
def warning(message)
puts "SAX Warning: #{message}"
end
end
begin
parser = Nokogiri::XML::SAX::Parser.new(MovieSAXHandler.new)
file_path = "movies.xml"
puts "Starting SAX parsing of #{file_path}...\n"
parser.parse(File.open(file_path))
puts "\nSAX parsing finished."
rescue Errno::ENOENT
puts "Error: movies.xml not found."
rescue StandardError => e
puts "An error occurred: #{e.message}"
end

SAX parsing is more verbose to set up but very efficient for large XML streams.

XPath is a language for navigating XML documents. CSS selectors, commonly used for HTML, can also be used with Nokogiri for XML. Nokogiri provides xpath() and css() methods on document and node objects. at_xpath() and at_css() find the first matching node.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'nokogiri'
xml_data = <<~XML
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
</book>
</catalog>
XML
doc = Nokogiri::XML(xml_data)
# XPath examples
puts "--- XPath --- "
puts "All book titles:"
doc.xpath("//book/title").each { |title| puts " - #{title.text}" }
puts "Author of book with id 'bk101': #{doc.at_xpath("//book[@id='bk101']/author")&.text}"
# CSS selector examples
puts "\n--- CSS Selectors --- "
puts "All prices:"
doc.css("book price").each { |price| puts " - #{price.text}" }
puts "Genre of book with id 'bk102': #{doc.at_css("book[id='bk102'] genre")&.text}"

XSLT (Extensible Stylesheet Language Transformations) is a language for transforming XML documents into other XML documents, or other formats like HTML or plain text. Nokogiri can apply XSLT stylesheets.

Suppose you have an XSLT stylesheet (movies_to_html.xsl):

<!-- movies_to_html.xsl -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<h2>Movie Collection</h2>
<table border="1">
<tr>
<th>Title</th>
<th>Type</th>
<th>Year/Episodes</th>
</tr>
<xsl:for-each select="collection/movie">
<tr>
<td><xsl:value-of select="@title"/></td>
<td><xsl:value-of select="type"/></td>
<td>
<xsl:choose>
<xsl:when test="year">
<xsl:value-of select="year"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="episodes"/> episodes
</xsl:otherwise>
</xsl:choose>
</td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

You can apply this transformation using Nokogiri:

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'nokogiri'
begin
xml_doc = Nokogiri::XML(File.open("movies.xml"))
xslt_doc = Nokogiri::XSLT(File.open("movies_to_html.xsl"))
html_output = xslt_doc.transform(xml_doc)
puts "--- Transformed HTML Output ---"
puts html_output.to_html # Or .to_xml if the output is XML
# Save to an HTML file
File.open("movies_report.html", "w") { |f| f.write(html_output.to_html) }
puts "\nReport saved to movies_report.html"
rescue Errno::ENOENT => e
puts "Error: Missing XML or XSLT file. #{e.message}"
rescue StandardError => e
puts "An error occurred during XSLT processing: #{e.message}"
end
  • Nokogiri Official Documentation: https://nokogiri.org/
  • REXML Documentation (Ruby Standard Library): Search for REXML in the Ruby core docs.
  • W3Schools XPath Tutorial: https://www.w3schools.com/xml/xpath_intro.asp
  • W3Schools XSLT Tutorial: https://www.w3schools.com/xml/xsl_intro.asp