Nokogiri : le parseur XML sexy de Ruby
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 :
Tokay Asie du Sud-Est Gris-Bleu à points Rouges et Blancs Grand Nocturne Leopard Moyen-Orient et Inde Leopard Moyen Nocturne Rhacodactylus ciliatus Nouvelle-Calédonie Orange Moyen Nocturne Phelsuma Ile de Madagascar et Environs Vert Moyenne Diurne
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 => 4, :encoding => '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 => 4, :encoding => '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.
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.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.Fred le 10 Nov 2011 à 11:33
Merci pour cet article.
Je rencontre un problème avec la fonction addText et l’utf-8En 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