GeckoGeek.fr

Paradigme d'un lézard

Samedi 19 Août 2017

Nokogiri : le parseur XML sexy de Ruby

Par Lya le 09/01/2010 dans Programmation | 3 commentaires

Nous allons découvrir dans ce billet une petite librairie bien sympathique pour Ruby qui vous sera surement très utile. Après une brève description, nous verrons comment l’utiliser dans ses fonctionnalités de bases. Nous nous focaliserons essentiellement sur le côté XML.

Nokogiri en quelques mots

Cette librairie a été créée par Aaron Patterson et Mike Dalessio. C’est un parseur HTML/XML/XSLT pour Ruby qui se sert de XPATH et/ou de CSS3. Elle est basée sur le modèle Hpricot et elle utilise libXML2 (et libXSLT) pour chercher et parser (d’où sa rapidité).

Vous utilisez peut-être toujours la librairie pure Ruby REXML, ou bien vous avez le soucis de la rapidité et vous êtes déjà passé à Hpricot ? Allez, allez il est temps de changer. La transition n’est pas trop dure de Hpricot vers Nokogiri (je vous rappelle que c’est le même modèle, donc vous n’avez quasiment rien à faire). Et de REXML à Nokogiri ? Très peu de changements, la syntaxe est assez proche mais Nokogiri est tellement plus élégant ;-] (et fait tellement plus de choses, et va tellement plus vite, et est tellement so cute!). Tentez le changement, vous allez aimer !

C’est vraiment rapide ?

C’est tout simple, d’un côté on a les escargots asthmatiques, de l’autre les libellules supraluminiques :-]. Comment ça j’exagère ? On n’en est pas loin.
Regardez ces benchmarks réalisés par RubyInside (ils ne couvrent pas tout, mais pour les fonctions de bases c’est suffisant, pour info l’axe des y est en seconde) :

Et puis, n’hésitez pas à faire vos propres benchmarks, il n’y a que ça de mieux pour être convaincu !

Personnellement pour des codes que nous utilisons, pour le chargement d’un arbre XML d’un millier de noeuds à deux/trois étages, nous passons d’une dizaine de secondes (REXML) à moins d’une seconde (Nokogiri). La classe :p. Ne parlons même pas de la recherche ou de l’écriture dans un fichier.

Même pour un tout petit mini document (une cinquantaine de noeuds sur deux étages), vous allez me dire : « on ne va pas s’embêter pour ça… » (au passage, l’embêtement => sudo gem install nokogiri). Ok, mais c’est quand même plus rapide de deux dixièmes de secondes facilement (vous passez d’un centième de seconde à un dix millième hop !), et puis, il faut prendre les bonnes habitudes :-].

Bref aperçu

On va voir rapidement les fonctionnalités de base. Afin que vous puissiez tester rapidement et facilement ces codes, j’ai mis tout dans une même classe. Parfois c’est un peu acrobatique, mais le but c’est de montrer les fonctionnalités :p.

Voici la classe dans laquelle nous évoluerons :

require 'nokogiri'

class XMLNokogiri

  attr_accessor :myDoc

  def initialize(myFileName)
    loadXML(myFileName)
  end

end

L’équivalent en REXML pour comparer :

require 'rexml/document'
include REXML

class XMLREXML

  attr_accessor :myDoc

  def initialize(myFileName)
    loadXML(myFileName)
  end

end

Et voici le fichier XML que l’on prendra en exemple :

<gekkonidae>

	<gecko name="Blue">
  		<espece>Tokay</espece>
  		<region>Asie du Sud-Est</region>
  		<couleur>Gris-Bleu à points Rouges et Blancs</couleur>
  		<taille>Grand</taille>
  		<periode>Nocturne</periode>
	</gecko>

	<gecko name="Leopard">
  		<espece>Leopard</espece>
  		<region>Moyen-Orient et Inde</region>
  		<couleur>Leopard</couleur>
  		<taille>Moyen</taille>
  		<periode>Nocturne</periode>
	</gecko>

	<gecko name="Orange">
  		<espece>Rhacodactylus ciliatus</espece>
  		<region>Nouvelle-Calédonie</region>
  		<couleur>Orange</couleur>
  		<taille>Moyen</taille>
  		<periode>Nocturne</periode>
	</gecko>

	<gecko name="Green">
  		<espece>Phelsuma</espece>
  		<region>Ile de Madagascar et Environs</region>
  		<couleur>Vert</couleur>
  		<taille>Moyenne</taille>
  		<periode>Diurne</periode>
	</gecko>

</gekkonidae>

Charger un arbre XML en mémoire

Pour charger le document en mémoire avec Nokogiri :

  def loadXML(myFileName)

    begin
      myFile = File.new(myFileName)
    rescue
      puts "Can't open the file. Please check the name: " + myFileName + ". Try it again: "
      myFileName = gets.chomp
      retry
    end

    @myDoc = Nokogiri::XML(myFile)

  end

Avec REXML, c’était juste la syntaxe de la ligne qui crée l’arbre qui changeait :

    @myDoc = Document.new(myFile)

Affichage basique des éléments

Globalement j’affiche l’arbre, mais le but est plutôt de montrer comment accéder aux différents éléments (de manière basique). Après dans cet exemple je les affiche, mais vous pouvez en faire ce que vous voulez.

  def readXML()

    # Get a node (or many)
    for gecko in @myDoc.root.xpath("//gecko")

      # Get an attribute
      puts gecko['name']

      # Get a text
      puts "\t" + gecko.xpath("./espece").text
      puts "\t" + gecko.xpath("./periode").text
      puts "\t" + gecko.xpath("./region").text
      puts "\t" + gecko.xpath("./taille").text
      puts "\t" + gecko.xpath("./couleur").text
      puts "\n"

    end

  end

Avec REXML, on pouvait avoir :

    # Get a node (or many)
    for gecko in @myDoc.root.elements

      # Get an attribute
      puts gecko.attributes['name']

      # Get a text
      puts "\t" + gecko.elements["espece"].text
      puts "\t" + gecko.elements["periode"].text
      puts "\t" + gecko.elements["region"].text
      puts "\t" + gecko.elements["taille"].text
      puts "\t" + gecko.elements["couleur"].text
      puts "\n"

    end

Chercher un noeud

Pour Nokogiri, je le fais avec XPATH, mais il y a d’autres moyens.

  def searchNode(xpathExpr)

    myNode = @myDoc.at(xpathExpr)

    if(myNode == nil)
      puts "Not found..."
    else
      puts myNode.to_xml
      return myNode
    end

  end

Avec REXML, seule la ligne de recherche était un peu différente (attention la méthode to_xml ne marche pas pour REXML) :

    myNode = @myDoc.elements.to_a(xpathExpr)

Ajouter un noeud

Je distingue deux cas, ajouter un noeud à la racine et ajouter un noeud n’importe où.

  def insertAChildNode(docPosition, myNode)
    docPosition.add_child(myNode)
  end

  def addARootNode(myNode)
    @myDoc.root = myNode
  end

Pour REXML, on utilisait :

  # Insert a node in the XML doc
    docPosition.add_element(myNode)

  # Insert a root node in the XML doc
    @myDoc.add_element(myNode)

Créer un nouveau noeud

Pour créer un nouveau noeud avec Nokogiri (avant de l’ajouter)

  def createANewNode(name)
    return Nokogiri::XML::Node.new(name, @myDoc)
  end

Avec REXML, on faisait :

    return Element.new(name)

Ajouter un texte et des attributs

Pour ajouter du texte ou un attribut avec Nokogiri :

  def addAnAttribute(myNode, name, value)
    myNode[name] = value
  end

  def addText(myNode, text)
    myNode.content = text.to_s
  end

Avec REXML :

  # Add attribute
    myNode.add_attribute(name, value)

  # Add text
    myNode.text = text.to_s

Créer un nouveau document

Avec Nokogiri :

  def createANewDoc()
    myNewDoc = Nokogiri::XML::Document.new
  end

Avec REXML c’était à peu près pareil :

    myNewDoc = Document.new

Sauvegarder l’arbre XML dans un fichier

Et pour finir, voici comment sauvegarder l’arbre dans un fichier avec Nokogiri :

  def saveToFile(myfileName)

    begin
      myFile = File.new(myfileName)
    rescue
    end

    myFile = File.open(myfileName, 'w')
    @myDoc.write_xml_to(myFile, :indent =&gt; 4, :encoding =&gt; 'UTF-8')
    myFile.close

  end

Avec REXML, la ligne d’écriture était différente :

    @myDoc.write(myFile, 4)

Utilisation

Maintenant vous pouvez utiliser ces fonctions de base (vous pouvez en faire de même avec celle en REXML). En vrac par exemple :

# Load the XML doc
myXMLNokogiri = XMLNokogiri.new("./Gekkonidae.xml")
# Print the XML doc
myXMLNokogiri.readXML()
# Get the node of the gecko named Green
myXMLNokogiri.searchNode('//gecko[@name = "Green"]')

# Add a new gecko node with an attribute name Color
myNewNode = myXMLNokogiri.createANewNode('gecko')
myXMLNokogiri.addAnAttribute(myNewNode, 'name', 'Color')
myXMLNokogiri.addARootNode(myNewNode)

# Add a node couleur to the node we have just added
myNewNode = myXMLNokogiri.createANewNode('couleur')
myXMLNokogiri.addText(myNewNode, 'Multicolor')
myNode = myXMLNokogiri.searchNode('//gecko[@name = "Color"]')
myXMLNokogiri.insertAChildNode(myNode, myNewNode)

# We create a new doc and save it at the place of the old one (yeah, it's just to test eh :p)
myXMLNokogiri.myDoc = myXMLNokogiri.createANewDoc()

# We create a new node with an attribute
myNewNode = myXMLNokogiri.createANewNode('gecko')
myXMLNokogiri.addAnAttribute(myNewNode, 'name', 'Geckogeek')
# We create a new node and add it to the gecko node we have created
myNewChildNode = myXMLNokogiri.createANewNode('espece')
myXMLNokogiri.addText(myNewChildNode, 'Geek')
myXMLNokogiri.insertAChildNode(myNewNode, myNewChildNode)
# We add the gecko node to our new tree
myXMLNokogiri.addARootNode(myNewNode)

# We save this tree in a file
myXMLNokogiri.saveToFile("./GekkonidaeGeek.xml")

Récapitulatif

Voici en un seul tenant la classe Nokogiri que nous venons d’écrire :

require 'nokogiri'

class XMLNokogiri

  attr_accessor :myDoc

  def initialize(myFileName)
    loadXML(myFileName)
  end

  def loadXML(myFileName)

    begin
      myFile = File.new(myFileName)
    rescue
      puts "Can't open the file. Please check the name: " + myFileName + ". Try it again: "
      myFileName = gets.chomp
      retry
    end

    @myDoc = Nokogiri::XML(myFile)

  end

  def readXML()

    # Get a node (or many)
    for gecko in @myDoc.root.xpath("//gecko")

      # Get an attribute
      puts gecko['name']

      # Get a text
      puts "\t" + gecko.xpath("./espece").text
      puts "\t" + gecko.xpath("./periode").text
      puts "\t" + gecko.xpath("./region").text
      puts "\t" + gecko.xpath("./taille").text
      puts "\t" + gecko.xpath("./couleur").text
      puts "\n"

    end

  end

  def searchNode(xpathExpr)

    myNode = @myDoc.at(xpathExpr)

    if(myNode == nil)
      puts "Not found..."
    else
      puts myNode.to_xml
      return myNode
    end

  end

  def insertAChildNode(docPosition, myNode)
    docPosition.add_child(myNode)
  end

  def addARootNode(myNode)
    @myDoc.root = myNode
  end

  def createANewNode(name)
    return Nokogiri::XML::Node.new(name, @myDoc)
  end

  def addAnAttribute(myNode, name, value)
    myNode[name] = value
  end

  def addText(myNode, text)
    myNode.content = text.to_s
  end

  def createANewDoc()
    myNewDoc = Nokogiri::XML::Document.new
  end

  def saveToFile(myfileName)

    begin
      myFile = File.new(myfileName)
    rescue
    end

    myFile = File.open(myfileName, 'w')
    @myDoc.write_xml_to(myFile, :indent =&gt; 4, :encoding =&gt; 'UTF-8')
    myFile.close

  end

end

Voilà vous avez les bases pour vous lancer dans cette librairie ! N’hésitez surtout pas à l’utiliser ;-] ça serait dommage de passer à côté. Vous pouvez aussi aller jeter un oeil sur le site officiel si ça vous dit.

Commentaires (3)
  1. AmineDigirep le 27 Oct 2010 à 22:43

    Merci pout cet article.
    Ne pas oublier d’ajouter avant “require ‘nokogiri'” “require ‘rubygems'”, pour faire marcher le code.

  2. Lya le 28 Oct 2010 à 09:28

    Effectivement :-)
    Ou bien rajouter dans le .profile qui se trouve à la racine la ligne suivante : “export RUBYOPT=rubygems”. Ainsi ça sera valable pour tous les codes, plus besoin de le préciser pour chaque fichier.

  3. Fred le 10 Nov 2011 à 11:33

    Merci pour cet article.
    Je rencontre un problème avec la fonction addText et l’utf-8

    En effet, si j’écris :
    data=”toto”
    textNode = @@letter.createANewNode(‘text’)
    @@letter.addText(textNode, data)
    @@letter.insertAChildNode(myNewChildNode, textNode)

    Je n’ai aucun problème (création du fichier avec le contenu que je lui ai demandé). Par contre, si j’écris :
    data=”zoé”
    textNode = @@letter.createANewNode(‘text’)
    @@letter.addText(textNode, data)
    @@letter.insertAChildNode(myNewChildNode, textNode)

    Je n’obtiens pas de message d’erreur (j’ai un rescue) mais mon fichier est vide.

    Pourriez-vous m’indiquer comment résoudre ce problème?

    Merci


Laisser un commentaire