Scraping

Arrêtez de parser du HTML : extraire des données structurées depuis JSON-LD et les données intégrées

La plupart des pages contiennent déjà des données lisibles par machine. Comment extraire le JSON-LD et le JSON intégré plutôt que d'utiliser des sélecteurs fragiles, et que faire quand ces données sont incomplètes.

Matt Brown

Matt Brown

26 septembre 2026 · 11 min de lecture

La plupart des scrapers sont construits de la même manière : ouvrir la page, trouver l’élément qui contient le prix, écrire un sélecteur CSS, répéter pour chaque champ. Cela fonctionne jusqu’à ce que le site déploie une refonte, renomme une classe, ou enveloppe le prix dans un nouveau composant. Le sélecteur ne renvoie alors plus rien, ou pire, renvoie la mauvaise chose, et le pipeline continue de tourner.

De nombreuses pages publient déjà les mêmes données sous une forme destinée aux machines. Les moteurs de recherche l’ont demandé, les sites l’ont fourni, et cela se trouve dans le code source de la page depuis le début. Ce tutoriel montre comment le trouver, l’extraire avec quelques dizaines de lignes de code, et gérer les cas où il est manquant ou incomplet, ce qui, comme le montre un exemple réel ci-dessous, arrive plus souvent que la documentation ne le laisse penser.

Points clés

  • Le JSON-LD était présent sur 41% des pages dans le Web Almanac 2024, contre 34% en 2022. Pour les produits, les articles, les événements et les organisations, c’est souvent la source la plus stable de la page.
  • Les données structurées changent beaucoup moins souvent que la mise en page, car les sites en dépendent pour les résultats de recherche. Les sélecteurs se cassent lors des refontes ; les données structurées y survivent généralement.
  • Elles ne sont pas toujours complètes. Une page produit peut publier des enregistrements complets pour la variante affichée à l’écran et seulement de simples liens pour toutes les autres variantes. Validez chaque enregistrement.
  • L’extracteur le plus robuste essaie d’abord les données structurées, puis le JSON intégré, et enfin les sélecteurs CSS, et note lequel il a utilisé.

Ce que signifie « données structurées » sur une page web

Il existe quatre endroits où résident habituellement les données lisibles par machine :

SourceÀ quoi cela ressembleContenu typique
JSON-LDblocs <script type="application/ld+json">objets schema.org : Product, Offer, NewsArticle, Organization, Event, BreadcrumbList
Microdataattributs itemprop sur les éléments visiblesLe même vocabulaire schema.org, réparti dans le balisage
Open Graph et balises meta<meta property="og:...">Titre, description, image, parfois le prix
État d’application intégréUn large objet JSON que le JavaScript de la page lit, tel que __NEXT_DATA__Souvent tout ce que la page affiche, et plus encore

Le Web Almanac 2024 de HTTP Archive a mesuré leur diffusion sur le web : le JSON-LD est passé « de 34% en 2022 à 41% en 2024 », le microdata est resté stable à 26%, et le RDFa et l’Open Graph, qui incluent les balises de partage social que la plupart des sites ajoutent, apparaissaient sur 66% et 64% des pages. Le JSON-LD est celui à privilégier en premier, car il s’agit d’un bloc de données autonome plutôt que d’attributs éparpillés dans la mise en page.

Pourquoi c’est plus fiable que les sélecteurs

Un sélecteur CSS dépend de l’apparence d’une page. Les données structurées dépendent de ce qu’une page signifie. Les sites changent constamment l’apparence de leurs pages. Ils changent beaucoup moins souvent ce que disent leurs données structurées, car celles-ci alimentent les résultats enrichis dans les moteurs de recherche, et les casser a un coût visible pour le site.

Cela échoue aussi de manière plus honnête. Un sélecteur qui cesse de correspondre peut silencieusement correspondre à un élément différent et renvoyer une valeur erronée mais plausible, le type d’erreur décrit dans le taux d’échec silencieux. Un objet JSON-LD contient un champ price ou il n’en contient pas, ce qui rend la validation simple.

Extraire le JSON-LD en Python

La bibliothèque standard suffit. Cet extracteur collecte chaque bloc JSON-LD, tolère ceux qui sont corrompus, et parcourt les conteneurs que les sites utilisent pour imbriquer les objets : les listes, @graph, et hasVariant, que schema.org utilise pour les variantes de produits.

import json
from html.parser import HTMLParser


class _Collector(HTMLParser):
    """Collect JSON-LD blocks and embedded JSON state from an HTML page."""

    def __init__(self):
        super().__init__()
        self.blocks, self._buf, self._kind = [], None, None

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "script" and a.get("type", "").lower() == "application/ld+json":
            self._buf, self._kind = [], "json-ld"
        elif tag == "script" and a.get("id") == "__NEXT_DATA__":
            self._buf, self._kind = [], "next-data"

    def handle_data(self, data):
        if self._buf is not None:
            self._buf.append(data)

    def handle_endtag(self, tag):
        if tag == "script" and self._buf is not None:
            raw = "".join(self._buf).strip()
            try:
                self.blocks.append((self._kind, json.loads(raw)))
            except json.JSONDecodeError:
                self.blocks.append((self._kind + "-invalid", raw[:200]))
            self._buf = self._kind = None


def _walk(node):
    """Yield every JSON-LD object, flattening lists and nested containers."""
    if isinstance(node, list):
        for item in node:
            yield from _walk(item)
    elif isinstance(node, dict):
        yield node
        for key in ("@graph", "mainEntity", "itemListElement", "hasVariant"):
            if key in node:
                yield from _walk(node[key])


def _types(obj):
    t = obj.get("@type", [])
    return {t} if isinstance(t, str) else set(t)


def jsonld_objects(html, wanted_type=None):
    """Every JSON-LD object on the page, optionally filtered by schema.org type."""
    collector = _Collector()
    collector.feed(html)
    objs = [o for kind, data in collector.blocks if kind == "json-ld" for o in _walk(data)]
    return [o for o in objs if wanted_type is None or wanted_type in _types(o)]

Sur un article en direct du Guardian, jsonld_objects(html, "NewsArticle") renvoie le titre, les horodatages de publication et de modification, et l’auteur, sans aucun sélecteur. Ces horodatages à eux seuls valent l’effort : ils sont exacts, lisibles par machine et cohérents à travers chaque article du site.

Normaliser les produits

Les produits demandent un peu plus de soin, car les prix se trouvent à l’intérieur d’offers imbriqués, parfois sous la forme d’une seule Offer et parfois d’une AggregateOffer avec une fourchette de prix.

def products(html):
    """Return normalised product records found in a page's JSON-LD."""
    out = []
    for obj in jsonld_objects(html, "Product"):
        offers = obj.get("offers") or {}
        offer = offers[0] if isinstance(offers, list) and offers else offers
        if isinstance(offer, dict) and "AggregateOffer" in _types(offer):
            price = offer.get("lowPrice")
        else:
            price = offer.get("price") if isinstance(offer, dict) else None
        brand = obj.get("brand")
        out.append({
            "name": obj.get("name"),
            "sku": obj.get("sku") or obj.get("gtin13") or obj.get("mpn"),
            "brand": brand.get("name") if isinstance(brand, dict) else brand,
            "price": float(price) if price not in (None, "") else None,
            "currency": offer.get("priceCurrency") if isinstance(offer, dict) else None,
            "availability": (offer.get("availability") or "").rsplit("/", 1)[-1] if isinstance(offer, dict) else None,
        })
    return out

Exécuté sur une page avec un produit et une offre standards, il renvoie des enregistrements propres tels que {"name": "Trail Runner", "sku": "TR-01", "brand": "Acme", "price": 89.0, "currency": "EUR", "availability": "InStock"}, quelle que soit la manière dont la page est stylée.

Quand les données structurées sont incomplètes : un exemple réel

Les exemples de documentation donnent l’impression que c’est facile. Les pages réelles sont plus désordonnées, et il vaut la peine d’en montrer une.

Nous avons exécuté l’extracteur sur la page produit d’une chaussure populaire sur une grande boutique Shopify. Le JSON-LD de la page décrivait un ProductGroup, le type schema.org pour un produit vendu en variantes, avec le nom du produit, la marque, la description et les images, et 49 variantes. Seules 7 d’entre elles, les tailles de la couleur affichée à l’écran, étaient des produits complets avec un prix de 100,00 $ et un statut de disponibilité. Les 42 autres, toutes les autres couleurs et tailles, étaient de simples références : un type et une URL, rien de plus. La note d’évaluation se trouvait dans un second bloc JSON-LD, séparé.

Ainsi, les données structurées décrivaient la page, pas le catalogue. Un pipeline qui aurait supposé que « toutes les variantes sont dans le JSON-LD » aurait silencieusement établi le prix d’une seule couleur et n’aurait rien enregistré pour les autres.

Le même magasin expose aussi une représentation JSON publique de chaque produit, qui correspondait bien : sept variantes pour cette couleur, chacune avec un prix et un indicateur de disponibilité. Mais là, le prix venait sous la forme 10000, en unités mineures. Un pipeline qui aurait mélangé les deux sources sans normalisation aurait enregistré une chaussure à 100 $ et la même chaussure à dix mille dollars.

Trois leçons en découlent, et elles s’appliquent bien au-delà de Shopify :

  • Validez, ne supposez pas. Un bloc JSON-LD qui s’analyse correctement n’est pas forcément un enregistrement complet. Vérifiez que chaque champ dont vous avez besoin est présent, et comparez le nombre obtenu à celui attendu.
  • Suivez les références lorsque les données structurées sont partielles. Les URL de variantes, le JSON intégré et les points de terminaison de produits publics comblent souvent les lacunes, et sont généralement plus propres que la page elle-même.
  • Normalisez les unités explicitement. Les unités mineures, les fourchettes de prix, les prix TTC et HT, et la devise doivent tous être gérés dans le code, et non supposés.

Une chaîne de repli qui note sa source

Assemblez les éléments en une chaîne : essayez d’abord les données structurées, puis le JSON intégré, puis les sélecteurs, et conservez une note indiquant lequel a produit chaque enregistrement.

REQUIRED = ("name", "price", "currency")


def extract_product(html, embedded=None, css_fallback=None):
    for source, candidates in (
        ("json-ld", products(html)),
        ("embedded-json", embedded(html) if embedded else []),
        ("css", css_fallback(html) if css_fallback else []),
    ):
        for record in candidates:
            if all(record.get(f) not in (None, "") for f in REQUIRED):
                return {**record, "source": source}
    return None

Le champ source se justifie rapidement. Quand un site qui produisait toujours des enregistrements json-ld commence à produire des enregistrements css, ses données structurées ont changé ou ont disparu, et vous voulez le savoir avant que le repli par sélecteur ne se casse aussi. C’est également une entrée utile pour un score de santé de cible.

Le faire avec la Web Scraping API

Si vous récupérez des pages via la Web Scraping API de Shifter, la même approche fonctionne sans avoir à exécuter vous-même un navigateur. Le paramètre extract_rules de l’API associe des sélecteurs CSS à des champs JSON, et sa sortie html renvoie le HTML interne d’un élément, de sorte qu’une règle qui sélectionne le script JSON-LD vous renvoie le bloc brut à analyser :

{
  "jsonld": { "selector": "script[type='application/ld+json']", "output": "html" }
}

Une seule règle renvoie le premier élément correspondant, donc pour les pages comportant plusieurs blocs JSON-LD, demandez le HTML complet et exécutez l’extracteur ci-dessus dessus. Combinez l’une ou l’autre approche avec render_js=1 pour les pages qui injectent leurs données structurées avec du JavaScript, et utilisez auto_parser=1 lorsque vous récupérez directement un point de terminaison JSON, comme une URL JSON de produit, pour récupérer le corps analysé. Les champs manquants reviennent sous forme de null plutôt que de faire échouer la requête, ce qui correspond au schéma valider-puis-replier décrit plus haut. La syntaxe complète se trouve dans la documentation des règles d’extraction.

Ce que les données structurées ne vous donneront pas

Les données structurées décrivent ce que le site a choisi de publier pour les moteurs de recherche. Elles peuvent avoir du retard sur la page visible, omettre des champs que le site ne se soucie pas d’exposer, ou décrire la variante par défaut plutôt que celle affichée à l’écran. Pour les prix en particulier, comparez-les à la page visible sur un échantillon, car un prix JSON-LD obsolète et un prix affiché à jour sont tous deux « corrects » selon des points de vue différents. Et pour les sites qui font varier le contenu selon la localisation du visiteur, les données structurées varient aussi, il faut donc les capturer depuis le marché qui vous intéresse, comme évoqué dans le scraping des prix de vols et d’hôtels.

En résumé

Avant d’écrire un sélecteur de plus, ouvrez le code source de la page et recherchez application/ld+json. Sur une large part du web, la donnée que vous cherchez s’y trouve déjà, étiquetée avec un vocabulaire partagé, et bien moins susceptible de changer que la mise en page qui l’entoure.

Extrayez-la en premier, validez-la, repliez-vous sur le JSON intégré puis sur les sélecteurs quand elle est incomplète, et notez de quelle source provient chaque enregistrement. Vos extracteurs se casseront moins souvent, et quand cela arrivera, ils vous le diront.

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