Scraping

Les sitemaps comme source de découverte : trouver chaque URL sans explorer tout le site

Les sitemaps XML listent les URL d'un site, et parfois leur date de modification. Comment les lire correctement, et pourquoi il ne faut jamais faire confiance à lastmod sans vérification.

James Meadow

James Meadow

29 septembre 2026 · 10 min de lecture

La plupart des crawlers découvrent les pages de la manière la plus coûteuse : récupérer une page, en extraire les liens, les mettre en file d’attente, recommencer. Cela fonctionne, mais cela consacre l’essentiel du budget à la navigation, aux pages de listing et aux pages déjà vues, et peut malgré tout manquer des pages vers lesquelles rien ne pointe.

De nombreux sites publient délibérément une liste de leurs URL. Les sitemaps XML existent pour que les moteurs de recherche puissent trouver les pages efficacement, et ce même fichier indique à une équipe data ce qui existe sur un site, comment il est organisé et, parfois, ce qui a récemment changé. Ce guide explique comment trouver et lire correctement les sitemaps, et pourquoi le champ le plus utile qu’ils contiennent, lastmod, doit être vérifié avant d’y accorder sa confiance. Nous l’avons vérifié sur deux sites réels, et il a échoué de deux manières différentes.

Points clés

  • Un sitemap liste jusqu’à 50 000 URL par fichier, et un index de sitemaps peut lister jusqu’à 50 000 sitemaps, si bien que même les très grands sites peuvent publier un inventaire complet.
  • Les sitemaps sont généralement déclarés dans le robots.txt via une ligne Sitemap: ; vérifiez-la avant de deviner un emplacement.
  • lastmod est le champ qui pourrait vous épargner le plus de récupérations, et c’est le moins fiable. Google indique qu’il n’utilise lastmod que si c’est « de façon cohérente et vérifiable » exact.
  • Dans notre vérification, le sitemap d’actualités d’un grand éditeur de presse attribuait à chaque entrée le même horodatage, celui du moment de génération du fichier. Une vaste archive documentaire publiait plus de 10 000 URL sans aucun lastmod.
  • Utilisez les sitemaps pour la découverte, vérifiez lastmod site par site avant de planifier dessus, et conservez le crawling par liens comme filet de sécurité.

Ce que le protocole permet

Le protocole des sitemaps est court et mérite d’être connu précisément :

RègleDétail
Limites de taille« pas plus de 50 000 URL » et « pas plus de 50 Mo (52 428 800 octets) » par fichier sitemap, non compressé
Index de sitemapsUn fichier listant d’autres sitemaps, avec les mêmes limites de 50 000 entrées et 50 Mo
CompressionLes fichiers peuvent être compressés en gzip, tant qu’ils restent dans la limite une fois décompressés
lastmodFacultatif, au format W3C Datetime, qui peut être une simple date, du type YYYY-MM-DD
DécouverteUne ligne Sitemap: dans le robots.txt ; un site peut en lister plusieurs

Deux champs facultatifs que vous rencontrerez, changefreq et priority, peuvent être ignorés. Google indique clairement qu’il « ignore les valeurs <priority> et <changefreq> », et il y a peu de raisons pour quiconque d’autre de leur accorder confiance non plus.

Les sites d’actualités publient souvent un sitemap d’actualités séparé, couvrant uniquement les articles récents, avec des champs supplémentaires comme une date de publication. Ces fichiers sont petits, régénérés fréquemment, et très utiles pour suivre le contenu récent.

Bien lire les sitemaps

Un lecteur robuste doit faire quatre choses : trouver les sitemaps depuis le robots.txt, gérer le gzip, suivre récursivement les index de sitemaps, et parser lastmod en quelque chose de comparable. Il doit aussi plafonner le nombre de fichiers récupérés, car un index peut pointer vers des milliers d’entre eux.

import gzip
import io
import re
import urllib.request
from datetime import datetime, timezone

from defusedxml.ElementTree import fromstring  # pip install defusedxml

NS = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
UA = "Mozilla/5.0 (compatible; sitemap-reader)"
MAX_BYTES = 52_428_800  # la limite du protocole de 50MB, non compressé


def fetch(url, opener=None):
    opener = opener or urllib.request.build_opener()
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with opener.open(req, timeout=45) as r:
        body = r.read(MAX_BYTES + 1)
    if body[:2] == b"\x1f\x8b":
        body = gzip.GzipFile(fileobj=io.BytesIO(body)).read(MAX_BYTES + 1)
    if len(body) > MAX_BYTES:
        raise ValueError(f"{url} exceeds the 50MB sitemap limit")
    return body


def sitemap_roots(origin, opener=None):
    """Sitemaps déclarés dans robots.txt, avec repli sur /sitemap.xml."""
    try:
        robots = fetch(origin.rstrip("/") + "/robots.txt", opener).decode("utf-8", "replace")
        found = re.findall(r"(?im)^\s*sitemap:\s*(\S+)", robots)
    except OSError:
        found = []
    return found or [origin.rstrip("/") + "/sitemap.xml"]


def parse_lastmod(value):
    if not value:
        return None
    value = value.strip().replace("Z", "+00:00")
    try:
        dt = datetime.fromisoformat(value)
    except ValueError:
        return None
    return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)


def walk(url, opener=None, max_files=20, _seen=None):
    """Yield (loc, lastmod) from a sitemap or sitemap index, following nested indexes."""
    seen = _seen if _seen is not None else set()
    if url in seen or len(seen) >= max_files:
        return
    seen.add(url)
    root = fromstring(fetch(url, opener))
    if root.tag == NS + "sitemapindex":
        children = sorted(root.findall(NS + "sitemap"),
                          key=lambda s: parse_lastmod(s.findtext(NS + "lastmod")) or datetime.min.replace(tzinfo=timezone.utc),
                          reverse=True)
        for child in children:
            yield from walk(child.findtext(NS + "loc").strip(), opener, max_files, seen)
    else:
        for u in root.findall(NS + "url"):
            yield u.findtext(NS + "loc").strip(), parse_lastmod(u.findtext(NS + "lastmod"))


def changed_since(origin, since, opener=None, max_files=20):
    """URLs whose sitemap lastmod is newer than `since`, plus those with no lastmod at all."""
    changed, undated, total = [], [], 0
    for root in sitemap_roots(origin, opener):
        for loc, lastmod in walk(root, opener, max_files):
            total += 1
            if lastmod is None:
                undated.append(loc)
            elif lastmod > since:
                changed.append((loc, lastmod))
    return {"total": total, "changed": changed, "undated": undated}

Le parcoureur d’index visite les sitemaps enfants du plus récent au plus ancien lorsque l’index fournit des dates, si bien qu’une exécution plafonnée lit d’abord les parties les plus récemment mises à jour d’un grand site. Deux détails sont là par sécurité. Un sitemap est une entrée non fiable provenant du serveur de quelqu’un d’autre, le code le parse donc avec defusedxml, qui refuse les astuces d’entités capables de faire gonfler un petit fichier à plusieurs gigaoctets avec les parseurs XML standards, et il plafonne à la fois le téléchargement et la taille décompressée à la limite de 50 Mo du protocole, si bien qu’un fichier compressé ne peut pas gonfler sans limite. Le paramètre opener permet de faire passer les requêtes par un proxy quand il faut voir une version d’un site spécifique à un marché, la même approche que pour toute autre récupération.

Ne faire confiance à lastmod qu’après vérification

lastmod promet exactement ce dont la collecte incrémentale a besoin : une liste de ce qui a changé depuis la dernière exécution. Si c’était fiable partout, un crawler pourrait ne récupérer que les pages modifiées et ignorer tout le reste. Ce n’est pas fiable partout, et nos deux sites de test illustrent les deux échecs courants.

Premier échec : lastmod correspond à l’heure de génération. Nous avons lu le sitemap d’actualités d’un grand éditeur de presse. Chaque entrée datée portait l’un de deux horodatages, à une seconde d’écart : le moment de génération du fichier, pas le moment où chaque article a changé. Filtrer sur « modifié dans les six dernières heures » renvoyait toutes les URL du fichier. Utilisé naïvement, ce lastmod déclencherait une re-récupération de tout, à chaque fois.

Second échec : aucun lastmod du tout. Nous avons lu le sitemap d’une vaste archive de documents techniques. Elle listait 10 236 URL, et aucune ne portait de lastmod. C’est une excellente source de découverte, mais d’aucune aide pour décider quoi re-récupérer.

C’est pourquoi les propres consignes de Google indiquent qu’il n’utilise lastmod que « si c’est de façon cohérente et vérifiable (par exemple en comparant à la dernière modification de la page) exact », et pourquoi vous devriez appliquer le même test vous-même. Avant de planifier sur le lastmod d’un site :

  1. Vérifiez la répartition. Si la plupart des entrées partagent un même horodatage, ou si les valeurs suivent l’heure de génération du fichier, le champ ne décrit pas les changements de page.
  2. Échantillonnez et comparez. Récupérez un échantillon de pages et comparez lastmod avec un signal indépendant, comme le dateModified propre à la page dans les données structurées ou une empreinte de contenu. La façon d’extraire dateModified est traitée dans stop parsing HTML, et la façon de comparer le contenu sans se noyer dans le bruit dans change detection at scale.
  3. Notez le site. Enregistrez, par site, la fréquence à laquelle lastmod a changé quand le contenu a changé, et la fréquence à laquelle le contenu a changé quand lastmod n’a pas changé. N’utilisez lastmod pour la planification que sur les sites qui réussissent ce test, et continuez à vérifier, car le générateur de sitemap d’un site peut changer sans préavis.

La place des sitemaps dans un pipeline de collecte

Les sitemaps sont les plus utiles en première étape, pas comme seule étape :

  • Découverte. Un sitemap donne directement l’inventaire, y compris les pages mal reliées par des liens. Pour les sites de catalogue et de contenu, c’est souvent la liste d’URL la plus complète disponible.
  • Segmentation. Les sitemaps sont couramment découpés par type de contenu ou par section, comme les produits, catégories, articles et vidéos. Ce découpage indique comment un site est organisé avant même de récupérer une seule page.
  • Collecte incrémentale, sur les sites dont le lastmod passe les vérifications ci-dessus, et via les sitemaps d’actualités pour les articles récents.
  • Vérifications de couverture. Comparer ce que votre crawler a trouvé avec ce que liste le sitemap montre ce qui manque.

Conservez le crawling par liens comme filet de sécurité. Les sitemaps peuvent être obsolètes, incomplets, ou délibérément limités à ce qu’un site souhaite voir indexé. Et respectez le robots.txt du site pour les pages elles-mêmes : qu’une URL apparaisse dans un sitemap est une invitation aux moteurs de recherche à l’indexer, pas une renonciation à tout autre demande du site, comme traité dans robots.txt, AI opt-outs and reservation signals.

Utilisés ainsi, les sitemaps réduisent le budget de récupération aux deux endroits où il est habituellement gaspillé : naviguer pour trouver des pages, et re-récupérer des pages qui n’ont pas changé. La seconde économie dépend entièrement de la fiabilité de lastmod, c’est pourquoi cost-aware crawl scheduling devrait traiter un lastmod non vérifié comme un indice, pas comme un fait.

L’essentiel

Les sitemaps sont l’inventaire d’URL le moins coûteux du web : un site qui vous indique, dans un format standard, ce qu’il souhaite voir trouvé. Lisez-les depuis le robots.txt, gérez le gzip et les index imbriqués, plafonnez ce que vous récupérez, et utilisez-les d’abord pour la découverte.

Traitez ensuite lastmod comme le fait Google : utile seulement quand il a été démontré exact pour ce site. Sur un site que nous avons vérifié, ce n’était que l’heure de génération du fichier. Sur un autre, il n’existait pas. Sur un site où il tient la route, il peut réduire considérablement les re-récupérations. Le seul moyen de savoir à quel type de site on a affaire est de vérifier.

Sources et références

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de 0,75 $/GB.

Commencer