W3docs

Parseur XML SAX en Java

Analysez de grands documents XML en Java avec le parseur SAX orienté événements, sans charger tout le document en mémoire.

SAX (Simple API for XML) est le parseur XML orienté événements et en flux continu du JDK. Au lieu de construire un arbre en mémoire comme le fait DOM, SAX lit le document une seule fois du début à la fin et vous envoie des événements — « élément ouvert », « texte rencontré », « élément fermé » — que vous traitez au fur et à mesure. Comme il ne retient jamais l'intégralité du document, SAX analyse des fichiers de toute taille avec une quantité de mémoire constante et minime. Il réside dans org.xml.sax et est créé via javax.xml.parsers.SAXParserFactory, tous deux faisant partie du JDK standard sans rien à installer.

Cette page explique en quoi l'analyse par événements diffère de la construction d'un arbre, la configuration factory-et-handler, les callbacks à surcharger, la gestion de l'état entre les événements, la gestion des erreurs, ainsi qu'un exemple complet exécutable. Si vous débutez avec XML en Java, commencez par l'introduction à XML ; lorsque vous avez besoin d'un accès aléatoire ou souhaitez modifier un document, utilisez plutôt le parseur DOM.

Analyse par événements vs. construction d'un arbre

Un parseur DOM lit l'intégralité du document et vous remet un objet Document navigable — pratique, mais il doit faire tenir chaque nœud en mémoire. SAX inverse le contrôle : le parseur mène la danse, en appelant des méthodes sur votre handler au fur et à mesure qu'il rencontre chaque morceau de balisage. Vous ne conservez que l'état qui vous intéresse. La contrepartie est que vous ne pouvez pas revenir en arrière ni anticiper — vous voyez chaque événement exactement une fois, dans l'ordre du document.

AspectSAXDOM
MémoireConstante, indépendante de la taille du fichierProportionnelle à la taille du document
ModèlePush : le parseur appelle vos callbacksPull/arbre : vous parcourez l'arbre chargé
NavigationEn avant uniquement, passage uniqueAccès aléatoire, dans n'importe quelle direction
ModificationLecture seuleLecture et écriture
Idéal pourFichiers volumineux, extraction d'un sous-ensembleDocuments petits/moyens à modifier

La factory et le handler

Deux types font presque tout le travail. SAXParserFactory crée un SAXParser, et vous sous-classez DefaultHandler pour recevoir les événements. DefaultHandler implémente chaque callback comme une opération nulle, vous n'avez donc à surcharger que ceux dont vous avez besoin :

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);          // optional: report namespace URIs
SAXParser parser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {
  @Override
  public void startElement(String uri, String localName, String qName, Attributes attr) {
    System.out.println("start <" + qName + ">");
  }
};
parser.parse(new File("data.xml"), handler);

Les callbacks principaux

Voici les méthodes de ContentHandler que vous surchargez le plus souvent (DefaultHandler les fournit toutes) :

CallbackDéclenché quand
startDocument() / endDocument()L'analyse commence / se termine
startElement(uri, localName, qName, attr)Une balise ouvrante est lue ; attr contient ses attributs
endElement(uri, localName, qName)Une balise fermante est lue
characters(ch, start, length)Du contenu textuel est lu — éventuellement en plusieurs morceaux
error() / fatalError()Le document est mal formé ou invalide

Deux points piègent les débutants. D'abord, characters ne garantit pas la livraison de tout le texte d'un élément en un seul appel — le parseur peut le fractionner, donc vous accumulez dans un StringBuilder et le lisez dans endElement. Ensuite, les valeurs d'attribut ne sont disponibles que dans startElement, via l'argument Attributes :

@Override
public void startElement(String uri, String localName, String qName, Attributes attr) {
  String id = attr.getValue("id");           // by name
  for (int i = 0; i < attr.getLength(); i++) // or by index
    System.out.println(attr.getQName(i) + "=" + attr.getValue(i));
}

Suivi de l'état entre les événements

Puisque SAX ne vous fournit aucun arbre, vous maintenez le contexte. Un pattern courant consiste à utiliser un indicateur positionné dans startElement et réinitialisé dans endElement, plus un tampon de texte que vous réinitialisez au début de chaque élément et consommez à la fin de l'élément :

private final StringBuilder text = new StringBuilder();

@Override public void startElement(String u, String l, String q, Attributes a) {
  text.setLength(0);            // begin collecting fresh text
}
@Override public void characters(char[] ch, int start, int len) {
  text.append(ch, start, len);  // text may arrive in pieces
}
@Override public void endElement(String u, String l, String q) {
  if (q.equals("title")) System.out.println("title = " + text.toString().trim());
}

Un exemple pratique : comptabiliser un catalogue sans arbre

Ce programme analyse un petit catalogue de livres contenu dans un bloc de texte. Le handler compte les livres, compte combien sont en stock (lu depuis un attribut stock) et additionne chaque prix — tout en parcourant le document une seule fois en flux continu. Seules des classes du JDK sont utilisées.

java— editable, runs on the server

Ce que l'on retient de l'exécution :

  • Les trois lignes parsed: s'affichent dans l'ordre du document — Effective Java, Clean Code, Java Concurrency in Practice — prouvant que SAX effectue un seul passage en avant : chaque endElement pour price se déclenche exactement une fois, dans l'ordre d'apparition des livres, sans jamais être désordonné.
  • books seen : 3 résulte de l'incrémentation d'un compteur dans startElement pour chaque balise <book>. Le compteur vit dans votre handler, pas dans un arbre — SAX n'a conservé aucun nœud, seulement l'entier que vous avez choisi de suivre.
  • in stock : 2 est lu depuis l'attribut stock via attr.getValue("stock"), disponible uniquement dans startElement. Le livre b2 a stock="0" et est exclu, donc deux des trois sont retenus.
  • total price : 135.50 est la somme de 45.00 + 38.50 + 52.00, accumulée en lisant le texte de chaque élément <price> dans son endElement. Récupérer le texte à la fin de l'élément (et non dans characters) est le pattern sûr, car characters peut livrer le texte en plusieurs morceaux.
  • L'intégralité du document a été fournie via un ByteArrayInputStream et consommée une seule fois ; à aucun moment le programme n'a détenu un arbre DOM. C'est exactement pourquoi SAX passe à l'échelle pour des fichiers de plusieurs gigaoctets là où DOM épuiserait le tas.

Gestion du XML mal formé

SAX signale les problèmes via trois callbacks ErrorHandler, tous surchargeables sur DefaultHandler :

CallbackSignificationL'analyse continue-t-elle ?
warning(SAXParseException e)Problème mineur (ex. un avertissement DTD récupérable)Oui
error(SAXParseException e)Une erreur de validité contre un DTD/schémaOui, sauf si vous relancez l'exception
fatalError(SAXParseException e)Violation de la forme correcte (balisage cassé)Non — l'analyse s'arrête

Par défaut, parse() lève une SAXParseException en cas d'erreur fatale, donc envelopper l'appel dans un bloc try/catch suffit dans la plupart des cas. L'exception fournit getLineNumber() et getColumnNumber(), ce qui permet de localiser facilement le balisage incriminé :

try {
  parser.parse(new File("data.xml"), handler);
} catch (SAXParseException e) {
  System.err.println("bad XML at line " + e.getLineNumber()
      + ", column " + e.getColumnNumber() + ": " + e.getMessage());
}
Avertissement

Si votre handler lève une exception non vérifiée (par exemple une NumberFormatException lors de l'analyse d'un attribut), elle se propage directement hors de parse() et interrompt le flux. Validez ou protégez les valeurs d'attribut à l'intérieur du callback plutôt que de supposer que l'entrée est bien formée.

Pratique

Pratique
Dans un handler SAX, pourquoi accumule-t-on généralement le texte dans un StringBuilder dans characters() et le lit-on dans endElement(), plutôt que d'utiliser le texte directement dans characters()?
Dans un handler SAX, pourquoi accumule-t-on généralement le texte dans un StringBuilder dans characters() et le lit-on dans endElement(), plutôt que d'utiliser le texte directement dans characters()?
Was this page helpful?