Le XML est encore partout : flux RSS et Atom, sitemaps des sites web, réponses SOAP et de nombreuses API REST, exports de systèmes de gestion (ERP), formats d'échange bancaires et administratifs. Python propose plusieurs façons d'analyser du XML — du module intégré ElementTree au rapide lxml, en passant par le parsing SAX en flux pour les fichiers géants. Voyons quand choisir quoi.
Cet article est un approfondissement thématique du guide général « Le web scraping avec Python ». Si le XML arrive mêlé à des pages HTML, commencez par ce guide ; ici, le focus est mis uniquement sur le XML.
Sommaire
- En quoi le XML diffère du HTML
- ElementTree — le module intégré
- lxml.etree — rapide et avec XPath
- Espaces de noms (namespaces)
- En pratique : flux RSS et sitemap
- Caractères spéciaux et encodages en XML
- Gros fichiers : parsing en flux
- Création et écriture de XML
- Sécurité du parsing XML
- Avantages et inconvénients des approches
1. En quoi le XML diffère du HTML
Bien que les deux soient des langages de balisage, la différence est fondamentale :
- Le XML est strict : chaque balise doit être fermée, la casse compte, la structure doit être valide. Le HTML pardonne les erreurs.
- En XML, il n'y a pas de balises prédéfinies — c'est vous (ou le format) qui les définissez :
<book>,<price>,<author>. - Le XML utilise souvent des espaces de noms pour séparer les vocabulaires.
Grâce à cette rigueur, le XML se parse de façon plus prévisible que le HTML, mais la moindre erreur de balisage fait échouer l'analyse. Pour du XML invalide, le mode « tolérant » de lxml vient à la rescousse.
Gardez aussi en tête la tendance générale : dans les API modernes, le XML a été largement supplanté par le JSON, plus léger, et si la source propose un endpoint JSON, il est généralement plus simple à exploiter — les techniques sont rassemblées dans « Parser du JSON en Python ». Mais là où le XML reste le standard (RSS, sitemaps, SOAP, formats administratifs, exports d'ERP), impossible de faire l'impasse sur son analyse.
2. ElementTree — le module intégré
xml.etree.ElementTree fait partie de la bibliothèque standard — aucune dépendance à installer. C'est suffisant pour la majorité des tâches.
import xml.etree.ElementTree as ET
xml_data = """
<catalog>
<book id="1">
<title>Python pour les débutants</title>
<price>590</price>
</book>
<book id="2">
<title>Extraction de données</title>
<price>720</price>
</book>
</catalog>
"""
root = ET.fromstring(xml_data) # depuis une chaîne
# root = ET.parse("catalog.xml").getroot() # depuis un fichier
for book in root.findall("book"):
title = book.find("title").text
price = book.find("price").text
book_id = book.get("id") # attribut
print(book_id, title, price)
Méthodes clés :
find("tag")— premier élément enfant portant la balise ;findall("tag")— tous ces éléments ;.text— texte contenu dans l'élément ;.get("attr")— valeur d'un attribut ;.attrib— dictionnaire de tous les attributs.
ElementTree prend en charge un XPath limité :
root.findall(".//book") # tous les book, à toute profondeur
root.findall("book[@id='1']") # par attribut
root.findall(".//price")
3. lxml.etree — rapide et avec XPath complet
Quand la vitesse ou un XPath complet est nécessaire, on prend lxml.etree. L'API est quasi identique à ElementTree — la transition est transparente.
from lxml import etree
root = etree.fromstring(xml_data.encode("utf-8"))
# XPath complet
titles = root.xpath("//book/title/text()")
expensive = root.xpath("//book[price > 600]/title/text()")
first_id = root.xpath("//book[1]/@id")
lxml offre ce qui manque à ElementTree : XPath 1.0 complet avec fonctions (contains, starts-with, comparaisons), axes (parent, sibling) et transformations XSLT. Pour ses capacités côté HTML, voir l'article consacré à lxml.
Astuce : avec lxml, encodez la chaîne en octets (
.encode("utf-8")) avantfromstring, sinon la présence d'une déclaration<?xml encoding=...?>dans le XML peut provoquer une erreur.
4. Espaces de noms (namespaces)
C'est la pierre d'achoppement des débutants. Si le XML contient xmlns, un simple find("title") ne trouvera rien — les balises sont « cachées » dans un espace de noms.
<feed xmlns="http://www.w3.org/2005/Atom">
<entry><title>Actualité</title></entry>
</feed>
Il faut indiquer l'espace de noms explicitement :
import xml.etree.ElementTree as ET
root = ET.fromstring(xml_data)
ns = {"atom": "http://www.w3.org/2005/Atom"}
# via un dictionnaire de préfixes
titles = root.findall(".//atom:title", ns)
for t in titles:
print(t.text)
Avec lxml, c'est plus pratique — on peut utiliser local-name() et ignorer l'espace de noms :
from lxml import etree
root = etree.fromstring(xml_bytes)
# on récupère title quel que soit le namespace
titles = root.xpath("//*[local-name()='title']/text()")
local-name() est une bouée de sauvetage quand les espaces de noms sont nombreux ou inconnus à l'avance.
5. En pratique : flux RSS et sitemap
Flux RSS
import requests
import xml.etree.ElementTree as ET
resp = requests.get("https://example.com/rss", timeout=10)
root = ET.fromstring(resp.content) # .content — des octets
for item in root.findall(".//item"):
title = item.findtext("title")
link = item.findtext("link")
date = item.findtext("pubDate")
print(title, link, date)
findtext est plus pratique que find().text — il ne plante pas avec une AttributeError si l'élément est absent, mais renvoie None ou la valeur par défaut indiquée.
Sitemap (plan du site)
Un sitemap comporte presque toujours un espace de noms — piège fréquent lors de la collecte de toutes les URL d'un site :
import requests
from lxml import etree
resp = requests.get("https://example.com/sitemap.xml", timeout=10)
root = etree.fromstring(resp.content)
# on contourne le namespace avec local-name()
urls = root.xpath("//*[local-name()='loc']/text()")
print(f"{len(urls)} URL trouvées")
Le sitemap est un excellent point de départ pour un crawler : la liste de toutes les pages du site sans parcourir les liens. Sur le stockage de ces URL dans une file d'attente, voir le guide central, section « Files d'attente ». Quand ces flux ou fichiers sitemap sont nombreux (sur les grands sites, le sitemap est découpé en dizaines de parties), il est judicieux de les télécharger en parallèle — voir « Le scraping asynchrone en Python ».
6. Caractères spéciaux et encodages en XML
En XML, l'encodage est déclaré dans le prologue :
<?xml version="1.0" encoding="windows-1251"?>
La règle d'or : passer au parseur des octets, pas une chaîne, pour qu'il lise lui-même cette déclaration :
# CORRECT : des octets
root = ET.fromstring(resp.content)
# ERREUR : si la chaîne a déjà été mal décodée — caractères illisibles
root = ET.fromstring(resp.text)
Erreur lxml typique : ValueError: Unicode strings with encoding declaration are not supported. Elle survient quand vous passez une chaîne déjà décodée pour un document contenant une déclaration d'encodage. La solution : passer des octets :
from lxml import etree
root = etree.fromstring(resp.content) # des octets, pas resp.text
Si l'encodage indiqué dans le prologue est erroné (cela arrive dans les exports de systèmes anciens), ré-encodez manuellement :
text = resp.content.decode("windows-1251", errors="replace")
# on supprime le prologue problématique et on ré-encode
text = text.replace('encoding="windows-1251"', 'encoding="utf-8"')
root = etree.fromstring(text.encode("utf-8"))
Pour la théorie générale des encodages, voir le guide central, section « Encodages ».
7. Gros fichiers : parsing en flux
Les exports de plusieurs centaines de mégaoctets (flux produits, dumps) ne peuvent pas être chargés entièrement en mémoire. Deux approches en flux.
iterparse de lxml — l'option recommandée
from lxml import etree
for event, elem in etree.iterparse("huge_feed.xml", tag="offer"):
title = elem.findtext("name")
price = elem.findtext("price")
process(title, price)
elem.clear() # on libère la mémoire des éléments traités
while elem.getprevious() is not None:
del elem.getparent()[0]
Le duo clear() + suppression des nœuds précédents maintient une consommation mémoire quasi constante, quelle que soit la taille du fichier. Cela permet d'analyser des fichiers de plusieurs dizaines de gigaoctets.
SAX — le parsing événementiel
Le module intégré xml.sax appelle vos callbacks à chaque balise ouvrante/fermante. Il consomme un minimum de mémoire, mais le code est plus verbeux et moins pratique que iterparse. En pratique, iterparse de lxml couvre presque tous les besoins de traitement en flux.
8. Création et écriture de XML
Le parsing va souvent de pair avec la génération — par exemple pour enregistrer le résultat en XML.
from lxml import etree
root = etree.Element("catalog")
book = etree.SubElement(root, "book", id="1")
etree.SubElement(book, "title").text = "Web scraping avec Python"
etree.SubElement(book, "price").text = "590"
xml_bytes = etree.tostring(
root,
pretty_print=True,
xml_declaration=True,
encoding="utf-8",
)
with open("output.xml", "wb") as f: # 'wb' — des octets !
f.write(xml_bytes)
encoding="utf-8" dans tostring garantit l'enregistrement correct des caractères accentués et spéciaux, et l'écriture en mode "wb" assure que les octets ne seront pas ré-encodés une seconde fois.
9. Sécurité du parsing XML
Un XML provenant de sources non fiables peut contenir des attaques — par exemple « billion laughs » (expansion exponentielle d'entités qui épuise la mémoire) ou des entités externes (XXE) qui lisent des fichiers sur votre serveur.
La protection : le paquet defusedxml :
pip install defusedxml
from defusedxml.ElementTree import fromstring # remplacement sécurisé
root = fromstring(untrusted_xml)
Si vous analysez du XML provenant de sources externes non maîtrisées, utilisez defusedxml à la place des parseurs standard. Cela neutralise les principales classes d'attaques XML.
10. Avantages et inconvénients des approches
| Outil | Avantages | Inconvénients |
|---|---|---|
| ElementTree | intégré, sans dépendances, API simple | XPath limité, plus lent que lxml |
| lxml.etree | rapide, XPath complet, iterparse, XSLT | dépendance externe, plus strict sur les erreurs |
| xml.sax | mémoire minimale sur les fichiers énormes | verbeux, peu pratique à développer |
| defusedxml | protection contre les attaques XML | uniquement pour l'analyse, pas pour l'écriture |
Comment choisir :
- Petit XML, sans dépendances superflues → ElementTree.
- Besoin de vitesse, de XPath avancé ou de namespaces → lxml.etree.
- Le fichier ne tient pas en mémoire → iterparse (lxml).
- XML d'une source non fiable → defusedxml.