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.
| Aspect | SAX | DOM |
|---|---|---|
| Mémoire | Constante, indépendante de la taille du fichier | Proportionnelle à la taille du document |
| Modèle | Push : le parseur appelle vos callbacks | Pull/arbre : vous parcourez l'arbre chargé |
| Navigation | En avant uniquement, passage unique | Accès aléatoire, dans n'importe quelle direction |
| Modification | Lecture seule | Lecture et écriture |
| Idéal pour | Fichiers volumineux, extraction d'un sous-ensemble | Documents 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) :
| Callback | Dé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.
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 : chaqueendElementpourpricese déclenche exactement une fois, dans l'ordre d'apparition des livres, sans jamais être désordonné. books seen : 3résulte de l'incrémentation d'un compteur dansstartElementpour 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 : 2est lu depuis l'attributstockviaattr.getValue("stock"), disponible uniquement dansstartElement. Le livreb2astock="0"et est exclu, donc deux des trois sont retenus.total price : 135.50est la somme de45.00 + 38.50 + 52.00, accumulée en lisant le texte de chaque élément<price>dans sonendElement. Récupérer le texte à la fin de l'élément (et non danscharacters) est le pattern sûr, carcharacterspeut livrer le texte en plusieurs morceaux.- L'intégralité du document a été fournie via un
ByteArrayInputStreamet 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 :
| Callback | Signification | L'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éma | Oui, 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());
}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.