SAX

1. Introduction à SAX

1.2 Présentation du modèle de traitement

Parmi les différentes API permettant de traiter XML, une des plus anciennes est SAX : Simple API for XML. Développée d'abord pour Java (avec JAXP dans les différentes versions de Java), elle a été aussi développée pour d'autres langages comme PHP, C++, Perl, etc.

SAX est une API parfois difficile à appréhender pour des débutants. En effet, elle est basée sur une méthode de programmation événementielle : les traitements sont liés à l'analyse syntaxique du document XML. Autrement dit, un "parseur" (analyseur syntaxique) parcours le document dans l'ordre de lecture et, à chaque fois qu'il rencontre un objet XML (commentaire, élément, texte, etc.), il produit un événement.

Lien sur l'animation en cas de problème de chargement.

Plus précisément, à chaque fois que l'analyseur rencontre un élément syntaxique, un événement est généré. Pour mieux comprendre, prenons l'exemple ci-dessous où le document décrit un contact (nom d'une personne et son adresse de courrier électronique).

En parcourant ce document (dans le sens de lecture), le premier objet rencontré est la balise ouvrante "<contact id='3'>". L'analyseur produit donc un événement (de type "startElement" à partir de cette balise. Il ajoute à cet événement, un tableau associatif contenant les attribut de la balise (ici, le couple <"id","3">). L'objet suivant est la balise ouvrante de l'élément "nom". Un événement "startElement" est à nouveau généré. Ensuite, l'analyseur rencontre une chaîne de caractères et, par conséquent, génère un événement "character" avec en paramètre cette chaîne. Puis, il rencontre la balise fermante de l'élément "nom" : un événement "endElement" est alors généré. Et ainsi de suite sur le document. Lorsqu'une balise est à la fois ouvrante et fermante, deux événements ("startElement" puis "endElement") seront tout de même produits. C'est le cas de la balise "courriel" de notre exemple. L'exemple ne le montre pas, mais tout objet XML produit un ou plusieurs événements (les commentaires, les instructions de traitement, etc.).
Schéma d'exploitation d'un document XML par l'API SAX

En plus des événements liés aux objets XML du document, sont produits de manière systématique deux événements particuliers :

Avec SAX, la programmation est "dirigée par la syntaxe" c'est le document XML qui donne l'ordre des événements et donc des actions à effectuer. Ce type de programmation n'est pas toujours facile à maîtriser. Aussi, dans un premier temps, il est fortement conseillé de dessiner, partiellement ou totalement, l'arbre XML issu d'un document exemple afin de comprendre comment seront parcourues les informations et quand effectuer les traitements. Sur notre exemple "fil rouge", cela donnera un arbre comme celui ci-contre (à quelques commentaires près).
Parcours du document XML fil rouge par l'API SAX

2. SAX, traitements de base en Java

2.1 Principes SAX en Java

Un programme SAX en Java consiste en un analyseur syntaxique standard auquel est adjoint un ensemble de gestionnaires d'événements par défaut. Pour modifier les traitements par défaut, il suffit de redéfinir les méthodes associées. Dans l'animation ci-dessous, le gestionnaire d'événements est redéfinit pour certaines méthodes. Les autres sont celles par défaut.

Principe de programmation SAX en Java - étape 1

Nous allons regarder de plus près la manière de spécifier des traitements particuliers liés aux objets XML. Il est aussi possible de modifier le comportement en cas d'erreur, par exemple, mais nous ne l'évoquerons pas plus ici.

NB : dans ce cours, nous ne présenterons que SAX2.

2.2 Gestion des événements de base

La gestion des contenus est pilotée par un ContentHandler et la gestion des erreurs par un ErrorHandler. Par conséquent, pour mettre notre propre traitement, il suffit d'indiquer à l'analyseur syntaxique (XMLReader) le nouveau gestionnaire de contenu ou d'erreurs (par les méthodes "setContentHandler" ou "setErrorHandler").

En java, il existe un gestionnaire (de contenu et d'erreur) par défaut : DefaultHandler. Plutôt que de redéfinir tous les traitements, il est plus facile de partir des traitements (vides) de cette classe et de créer un nouveau gestionnaire qui en hérite. Ainsi, pour les traitements particuliers, il suffit de surcharger uniquement les méthodes nécessaires à ces traitements.

NB : les paquetages qui contiennent les interfaces et les objets spécifiques à SAX sont : org.xml.sax (les principales interfaces) et org.xml.sax.helpers (pour DefaultHandler en particulier).

2.3 Gestion des contenus

Les principales méthodes de ContentHandler sont les suivantes :

void startDocument() throws SAXException
void endDocument() throws SAXException
void startElement(String uri, String localName, String qName,  Attributes atts)
                  throws SAXException
void endElement(String uri, String localName, String qName)
                throws SAXException

void characters(char[] ch, int start, int length)
                throws SAXException

void ignorableWhitespace(char[] ch, int start, int length)
                         throws SAXException

void processingInstruction(String target, String data)
                           throws SAXException

Les différents paramètres sont les suivants :

Afin de mieux comprendre, voici un programme simple permettant d'afficher les événements principaux (début/fin de document et début/fin d'élément) et, pour les éléments, leurs noms en fonction des espaces de noms utilisés :

import java.io.File;

// Paquetages SAX
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

// Description du gestionnaire d'événements spécifique
public class TestHandler extends DefaultHandler {

  // Méthodes surchargées
  public void startDocument() throws SAXException {
  	System.out.println("startDocument");}

  public void endDocument() throws SAXException {
  	System.out.println("endDocument");}

  public void startElement(String namespaceURI, String sName, String qName, 
			               Attributes attrs) throws SAXException {
    System.out.println("startElement (" + namespaceURI  
                       + " - " + sName  + " - " + qName  + ")");
  }

  public void endElement(String namespaceURI, String sName, String qName) 
                         throws SAXException {
    System.out.println("endElement (" + namespaceURI  
                       + " - " + sName  + " - " + qName  + ")");
  }

  // Mise en place de l'analyseur et insertion du gestionnaire spécifique
  public static void main(String[] args) {
    DefaultHandler handler = new TestHandler();
    try {
      XMLReader saxParser = XMLReaderFactory.createXMLReader();
      saxParser.setContentHandler(handler);
      saxParser.setErrorHandler(handler);
      saxParser.parse( "exempleTestSAX.xml" ); 
    } catch (Throwable t) {t.printStackTrace();}
    System.exit(0);
  }
}

Appliqué sur un document test, ce programme donnera le résultat suivant :

startDocument
startElement ( - a - a)
startElement (http://www.fff.com/yyy/ - b - b)
startElement (http://www.fff.fr/zzz/ - c - p:c)
startElement (http://www.fff.com/yyy/ - d - d)
endElement (http://www.fff.com/yyy/ - d - d)
endElement (http://www.fff.fr/zzz/ - c - p:c)
endElement (http://www.fff.com/yyy/ - b - b)
startElement ( - b - b)
endElement ( - b - b)
endElement ( - a - a)
endDocument

3. Exemple

Sur le document "fil-rouge", l'objectif de ce premier exemple est simplement de compte le nombre d'ouvrages édités depuis 1960. Cet exemple est le même que celui utilisé pour illustrer DOM. En SAX, ce traitement est très simple puisque l'analyseur "s'occupe du parcours". De plus, nous avons beaucoup de chance, car les données utiles au traitement se trouvent dans la balise ouvrante "livre". Il suffit donc, lorsque cette balise est rencontrée, de la tester et d'incrémenter un compteur. Le programme principal n'est pas ici présenté, mais est identique à l'exemple précédent. Les événements de début et de fin de document servent à initialiser le compteur (pour le premier) et à afficher le résultat (pour le second).

import java.io.*;
import org.xml.sax.*;
import javax.xml.parsers.SAXParserFactory; 
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser; 
import org.xml.sax.helpers.DefaultHandler;

public class nbl_sax extends DefaultHandler {
	public int nb;
	
	public void startDocument() throws SAXException {
		nb=0; }
	
	public void endDocument() throws SAXException {
		System.out.println(nb);}
	
	public void startElement(String namespaceURI, 
	                         String nom, String nom_qualifie, 
	                         Attributes attrs) throws SAXException {
  	  if (nom == "livre")
   	    if (Integer.parseInt(attrs.getValue("annee"))>1960) 
   	      nb = nb +1;
	} 

	public static void main(String argv[]){...} 
}

Cet exemple sera aussi développé en PHP.

4. Extension de SAX

Java propose aussi certaines extensions complémentaires : org.xml.sax.helpers. Ces extensions ne sont pas toujours présentes dans les implémentations.

Il est proposé deux nouveaux "handler" ("DeclHandler" et "LexicalHandler") qui permettent de gérer :

Un nouveau gestionnaire par défaut est aussi proposé pour tenir compte de ces nouveaux gestionnaires : DefaultHandler2 qui hérite de "DefaultHandler".

5. SAX et PHP

5.1 Flux XML en PHP

PHP propose de traiter un document XML de manière très similaire à SAX à l'aide d'un analyseur syntaxique XML. Le principe est de transformer un document XML (ou une chaîne de caractères XML) en une suite d'événements. Les événements produits ne sont pas aussi diversifiés qu'en SAX/Java.

Le principe est le suivant : un analyseur est créé à partir d'un moteur générique (xml_parser_create) et d'un objet possédant des méthodes spécifiques qui seront appelées selon l'événement produit. L'objet doit comporter, entre autre, des méthodes avec des signatures spécifiques comme, par exemple :

Ensuite, il suffit d'indiquer à l'analyseur, l'objet de traitement et les méthodes adaptées pour :

Pour illustrer l'utilisation de ces méthodes, un exemple permettant d'afficher les éléments, les textes, les instructions de traitement et "le reste" est présenté ci-dessous. L'objet de traitement est en bleu et la manipulation de l'analyseur en rouge.

// Objet de gestion des différents événements
class visu_sax {
  private $sax;
  
  function texte_node($sax, $txt) {
    $txt = trim($txt);
    if (!(empty($txt))) echo "txt#".utf8_decode($txt)."#";
  }
  
  function debut_element($sax, $nom, $att) {
  	echo 'Début:'.utf8_decode($nom)."\n";}
  
  function fin_element($sax, $nom) {
  	echo 'Fin:'.utf8_decode($nom)."\n";}
  
  function instruction_traitement($sax, $cible, $contenu) {
  	echo 'PI:'.$cible."\n";}
  
  function defaut($sax,$data) {
  	echo '???'.$data."\n";}
}

// Fonction pour mettre en place l'analyseur avec l'objet de traitement
function doSAX($fic) {
    //Création de l'analyseur générique
    $sax = xml_parser_create('UTF-8');
    
    //Association avec l'objet traitant les événements
    xml_set_object($sax, new visu_sax() );
    xml_parser_set_option($sax,XML_OPTION_CASE_FOLDING, FALSE);
    xml_parser_set_option($sax,XML_OPTION_TARGET_ENCODING, 'UTF-8');
    
    //Description des méthodes à appeler en fonction des événements
    xml_set_character_data_handler($sax,'texte_node');
    xml_set_element_handler($sax,'debut_element','fin_element');
    xml_set_processing_instruction_handler($sax,'instruction_traitement');
    xml_set_default_handler($sax,'defaut');
    
    //Traitement du document
    $fic=file_get_contents($fic);
    xml_parse($sax,$fic,TRUE);
    
    //Libération de l'analyseur
    xml_parser_free($sax);
}

// Traitement d'un document XML
doSAX("ex_asimov.xml");

L'analyseur proprement dit et l'objet de gestion peuvent être encapsulés dans un seul et même objet. L'exemple suivant reprend cette idée avec le même traitement que l'exemple précédent.

class visu_sax {
  private $sax;
  
  // Partie analyseur
  
  function __construct() {
    //Création de l'analyseur générique
    $this->sax = xml_parser_create('UTF-8');
    
    //Association avec l'objet traitant les événements
    xml_set_object($this->sax,$this);
    xml_parser_set_option($this->sax,XML_OPTION_CASE_FOLDING, FALSE);
    xml_parser_set_option($this->sax,XML_OPTION_TARGET_ENCODING, 'UTF-8');
    
    //Description des méthodes à appeler en fonction des événements
    xml_set_character_data_handler($this->sax,'texte_node');
    xml_set_element_handler($this->sax,'debut_element','fin_element');
    xml_set_processing_instruction_handler($this->sax,'instruction_traitement');
    xml_set_default_handler($this->sax,'defaut');
  }
  
  function __destruct() {//Libération de l'analyseur
    xml_parser_free($this->sax);}

  function parse($fic) {//Traitement du document
    $fic=file_get_contents('ex_asimov.xml');
    xml_parse($this->sax,$fic,TRUE);
  }
  
  // Partie gestionnaire d'événements
  
  function texte_node($sax, $txt) {
    $txt = trim($txt);
    if (!(empty($txt))) echo "txt#".utf8_decode($txt)."#";
  }
  
  function debut_element($sax, $nom, $att) {
  	echo 'Début:'.utf8_decode($nom)."\n";}
  
  function fin_element($sax, $nom) {
  	echo 'Fin:'.utf8_decode($nom)."\n";}
  
  function instruction_traitement($sax, $cible, $contenu) {
  	echo 'PI:'.$cible."\n";}
  
  function defaut($sax,$data) {
  	echo '???'.$data."\n";}
}

// Utilisation de l'objet
$visu = new visu_sax();
$visu->parse("ex_asimov.xml");

Ces deux programmes, sur notre exemple "fil-rouge", donneront le résultat suivant :

Début:auteur
Début:identité
Début:nom
txt#Asimov#Fin:nom
Début:prenom
txt#Isaac#Fin:prenom
Début:date-naissance
txt#1920-01-02#Fin:date-naissance
Début:date-deces
txt#1992-04-06#Fin:date-deces
Début:nationalite
txt#Russe/Am#txt#éricain#Fin:nationalite
Fin:identité
???<!-- Liste des ouvrages -->
Début:livre
Début:titre
txt#Un d#txt#éfilé de robots#Fin:titre
Fin:livre
Début:livre
Début:titre
txt#I, robot#Fin:titre
Fin:livre
Début:livre
Début:titre
txt#Le robot qui r#txt#êvait#Fin:titre
Fin:livre
Fin:auteur

Cet exemple illustre que les noeuds textes, en particulier en PHP, apparaissent parfois "fragmentés" en plusieurs événements. Ce phénomène apparaît lorsque le texte contient des caractères qui ne sont pas dans la table ASCII standard.

5.2 Une imitation de SAX

Comme nous pouvons le constater, l'approche PHP est extrêmement similaire à celle en Java. Aussi, il est possible de construire une bibliothèque permettant d'avoir des noms de classe ou méthode ainsi qu'une méthode de conception proche de Java. C'est ce que propose Sax4PHP. Cette bibliothèque ne respecte pas avec exactitude l'API SAX, mais permet de faire une programmation assez proche. Pour comparer, reprenons l'exemple précédent. Le code ci-dessous utilise Sax4PHP.

<?php 
header('Content-type: text/plain');
include(
'Sax4PHP/Sax4php.php');

class 
visu_sax extends DefaultHandler {
    
  function 
characters($txt) {
      
$txt trim($txt);
      if (!(empty(
$txt))) echo "txt#".utf8_decode($txt)."#";
  }
  
  function 
startElement($nom$att) {
      echo 
'Début:'.utf8_decode($nom)."\n";}
      
  function 
endElement($nom) {
      echo 
'Fin:'.utf8_decode($nom)."\n";}
      
  function 
processingInstruction($cible$contenu) {
      echo 
'PI:'.$cible."\n";}
      
  function 
node($data) {
      echo 
'???'.utf8_decode($data)."\n";}
}

$visu file_get_contents('ex_asimov.xml');

try {
    
$sax = new SaxParser(new visu_sax());
    
$sax->parse($visu); }catch(SAXException $e){ echo "\n",$e;
}catch(
Exception $e) {echo "Capture l'exception par défaut\n"$e;}
?>

5.3 Exemple de traitement

Reprenons le document "fil-rouge" et reprenons aussi l'exemple déjà utilisé pour DOM et en SAX/Java : compter le nombre d'ouvrages édités depuis 1960. Le code suivant propose une solution en PHP standard.

<?php
header
('Content-type: text/plain');

class 
nbl_sax {
  private 
$sax;
  public 
$nb;

  function 
__construct() {
    
$this->sax xml_parser_create('UTF-8');
    
xml_set_object($this->sax,$this);
    
xml_parser_set_option($this->sax,XML_OPTION_CASE_FOLDINGFALSE);
    
xml_set_element_handler($this->sax,'deb_elem','fin_elem');
    
xml_set_default_handler($this->sax,'defaut');
  }
  
  function 
__destruct() {xml_parser_free($this->sax);}

  function 
compter($fic_name) {
      
$this->nb 0;
    
$fic=file_get_contents($fic_name);
    
xml_parse($this->sax,$fic,TRUE);
  }

  function 
deb_elem($sax$nom$att) {
    if (
$nom=='livre')
      if (
$att['annee']>1960$this->nb += 1;
  }
  
  function 
fin_elem($sax$nom) {}
  
  function 
defaut($sax,$data) {}
}

$nbl = new nbl_sax();
$nbl->compter("ex_asimov.xml");
echo 
$nbl->nb;
?>

En utilisant la bibliothèque Sax4PHP, le code devient :


<?php 
header
('Content-type: text/plain');
include(
'Sax4PHP/Sax4php.php');

class 
nbl_sax2 extends DefaultHandler {
  public 
$nb;
  
  function 
startDocument() {$this->nb 0;}
  
  function 
startElement($nom$att) { 
      if (
$nom=='livre'
        if (
$att['annee']>1960
          
$this->nb += 1;}
}

$fic file_get_contents('ex_asimov.xml');

try {
    
$nbl = new nbl_sax2();
    
$sax = new SaxParser($nbl);
    
$sax->parse($fic);
    echo 
$nbl->nb;
}catch(
SAXException $e){  
    echo 
$e; }?>

Ce dernier est assez similaire à ce qui a été proposé en Java.

6. Conclusion

L'API SAX propose une approche assez particulière pour traiter des ressources XML, pas toujours aisée à maîtriser. Cependant, elle possède des caractéristiques très intéressantes. Le principal atout est de ne pas nécessiter une mise en mémoire de l'intégralité du document. Seules les informations des unités syntaxiques courantes sont en mémoire à un moment donné. Aussi, le traitement de gros documents est plus facile qu'avec DOM qui demande de tout charger en mémoire avant toute manipulation. Le traitement (et la génération de résultats) peut être entamé dès le début de la lecture, alors que l'ensemble du document n'est pas encore chargé. De plus, dans le cas où la ressource XML sert à renseigner une structure de donnée métier, il n'y a pas duplication de l'information en mémoire (DOM et structure de données).

Par contre, SAX n'autorise que des traitements quasi-linéaires (les références avant/arrière sont difficiles). De plus, la détection des erreurs de validation sont tardives. Il convient donc, en cas d'erreur, de pouvoir gérer les traitements déjà effectués. Pour terminer, il est nécessaire de définir une méthode spécifique pour la sérialisation.

SAX est donc bien adapté pour le chargement de structures de données, des actions de filtrage XML, des transformations simples, etc. Méthode événementielle, c'est une API légère permettant de traiter des données "à la volée". Méthode efficace, elle sert aussi... au chargement de DOM en mémoire !

StAX est une autre API pour traiter du XML. Cette API est très proche de SAX (Java). Son principe est basé sur la gestion d'événements. Les événements produits sont des objets. StAX propose aussi des classes pour construire des documents XML. Pour en savoir plus, il faut consulter : http://stax.codehaus.org, http://hikage.developpez.com/java/articles/xml/stax/ et http://www.xml.com/pub/a/2003/09/17/stax.html


Exercices et tests [1]

Exercice 1

Soit le document exo_sax_01.xml :

Écrire un programme utilisant l'API SAX (en Java ou en PHP) pour générer une page HTML de type "Contact".

Faire la même chose avec le document exo_sax_02.xml. aide

Solution

Exercice 2

Soit le document exo_sax_03.xml.

Écrire un programme utilisant l'API SAX (en Java ou en PHP) pour générer une page HTML de type "Contact", en indiquant l'institut et son adresse en taille de police assez petite ("x-small"). Aide

Faire la même chose avec le document exo_sax_04.xml où un contact peut avoir plusieurs prénoms (mais un seul prénom principal), mais il ne faut afficher que le prénom principal.

Solution

Exercice 3

La DTD edt.dtd est un modèle pour des documents qui mémorisent l'emploi du temps d'un groupe d'édudiants au cours d'une année universitaire. Un exemple est présenté dans edt1213.xml. Cet exemple est le même que celui proposé pour apprendre à manipuler DOM.

En utilisant en PHP/SAX ou PHP/Sax4PHP pour le traitement, donner l'emploi du temps pour une matière pour la semaine donnée selon la DTD semaine1.dtd. Le résultat, produit par des "echo", devra afficher les jours présents dans l'emploi du temps.

Remarques :

Par exemple, si l'on veut l'emploi du temps en semaine 12 pour "Projet XML", il faut faire l'URL "http://.../semaine_dom.php?no=s12&matiere=Projet%20XML". Cela doit produire le document ci-dessous.

<emploi-du-temps semaine='s12' matière='Projet XML'>
  <jour nom='lu' date='19/03/2007'/>
  <jour nom='me' date='21/03/2007'>
    <enseignement heure-début='11:00:00' type='TP'/>
  </jour>
  <jour nom='je' date='22/03/2007'>
    <enseignement heure-début='09:30:00' type='TP'/>
    <enseignement heure-début='11:00:00' type='TP'/>
  </jour>
  <jour nom='ve' date='23/03/2007'/>
</emploi-du-temps>
Aide

Solution

Cet exercice est la version en SAX de l'exercice 5 de la section DOM PHP/Java. Comme exercices complémentaires, il est intéressant de faire les autres exercices de cette section (n'afficher que les jours avec des cours, afficher tous les jours de la semaine systématiquement, etc.) avec "la vision SAX".

Exercice 4

On souhaite décrire à l'aide d'XML des séries de calculs. La DTD suivante permet de décrire de telles séries.

<!ENTITY % expr "(const|somme|mult|div|sous)">
<!ELEMENT calculs (%expr;)*>
<!ELEMENT const (#PCDATA)>
<!ELEMENT somme (%expr;,%expr;)>
<!ELEMENT mult (%expr;,%expr;)>
<!ELEMENT div (%expr;,%expr;)>
<!ELEMENT sous (%expr;,%expr;)>

Une série de calculs est une liste composée d'expressions. Une expression est une constante ("const"), la somme d'un ensemble d'expressions ("somme"), la multiplication d'un ensemble d'expressions ("mult"), la division d'une expression par une autre ("div"), la soustraction d'une expression par une autre ("sous").

Donner le document XML décrivant la série de calculs suivants :

Donner la séquence d'événements SAX issue de ce document.

On désire obtenir le résultat sous forme d'un document XML. Ce document pourra être validé par la DTD "res.dtd" suivante :

<!ELEMENT résultat (valeur|erreur)* >
<!ELEMENT valeur (#PCDATA) >
<!ELEMENT erreur EMPTY>
<!ATTLIST erreur code (div0|nonBinaire|autre) #REQUIRED>

Écrire un programme PHP/Sax4PHP permettant d'effectuer une série de calculs et d'afficher les résultats. Attention à ne pas afficher les résultats intermédiaires.

Remarque : pour le calcul, vous pourrez, par exemple, utiliser une (des) pile(s). Pour cet exercice, vous utiliserez PHP/SAX et la bibliothèque Sax4PHP (donc pas de XPATH) pour l'extraction des informations.

Indiquer les modifications à effectuer si la DTD devient la suivante :

<!ENTITY % expr "(const|somme|mult|div|sous)">
<!ELEMENT calculs (%expr;)*>
<!ELEMENT const (#PCDATA)>
<!ELEMENT somme (%expr;+)>
<!ELEMENT mult (%expr;+)>
<!ELEMENT div (%expr;,%expr;)>
<!ELEMENT sous (%expr;,%expr;)>

Notes

1. Dans cette section, ne seront présentées comme solution que les parties liées au traitement des événements. Des plus, les solutions seront données en utilisant PHP et la bibliothèque Sax4PHP.