Ruby Ruby/XML, XSLT
Ruby: XML, XPath, and XSLT
Section titled “Ruby: XML, XPath, and XSLT”What is XML?
Section titled “What is XML?”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.
XML Parser Approaches
Section titled “XML Parser Approaches”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.
Installing Nokogiri:
Section titled “Installing Nokogiri:”gem install nokogiriREXML: 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>DOM-style Parsing with Nokogiri
Section titled “DOM-style Parsing with Nokogiri”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 directorybegin 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}"endThis example demonstrates reading the XML, accessing elements and attributes using XPath and CSS selectors, and iterating through nodes.
SAX-style Parsing with Nokogiri
Section titled “SAX-style Parsing with Nokogiri”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}" endend
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}"endSAX parsing is more verbose to set up but very efficient for large XML streams.
XPath and CSS Selectors with Nokogiri
Section titled “XPath and CSS Selectors with Nokogiri”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: truerequire '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 examplesputs "--- 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 examplesputs "\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 Processing with Nokogiri
Section titled “XSLT Processing with Nokogiri”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}"endFurther Reading:
Section titled “Further Reading:”- 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