Comment Extraire les Données Schema.org et JSON-LD de N'importe Quelle Page Web
Vous écrivez des sélecteurs CSS pour une page produit. Ils fonctionnent pendant deux semaines. Puis le site lance une refonte, le prix passe dans un nouveau <span>, et votre scraper renvoie discrètement None pour chaque ligne.
Pendant ce temps, la même page contenait depuis le début son nom, son prix, sa devise, son état de stock et sa note dans un bloc JSON propre. Il est là pour Google, pas pour vous, mais rien ne vous empêche de le lire. Ce guide vous montre comment trouver ces données Schema.org, les extraire en Python, les transformer en un enregistrement propre et gérer les cas où elles vous font défaut.
Ce Que Sont les Données Structurées et Pourquoi les Sites les Publient
Schema.org est un vocabulaire partagé pour décrire des choses sur le web : Product, Offer, JobPosting, Recipe, Article, Event, LocalBusiness et des centaines d’autres. Les moteurs de recherche le lisent pour construire des résultats enrichis, les étoiles, les prix et les mentions de stock que vous voyez sous certains résultats. Les sites qui tiennent à leur trafic organique le publient sur leurs pages les plus importantes.
Il existe trois syntaxes :
- JSON-LD. Un bloc
<script type="application/ld+json">contenant un document JSON. C’est le format le plus courant aujourd’hui et celui que recommande Google. - Microdata. Des attributs comme
itemscope,itemtypeetitemproprépartis dans le HTML visible. Fréquent sur les boutiques et les templates plus anciens. - RDFa. Le même principe avec les attributs
vocab,typeofetproperty. Moins courant.
Beaucoup de pages contiennent aussi des balises meta Open Graph (og:title, og:image, og:price:amount). Ce n’est pas du Schema.org, mais c’est une solution de repli pratique pour les titres et les images.
Pourquoi est-ce important pour le scraping ? Les données structurées sont la partie la plus stable d’une page. Une refonte change les noms de classes, mais l’équipe SEO casse rarement le balisage qui alimente ses résultats enrichis. Un parser qui s’appuie dessus survit aux changements de mise en page qui tuent les scrapers basés sur des sélecteurs.
Comment les Trouver sur une Page
Ouvrez la page, affichez le code source (pas l’arbre des éléments des DevTools, qui montre le DOM après l’exécution du JavaScript) et cherchez :
application/ld+jsonpour les blocs JSON-LDitemprop=pour Microdataproperty="og:pour les balises Open Graph
Une page contient souvent plusieurs blocs JSON-LD : un pour l’organisation, un pour le fil d’Ariane, un pour le produit. Pour voir d’un coup d’oeil tout ce qu’une page publie, collez l’URL dans le Schema Markup Validator. Il liste chaque élément trouvé, dans toutes les syntaxes.
Extraire le JSON-LD en Python
Il vous faut requests et beautifulsoup4. La fonction ci-dessous récupère tous les noeuds JSON-LD d’une page et gère deux formes qui font échouer les premières tentatives : un bloc qui contient une liste d’objets, et un bloc qui enveloppe tout dans un tableau @graph.
import json
import requests
from bs4 import BeautifulSoup
def jsonld_nodes(html):
soup = BeautifulSoup(html, "html.parser")
nodes = []
for tag in soup.find_all("script", type="application/ld+json"):
raw = tag.string or tag.get_text()
if not raw or not raw.strip():
continue
try:
# strict=False accepte les retours à la ligne bruts dans les strings, une erreur courante
doc = json.loads(raw, strict=False)
except json.JSONDecodeError:
continue
for item in doc if isinstance(doc, list) else [doc]:
if isinstance(item, dict) and "@graph" in item:
nodes.extend(item["@graph"])
else:
nodes.append(item)
return [n for n in nodes if isinstance(n, dict)]
def has_type(node, wanted):
types = node.get("@type", [])
return wanted in (types if isinstance(types, list) else [types])
Notez que @type peut être une chaîne ou une liste. "@type": ["Product", "Car"] est valide, donc vérifiez toujours l’appartenance au lieu de comparer des chaînes.
Ou Lisez Toutes les Syntaxes d’un Coup avec extruct
Si vous scrapez beaucoup de sites différents, certains n’utiliseront que Microdata ou RDFa. La bibliothèque open source extruct les lit toutes en un seul appel :
import extruct
from w3lib.html import get_base_url
def all_structured_data(html, url):
return extruct.extract(
html,
base_url=get_base_url(html, url),
syntaxes=["json-ld", "microdata", "rdfa", "opengraph"],
uniform=True, # Microdata et Open Graph dans la même forme que JSON-LD
errors="ignore",
)
Avec uniform=True, les éléments Microdata arrivent avec @type et les mêmes noms de propriétés que JSON-LD, ce qui permet au même code en aval de traiter les deux.
Une subtilité : extruct parse le JSON-LD par page, pas par bloc. Si un seul bloc de la page est cassé, errors="ignore" supprime tout le JSON-LD de cette page, y compris le bloc produit valide. Une bonne répartition consiste à utiliser la fonction jsonld_nodes ci-dessus pour le JSON-LD, et extruct pour Microdata, RDFa et Open Graph.
Transformer un Noeud Product en Enregistrement Propre
Trouver le noeud Product est la partie facile. La spécification Schema.org autorise beaucoup de variations, et les vrais sites les utilisent toutes :
offerspeut être un seulOffer, une liste d’offres ou unAggregateOfferaveclowPriceethighPrice.pricepeut être un nombre, une chaîne comme"1299.00"ou une chaîne avec un séparateur de milliers. Il se cache parfois dans unpriceSpecificationimbriqué.availabilityest généralement une URL commehttps://schema.org/InStock, parfois enhttp, parfois justeInStock.brandpeut être une simple chaîne ou un objet avec unname.imagepeut être une chaîne, une liste ou unImageObjectavec uneurl.
Un petit normaliseur absorbe tout cela :
import re
def first(value):
if isinstance(value, list):
return value[0] if value else None
return value
def name_of(value):
value = first(value)
return value.get("name") if isinstance(value, dict) else value
def to_number(value):
if isinstance(value, (int, float)):
return float(value)
text = re.sub(r"[^\d.,]", "", str(value or ""))
if "," in text and "." in text:
# Le séparateur qui arrive en dernier est le séparateur décimal
if text.rfind(",") > text.rfind("."):
text = text.replace(".", "").replace(",", ".")
else:
text = text.replace(",", "")
elif "," in text:
decimals = text.split(",")[-1]
text = text.replace(",", ".") if len(decimals) <= 2 else text.replace(",", "")
try:
return float(text)
except ValueError:
return None
def product_record(node):
offer = first(node.get("offers")) or {}
spec = first(offer.get("priceSpecification")) or {}
if has_type(offer, "AggregateOffer"):
price = offer.get("lowPrice") or offer.get("price")
else:
price = offer.get("price") or spec.get("price")
image = first(node.get("image"))
rating = node.get("aggregateRating") or {}
return {
"name": node.get("name"),
"sku": node.get("sku"),
"brand": name_of(node.get("brand")),
"price": to_number(price),
"currency": offer.get("priceCurrency") or spec.get("priceCurrency"),
"availability": str(offer.get("availability") or "").rstrip("/").split("/")[-1] or None,
"image": image.get("url") if isinstance(image, dict) else image,
"rating": to_number(rating.get("ratingValue")),
"review_count": to_number(rating.get("reviewCount") or rating.get("ratingCount")),
}
url = "https://www.example.com/product/123"
html = requests.get(url, timeout=30).text
products = [product_record(n) for n in jsonld_nodes(html) if has_type(n, "Product")]
print(products)
Le même principe fonctionne pour d’autres types. Un JobPosting a title, hiringOrganization, jobLocation et baseSalary. Un Recipe a recipeIngredient et recipeInstructions. Un RealEstateListing ou un Car apporte ses propres propriétés. Écrivez une petite fonction de mapping par type qui vous intéresse, pas par site.
Quand les Données Structurées Vous Font Défaut
Les données structurées sont la meilleure première étape, pas une réponse complète. Prévoyez ces cas.
La Fiche Technique N’est Pas dans le Balisage
Les sites publient ce que les moteurs de recherche récompensent : nom, prix, note, disponibilité. Les caractéristiques détaillées qu’un acheteur lit, comme les dimensions, les matériaux, la puissance du moteur ou la surface, se trouvent généralement uniquement dans un tableau HTML ou une liste de définitions. Vous pouvez les lire de façon générique, sans sélecteurs par site, en collectant des paires libellé et valeur :
def spec_table(html):
soup = BeautifulSoup(html, "html.parser")
specs = {}
for row in soup.select("table tr"):
cells = row.find_all(["th", "td"])
if len(cells) == 2:
specs[cells[0].get_text(" ", strip=True)] = cells[1].get_text(" ", strip=True)
for dt in soup.find_all("dt"):
dd = dt.find_next_sibling("dd")
if dd:
specs[dt.get_text(" ", strip=True)] = dd.get_text(" ", strip=True)
return specs
Gardez les libellés tels que la page les affiche. Si vous scrapez le même site en plusieurs langues, associez ensuite les libellés à vos propres noms de champs (“Kilometerstand” et “Mileage” sont le même champ).
Le JSON-LD Est Ajouté par JavaScript
Certains sites injectent leurs données structurées côté client, par exemple via un gestionnaire de balises. Le HTML brut renvoyé par requests ne contient alors aucun JSON-LD, alors que le validateur, qui affiche la page, le montre sans problème. Affichez la page avec un navigateur et lisez les scripts depuis le DOM :
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url, wait_until="networkidle")
blocks = page.locator('script[type="application/ld+json"]').all_text_contents()
browser.close()
Les Données Sont Périmées ou Concernent une Autre Variante
Les consignes de Google exigent que le balisage corresponde au contenu visible, mais les templates et les caches dérivent. Sur un produit avec des tailles ou des couleurs, le JSON-LD peut décrire la variante par défaut, ou lister toutes les variantes dans un ProductGroup avec hasVariant. Vérifiez un échantillon de pages par rapport à ce qu’affiche le navigateur avant de faire confiance au champ prix à grande échelle.
Le JSON Est Cassé
Le JSON-LD écrit à la main contient des virgules finales, des guillemets non échappés et des commentaires HTML. strict=False règle les retours à la ligne bruts. Pour le reste, un parser tolérant comme chompjs récupère beaucoup de blocs que json.loads refuse. Si un bloc échoue encore, journalisez-le et passez à la source suivante.
Il N’y a Rien Parce Que Vous Avez Été Bloqué
Un résultat vide ne signifie pas toujours que la page n’a pas de balisage. Une page de challenge anti-bot ou un “403 déguisé en 200” n’a pas de JSON-LD non plus. Cherchez les signes de blocage avant de conclure qu’un site ne publie pas de données structurées. Notre guide sur la détection des blocages silencieux passe en revue les signes habituels.
Un Ordre d’Extraction Pratique
Pour chaque page, essayez les sources de la plus stable à la moins stable et arrêtez-vous dès que vous avez les champs nécessaires :
- JSON-LD
- Microdata et RDFa
- L’état embarqué du framework, comme
__NEXT_DATA__(voir comment trouver le JSON caché derrière un site JavaScript) - Les balises meta Open Graph pour le titre, la description et l’image
- Les tableaux de caractéristiques et les valeurs libellées
- Les sélecteurs CSS ou XPath, en dernier recours
Notez de quelle source provient chaque champ. Quand un site change, vous saurez quelle couche a bougé.
FAQ
Quelle est la différence entre JSON-LD, Microdata et RDFa ?
Ce sont trois façons d’écrire le même vocabulaire Schema.org. JSON-LD est un bloc JSON séparé dans une balise script. Microdata et RDFa sont des attributs sur les éléments HTML visibles. Le modèle de données est le même, c’est pourquoi extruct peut renvoyer les trois dans la même forme.
Le prix dans le JSON-LD est-il toujours celui affiché sur la page ?
En général oui, car les moteurs de recherche attendent que le balisage corresponde au contenu visible. Il peut être en retard après un changement de prix, ou décrire une autre variante que celle sélectionnée. Validez-le sur un échantillon avant de vous y fier.
Ai-je besoin d’un navigateur headless pour lire les données structurées ?
Seulement si le site les injecte en JavaScript ou bloque les clients HTTP simples. La plupart du JSON-LD se trouve dans le HTML rendu côté serveur, une simple requête suffit donc.
Puis-je combiner les données structurées avec mes propres sélecteurs ?
Oui, et c’est la configuration la plus robuste. Prenez tous les champs possibles dans les données structurées, et n’utilisez des sélecteurs que pour les quelques champs qu’elles ne couvrent pas.
Laissez une API S’occuper du Parsing
Si vous préférez ne pas maintenir de code d’extraction pour chaque type de page, ScrapeUnblocker peut renvoyer du JSON structuré au lieu du HTML. Ajoutez parsed_data=true à une requête /getPageSource :
import requests
r = requests.post(
"https://api.scrapeunblocker.com/getPageSource",
params={"url": "https://www.example.com/product/123", "parsed_data": True},
headers={"x-scrapeunblocker-key": "YOUR_API_KEY"},
timeout=120,
)
print(r.json()["data"])
La réponse indique de quel type de page il s’agit (product_detail, listing_detail, search_list, etc.) et renvoie des champs en anglais au format snake_case, avec des prix normalisés en une valeur et une devise. Les pages de détail incluent désormais aussi la fiche technique complète de la page sous specifications, exactement telle que la page l’affiche, à côté des données Schema.org. Quand une page n’a rien à extraire, vous recevez le HTML rendu, donc le parser de ce guide a toujours de quoi travailler. Le guide des données parsées montre la structure complète de la réponse.
Essayez ScrapeUnblocker gratuitement
Taux de réussite de plus de 95 % · à partir de 0,55 € pour 1 000 appels (TVA éventuelle en sus) · 500 requêtes gratuites à l'inscription.