# Dependances pre-provisionnees (rdflib) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.SW-9-Python-JSONLD
Navigation : << 8-Python-SHACL | Index | 10-Python-RDFStar >>
JSON-LD : Le Web Sémantique rencontre JSON
Duree estimee : 40 minutes
Objectifs d’apprentissage
A la fin de ce notebook, vous saurez : 1. Comprendre JSON-LD et son rôle de pont entre JSON et le Linked Data 2. Créer et manipuler des contextes JSON-LD (@context, @id, @type, @graph) 3. Utiliser le vocabulaire Schema.org pour decrire des entites web 4. Generer et parser des données structurees avec rdflib
Prerequis
- Python 3.10+
- Notebook SW-8-Python-SHACL (bases rdflib, SPARQL et SHACL)
Installation des dependances
1. Pourquoi JSON-LD ? Le pont entre JSON et le Linked Data
Le Web regorge de données en JSON, le format natif de JavaScript. Mais JSON seul ne porte aucune sémantique : les cles sont des chaînes arbitraires, sans signification universelle. Un champ "name" dans une API peut designer un nom de personne, de produit ou de fichier.
JSON-LD (JSON for Linking Data) resout ce problème en ajoutant une couche de contexte (@context) qui relie chaque cle JSON a un IRI sémantique, transformant du JSON ordinaire en données RDF exploitables.
L’adoption de JSON-LD en chiffres
| Metrique | Valeur | Source |
|---|---|---|
| Part des rich snippets Google utilisant JSON-LD | ~73% | Schema.org community (2024) |
| Amelioration moyenne du CTR avec données structurees | +35% | Études SEO |
| Format recommande par Google | JSON-LD | Google Developers |
| Types Schema.org disponibles | 800+ | schema.org/docs |
Historique
JSON-LD a ete créé par le W3C (recommandation JSON-LD 1.0 en 2014, puis 1.1 en 2020). L’objectif etait de rendre le Linked Data accessible aux developpeurs web habitues a JSON, sans leur imposer la syntaxe RDF/XML ou Turtle.
Point cle : JSON-LD est du JSON valide. Tout parser JSON peut le lire. Mais un parser JSON-LD peut en extraire des triples RDF.
import json
# JSON classique : pas de semantique
json_simple = {
"name": "Ada Lovelace",
"job": "Mathematician"
}
# JSON-LD : meme structure, mais avec un contexte semantique
jsonld_with_context = {
"@context": "https://schema.org/",
"@type": "Person",
"name": "Ada Lovelace",
"jobTitle": "Mathematician"
}
print("=== JSON simple (pas de semantique) ===")
print(json.dumps(json_simple, indent=2))
print()
print("=== JSON-LD (avec contexte Schema.org) ===")
print(json.dumps(jsonld_with_context, indent=2))=== JSON simple (pas de semantique) ===
{
"name": "Ada Lovelace",
"job": "Mathematician"
}
=== JSON-LD (avec contexte Schema.org) ===
{
"@context": "https://schema.org/",
"@type": "Person",
"name": "Ada Lovelace",
"jobTitle": "Mathematician"
}
Interpretation — deux blocs valides, un seul porteur de sens universel
Les deux blocs sont du JSON valide, mais seul le second porte une signification universelle : - "@context": "https://schema.org/" declare que les cles utilisent le vocabulaire Schema.org - "@type": "Person" indique qu’il s’agit d’une personne (equivalent de rdf:type schema:Person) - "name" est resolu en schema:name, une propriete standardisee
Un moteur de recherche comme Google peut donc extraire automatiquement ces informations.
2. Syntaxe fondamentale de JSON-LD
JSON-LD repose sur quatre mots-cles principaux qui transforment du JSON ordinaire en données liees.
2.1 @context : le dictionnaire de traduction
Le @context associe chaque cle JSON a un IRI (Internationalized Resource Identifier). Il peut etre : - Une URL : "@context": "https://schema.org/" (contexte distant) - Un objet : definition locale des mappings - Un tableau : combinaison de plusieurs contextes
import json
# Contexte distant (Schema.org)
ctx_remote = {
"@context": "https://schema.org/",
"@type": "Person",
"name": "Alan Turing"
}
# Contexte local (mappings explicites)
ctx_local = {
"@context": {
"name": "http://xmlns.com/foaf/0.1/name",
"homepage": {
"@id": "http://xmlns.com/foaf/0.1/homepage",
"@type": "@id"
}
},
"name": "Alan Turing",
"homepage": "https://en.wikipedia.org/wiki/Alan_Turing"
}
# Contexte combine (tableau)
ctx_combined = {
"@context": [
"https://schema.org/",
{
"foaf": "http://xmlns.com/foaf/0.1/"
}
],
"@type": "Person",
"name": "Alan Turing"
}
print("=== Contexte distant ===")
print(json.dumps(ctx_remote, indent=2))
print()
print("=== Contexte local (mappings explicites) ===")
print(json.dumps(ctx_local, indent=2))
print()
print("=== Contexte combine ===")
print(json.dumps(ctx_combined, indent=2))=== Contexte distant ===
{
"@context": "https://schema.org/",
"@type": "Person",
"name": "Alan Turing"
}
=== Contexte local (mappings explicites) ===
{
"@context": {
"name": "http://xmlns.com/foaf/0.1/name",
"homepage": {
"@id": "http://xmlns.com/foaf/0.1/homepage",
"@type": "@id"
}
},
"name": "Alan Turing",
"homepage": "https://en.wikipedia.org/wiki/Alan_Turing"
}
=== Contexte combine ===
{
"@context": [
"https://schema.org/",
{
"foaf": "http://xmlns.com/foaf/0.1/"
}
],
"@type": "Person",
"name": "Alan Turing"
}
Interpretation : les trois facons de poser un @context
La sortie ci-dessus montre trois documents qui disent tous la meme chose sur Alan Turing – et qui ne signifient rien tant que le @context n’a pas tranche.
- Contexte distant : la chaine
"https://schema.org/"delegue tout le travail de traduction. Les termes@typeetnamene sont des proprietes RDF que parce que le processeur va resoudre ce prefixe. C’est la forme la plus courte a ecrire et la plus dependante du reseau : deux traitements successifs peuvent differer si le vocabulaire distant evolue. - Contexte local : l’objet remplace la delegation par des mappings explicites –
namepointe vershttp://xmlns.com/foaf/0.1/name(le vocabulaire FOAF, pas Schema.org). Le document devient autonyme : sa semantique ne depend plus d’une resolution distante. Notez la structure emboitee dehomepage: un objet avec@id(l’URI de la propriete) et@type: "@id"(la contrainte que la valeur sera traitee comme un URI, pas comme une chaine littérale). Sans cette contrainte,https://en.wikipedia.org/wiki/Alan_Turingresterait un simple texte. - Contexte combine : un tableau qui melange la delegation distante et un mapping local (
foafvershttp://xmlns.com/0.1/). C’est le compromis pratiqu� dans les vraies pages : on herite du socle Schema.org tout en important des vocabulaires complementaires.
La lecon structurante : en JSON classique, name n’est qu’une cle parmi d’autres ; en JSON-LD, aucune cle n’a de sens hors de son contexte. Deux documents aux corps identiques mais aux contextes differents decrivent des ressources differentes – c’est exactement la symetrie inverse du monde RDF, ou l’URI fait tout le travail et la syntaxe n’est qu’un habit.
2.2 @id et @type : identifier et typer les ressources
@id: identifie la ressource (le sujet du triple RDF). Si absent, un blank node est créé.@type: définit le type de la ressource (equivalent derdf:type).
import json
person_with_id = {
"@context": "https://schema.org/",
"@id": "https://example.org/people/turing",
"@type": "Person",
"name": "Alan Turing",
"birthDate": "1912-06-23",
"birthPlace": {
"@type": "Place",
"name": "London"
}
}
print(json.dumps(person_with_id, indent=2))
print()
print("Triples RDF generes :")
print(" <https://example.org/people/turing> rdf:type schema:Person .")
print(' <https://example.org/people/turing> schema:name "Alan Turing" .')
print(' <https://example.org/people/turing> schema:birthDate "1912-06-23" .')
print(" <https://example.org/people/turing> schema:birthPlace _:b0 .")
print(" _:b0 rdf:type schema:Place .")
print(' _:b0 schema:name "London" .'){
"@context": "https://schema.org/",
"@id": "https://example.org/people/turing",
"@type": "Person",
"name": "Alan Turing",
"birthDate": "1912-06-23",
"birthPlace": {
"@type": "Place",
"name": "London"
}
}
Triples RDF generes :
<https://example.org/people/turing> rdf:type schema:Person .
<https://example.org/people/turing> schema:name "Alan Turing" .
<https://example.org/people/turing> schema:birthDate "1912-06-23" .
<https://example.org/people/turing> schema:birthPlace _:b0 .
_:b0 rdf:type schema:Place .
_:b0 schema:name "London" .
Interpretation : ce que @id et @type changent dans les triples
La sortie superpose les deux etages : le document JSON-LD en entree, puis sa projection en triples RDF. La comparaison ligne a ligne est la demonstration cherchee.
- Sans
@id(section precedente), la personne decrite etait un blank node anonyme : personne ne pouvait la referencer de l’exterieur. Avec"@id": "https://example.org/people/turing", la ligne<https://example.org/people/turing> schema:name "Alan Turing"– le sujet est desormais une URI dereferencable. Deux documents posant le meme@idparlent de la meme ressource : c’est le mecanisme par lequel le Linked Data agrège des sources disparates. @typedevientrdf:type:schema:Personn’est plus une etiquette decorative mais le triple<...turing> rdf:type schema:Person, interrog� par le motif SPARQL?s a schema:Person.- La valeur emboitee devient un blank node :
birthPlacene peut pas etre un littéral (une place est une entite avec ses propres proprietes), et la sortie le materialise – le tripleschema:birthPlace _:b0pointe vers un noeud anonyme, lui-meme typeschema:Placeet nomme"London". Le prefixe_:b0est l’artefact de serialisation : le document JSON disait “London” en propres mots, le graphe RDF dit “un noeud anonyme que voici”. - Le littéral date reste nu :
"1912-06-23"devient le tripleschema:birthDate "1912-06-23"– sans type explicite, la date est une chaine. On verra en section 4 la forme{"@value": ..., "@type": ...}qui la datera reellement enxsd:date.
Six lignes de sortie pour quatre proprietes JSON : le cout de la projection RDF est visible, et il est le prix de l’interoperabilite.
2.3 @graph : regrouper plusieurs ressources
Le mot-cle @graph permet de decrire plusieurs entites dans un seul document JSON-LD, partageant le même @context.
import json
multi_entities = {
"@context": "https://schema.org/",
"@graph": [
{
"@type": "Person",
"@id": "https://example.org/people/turing",
"name": "Alan Turing",
"alumniOf": {"@id": "https://example.org/org/cambridge"}
},
{
"@type": "CollegeOrUniversity",
"@id": "https://example.org/org/cambridge",
"name": "University of Cambridge",
"foundingDate": "1209"
},
{
"@type": "Person",
"@id": "https://example.org/people/lovelace",
"name": "Ada Lovelace",
"jobTitle": "Mathematician"
}
]
}
print(f"Document contenant {len(multi_entities['@graph'])} entites :")
for entity in multi_entities["@graph"]:
print(f" - {entity['@type']} : {entity['name']} ({entity['@id']})")Document contenant 3 entites :
- Person : Alan Turing (https://example.org/people/turing)
- CollegeOrUniversity : University of Cambridge (https://example.org/org/cambridge)
- Person : Ada Lovelace (https://example.org/people/lovelace)
Interpretation : @graph, ou comment un seul document porte un mini-web
La sortie resume le document en trois entites : deux Person (Alan Turing a https://example.org/people/turing, Ada Lovelace a https://example.org/people/lovelace) et une CollegeOrUniversity (University of Cambridge a https://example.org/org/cambridge).
Sans @graph, un objet JSON-LD de haut niveau decrit une ressource principale – les autres n’existent qu’en tant que valeurs emboitees de ses proprietes, donc comme blank nodes. Avec @graph, les trois entites sont co-premieres : chacune a son @id directement referencable, aucune n’est la propriete d’une autre. C’est la difference entre “une fiche centrale et ses annexes” et “un corpus de fiches liees”.
Concretement :
- c’est la forme naturelle d’un export de base de connaissances – l’ensemble des fiches d’un systeme, pas une fiche choisie comme racine ;
- les liens entre entites du graphe (
knows,alumniOf,worksFor) relient des@idinternes au document, donc restent dereferencables sans sortir du fichier ; - c’est aussi ce que produit la forme aplatie (section 2.4) : quand on aplatit un document emboite, les blank nodes reçoivent des identifiants et migrent dans le
@graph– la structure arborescente de JSON se dissout au profit de la structure de graphe de RDF.
Retenez le tandem : @graph pour le conteneur, @id pour la reference. Ensemble, ils font d’un fichier JSON isole le point de depart possible d’un web de donnees.
2.4 Formes JSON-LD : Compacte, Etendue et Aplatie
Un même graphe RDF peut etre represente sous trois formes JSON-LD :
| Forme | Description | Usage |
|---|---|---|
| Compacte | Utilise un @context pour abreger les IRIs |
Production web (lisibilite) |
| Etendue | IRIs complets, pas de @context |
Echanges machine a machine |
| Aplatie | Toutes les entites au même niveau dans @graph |
Normalisation, deduplication |
import json
# Forme COMPACTE (la plus courante)
compact_form = {
"@context": "https://schema.org/",
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "Physicist"
}
# Forme ETENDUE (expanded) - IRIs complets, pas de contexte
expanded_form = [
{
"@type": ["http://schema.org/Person"],
"http://schema.org/name": [
{"@value": "Marie Curie"}
],
"http://schema.org/jobTitle": [
{"@value": "Physicist"}
]
}
]
# Forme APLATIE (flattened) - toutes les entites au meme niveau
flattened_form = {
"@context": "https://schema.org/",
"@graph": [
{
"@id": "_:b0",
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "Physicist"
}
]
}
print("=== Forme COMPACTE ===")
print(json.dumps(compact_form, indent=2))
print()
print("=== Forme ETENDUE (expanded) ===")
print(json.dumps(expanded_form, indent=2))
print()
print("=== Forme APLATIE (flattened) ===")
print(json.dumps(flattened_form, indent=2))=== Forme COMPACTE ===
{
"@context": "https://schema.org/",
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "Physicist"
}
=== Forme ETENDUE (expanded) ===
[
{
"@type": [
"http://schema.org/Person"
],
"http://schema.org/name": [
{
"@value": "Marie Curie"
}
],
"http://schema.org/jobTitle": [
{
"@value": "Physicist"
}
]
}
]
=== Forme APLATIE (flattened) ===
{
"@context": "https://schema.org/",
"@graph": [
{
"@id": "_:b0",
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "Physicist"
}
]
}
Interpretation — trois formes, un seul graphe de triples
Les trois formes representent exactement les mêmes triples RDF. La forme compacte est la plus utilisee dans les pages web car elle est lisible par les developpeurs. La forme etendue est utile pour le traitement automatise (pas besoin de resoudre le contexte). La forme aplatie normalise la structure pour faciliter la comparaison et la fusion de documents.
3. Le vocabulaire Schema.org
Schema.org est le vocabulaire collaboratif créé par Google, Microsoft, Yahoo et Yandex pour structurer les données du Web. Il définit plus de 800 types et 1500 proprietes couvrant les domaines les plus courants.
Types les plus utilises
| Type | Description | Exemple d’usage |
|---|---|---|
Person |
Personne physique | Profil, auteur d’article |
Organization |
Entreprise, association | Page entreprise |
Product |
Produit commercial | Fiche produit e-commerce |
Event |
Événement | Concert, conference |
Article |
Article de presse/blog | Billet de blog |
Recipe |
Recette de cuisine | Site culinaire |
FAQPage |
Page de FAQ | Support client |
BreadcrumbList |
Fil d’Ariane | Navigation de site |
Course |
Cours en ligne | Plateforme educative |
3.1 Explorer le namespace Schema.org avec rdflib
rdflib fournit un namespace SDO (Schema.org) predefini pour acceder aux termes Schema.org.
from rdflib import Namespace, URIRef
from rdflib.namespace import RDF, RDFS
# Definir le namespace Schema.org
SDO = Namespace("https://schema.org/")
# Exemples de termes Schema.org
common_types = [
("Person", SDO.Person),
("Organization", SDO.Organization),
("Product", SDO.Product),
("Event", SDO.Event),
("Article", SDO.Article),
("Course", SDO.Course),
]
common_props = [
("name", SDO.name),
("description", SDO.description),
("url", SDO.url),
("author", SDO.author),
("datePublished", SDO.datePublished),
]
print("=== Types Schema.org courants ===")
for label, uri in common_types:
print(f" {label:20s} -> {uri}")
print()
print("=== Proprietes Schema.org courantes ===")
for label, uri in common_props:
print(f" {label:20s} -> {uri}")=== Types Schema.org courants ===
Person -> https://schema.org/Person
Organization -> https://schema.org/Organization
Product -> https://schema.org/Product
Event -> https://schema.org/Event
Article -> https://schema.org/Article
Course -> https://schema.org/Course
=== Proprietes Schema.org courantes ===
name -> https://schema.org/name
description -> https://schema.org/description
url -> https://schema.org/url
author -> https://schema.org/author
datePublished -> https://schema.org/datePublished
Interpretation : le namespace Schema.org vu par rdflib
La sortie aligne deux tables : a gauche les types (Person, Organization, Product, Event, Article, Course), a droite les proprietes (name, description, url, author, datePublished). Les fleches -> montrent ce que rdflib fait silencieusement depuis le debut du notebook : chaque nom court est l’abreviation d’une URI complete (Person -> https://schema.org/Person).
Trois remarques pour la suite :
- Ceci n’est pas une enumeration. Schema.org compte plusieurs centaines de types et plusieurs milliers de proprietes ; la sortie n’en montre que six et cinq, ceux que les notebooks de la serie emploient. L’exploration complete vit sur schema.org, et le namespace rdflib n’est qu’un acces de convenance aux plus courants.
- Types et proprietes vivent dans le meme namespace – contrairement a OWL ou T-Box et A-Box sont souvent separees.
https://schema.org/namen’est passchema:nameClass: Schema.org est un vocabulaire pragmatique (pense pour l’annotation de pages web), ou la classe et la propriete cohabitent sous la meme autorite d’URI. - Le prefixe
schema:n’a rien d’officiel – c’est une convention d’ecriture locale. On croise aussischema1:(la section 5.3 en produira un automatiquement), qui designe exactement le meme namespacehttp://schema.org/. Ne confondez pas la variation de prefixe avec une variation de vocabulaire : en RDF, seul l’URI compte, jamais son raccourci.
Cette table est le dictionnaire implicite de toutes les sections qui suivent : chaque @type et chaque propriete des documents Recipe, Event, Product se resoudra par ces URI.
3.2 Charger et afficher le fichier data/product.jsonld
Le fichier data/product.jsonld contient un exemple Schema.org de type Product decrivant le cours CoursIA. Chargeons-le et examinons sa structure.
import json
from pathlib import Path
# Charger le fichier JSON-LD
product_path = Path("data/product.jsonld")
with open(product_path, "r", encoding="utf-8") as f:
product_data = json.load(f)
print("=== Contenu de data/product.jsonld ===")
print(json.dumps(product_data, indent=2, ensure_ascii=False))
print()
print(f"Type : {product_data['@type']}")
print(f"Nom : {product_data['name']}")
print(f"Auteur : {product_data['author']['name']}")
print(f"Sujets enseignes : {len(product_data['teaches'])}")
for i, topic in enumerate(product_data["teaches"], 1):
print(f" {i}. {topic}")=== Contenu de data/product.jsonld ===
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "Semantic Web avec Python et .NET",
"description": "Cours complet sur le Web Semantique, de RDF aux graphes de connaissances",
"image": "https://example.org/images/semantic-web-course.png",
"brand": {
"@type": "Brand",
"name": "CoursIA"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"url": "https://github.com/jsboige/CoursIA"
},
"educationalLevel": "University",
"teaches": [
"RDF et triples",
"SPARQL queries",
"OWL ontologies",
"SHACL validation",
"JSON-LD et Schema.org",
"Knowledge Graphs",
"GraphRAG"
],
"author": {
"@type": "Person",
"name": "Jean-Sebastien Boige"
}
}
Type : Product
Nom : Semantic Web avec Python et .NET
Auteur : Jean-Sebastien Boige
Sujets enseignes : 7
1. RDF et triples
2. SPARQL queries
3. OWL ontologies
4. SHACL validation
5. JSON-LD et Schema.org
6. Knowledge Graphs
7. GraphRAG
Interpretation : Schema.org Product
Ce document JSON-LD decrit un produit educatif avec les proprietes Schema.org suivantes :
| Propriete | Valeur | Signification Schema.org |
|---|---|---|
@type |
Product | Type de l’entite |
name |
Semantic Web avec Python et .NET | Nom du produit |
brand |
CoursIA | Marque associee |
offers |
Prix 0 EUR, InStock | Disponibilite et prix |
educationalLevel |
University | Niveau educatif |
teaches |
7 sujets | Competences enseignees |
author |
Jean-Sebastien Boige | Auteur du cours |
Note : Ce JSON-LD pourrait etre integre directement dans une balise
<script type="application/ld+json">d’une page HTML pour que Google affiche un rich snippet.
4. Créer du JSON-LD avec rdflib
rdflib permet de construire un graphe RDF et de le serialiser directement en JSON-LD. C’est l’approche programmatique pour generer des données structurees.
4.1 Construire un graphe et serialiser en JSON-LD
Nous allons créer une entite Schema.org Person avec plusieurs proprietes, puis l’exporter en JSON-LD.
from rdflib import Graph, Literal, Namespace, URIRef, BNode
from rdflib.namespace import RDF, RDFS, XSD
# Namespaces
SDO = Namespace("https://schema.org/")
EX = Namespace("https://example.org/people/")
# Creer un graphe
g = Graph()
g.bind("schema", SDO)
g.bind("ex", EX)
# Definir une personne
person = EX.ada_lovelace
g.add((person, RDF.type, SDO.Person))
g.add((person, SDO.name, Literal("Ada Lovelace")))
g.add((person, SDO.jobTitle, Literal("Mathematician and Writer")))
g.add((person, SDO.email, Literal("ada@example.org")))
g.add((person, SDO.birthDate, Literal("1815-12-10", datatype=XSD.date)))
# Ajouter une relation "knows"
babbage = EX.charles_babbage
g.add((babbage, RDF.type, SDO.Person))
g.add((babbage, SDO.name, Literal("Charles Babbage")))
g.add((babbage, SDO.jobTitle, Literal("Inventor")))
g.add((person, SDO.knows, babbage))
print(f"Graphe construit : {len(g)} triples")
print()
# Serialiser en JSON-LD
jsonld_output = g.serialize(format="json-ld", indent=2)
print("=== Serialisation JSON-LD ===")
print(jsonld_output)Graphe construit : 9 triples
=== Serialisation JSON-LD ===
[
{
"@id": "https://example.org/people/charles_babbage",
"@type": [
"https://schema.org/Person"
],
"https://schema.org/jobTitle": [
{
"@value": "Inventor"
}
],
"https://schema.org/name": [
{
"@value": "Charles Babbage"
}
]
},
{
"@id": "https://example.org/people/ada_lovelace",
"@type": [
"https://schema.org/Person"
],
"https://schema.org/birthDate": [
{
"@type": "http://www.w3.org/2001/XMLSchema#date",
"@value": "1815-12-10"
}
],
"https://schema.org/email": [
{
"@value": "ada@example.org"
}
],
"https://schema.org/jobTitle": [
{
"@value": "Mathematician and Writer"
}
],
"https://schema.org/knows": [
{
"@id": "https://example.org/people/charles_babbage"
}
],
"https://schema.org/name": [
{
"@value": "Ada Lovelace"
}
]
}
]
Interpretation — lire la forme etendue comme un graphe
rdflib genere automatiquement le JSON-LD a partir du graphe ; la forme etendue deplace toutes les decisions sémantiques du contexte vers les donnees, et la sortie merite une lecture ligne a ligne :
- Plus de
@context: les cles ne sont plusnameoujobTitlemais des URI complets ("https://schema.org/jobTitle"). Le document est autoporteur – chaque predicat emporte sa propre signification – au prix de la verbosite. C’est la forme qu’un processeur produit en interne apres resolution du contexte ; la forme compacte n’est qu’une compression de celle-ci. - Les valeurs deviennent des tableaux d’objets :
"https://schema.org/name": [{"@value": "Charles Babbage"}]. Cette structure apparemment lourde encode deux regularites de RDF : une propriete peut avoir plusieurs valeurs (le tableau), et chaque valeur peut etre typée ({"@value": ..., "@type": "http://www.w3.org/2001/XMLSchema#date"}pour la date de naissance d’ada_lovelace– comparez au littéral nu"1912-06-23"de la section 2.2 : ici la date EST une date au sens des types XML Schema, pas une chaine qui lui ressemble). @typeest lui-meme un tableau :[ "https://schema.org/Person" ]– une ressource peut avoir plusieurs classes, et la forme etendue ne prerien decide pour vous.- Les 9 triples sont tous la :
charles_babbageavec sonjobTitle“Inventor”,ada_lovelaceavec sa date typée, la relationknowsliant deux ressources par leur@id– la sortie du graphe (9 triples) se retrouve integrale, juste re-habillee.
La forme etendue est celle des echanges entre machines ; la compacte, celle des humains qui ecrivent. La section 4.2 montre justement comment redemander la forme compacte avec son propre contexte – en passant un contexte au serialiseur, ce que l’exercice correspondant met en pratique.
4.2 Serialisation compacte avec contexte
Pour generer du JSON-LD compact lisible, on peut fournir un contexte de serialisation.
import json
# Definir le contexte pour la serialisation compacte
context = {
"schema": "https://schema.org/",
"ex": "https://example.org/people/",
"name": "schema:name",
"jobTitle": "schema:jobTitle",
"email": "schema:email",
"birthDate": "schema:birthDate",
"knows": {"@id": "schema:knows", "@type": "@id"}
}
# Serialiser avec le contexte
jsonld_compact = g.serialize(
format="json-ld",
indent=2,
context=context
)
print("=== JSON-LD compact avec contexte ===")
print(jsonld_compact)=== JSON-LD compact avec contexte ===
{
"@context": {
"birthDate": "schema:birthDate",
"email": "schema:email",
"ex": "https://example.org/people/",
"jobTitle": "schema:jobTitle",
"knows": {
"@id": "schema:knows",
"@type": "@id"
},
"name": "schema:name",
"schema": "https://schema.org/"
},
"@graph": [
{
"@id": "ex:charles_babbage",
"@type": "schema:Person",
"jobTitle": "Inventor",
"name": "Charles Babbage"
},
{
"@id": "ex:ada_lovelace",
"@type": "schema:Person",
"birthDate": {
"@type": "http://www.w3.org/2001/XMLSchema#date",
"@value": "1815-12-10"
},
"email": "ada@example.org",
"jobTitle": "Mathematician and Writer",
"knows": "ex:charles_babbage",
"name": "Ada Lovelace"
}
]
}
Interpretation : la serialisation compacte, ou le contexte reprend la main
La sortie montre le meme graphe (Babbage, Lovelace) re-serialise en forme compacte – et le contraste avec la forme etendue de la section precedente est instructif terme a terme :
- Le contexte reapparait en tete, mais cette fois rdflib l’a construit a partir des contraintes passees a la serialisation : le mapping
exvershttps://example.org/people/, les proprietes raccourcies ("jobTitle": "schema:jobTitle"), et pourknowsla structure{"@id": ..., "@type": "@id"}– exactement la forme qu’on ecrivait a la main pourhomepageen section 2.1. La boucle est bouclee : ce que la section 2.1 posait comme entree manuelle, rdflib le regenere comme sortie. - Les URI complets ont disparu du corps :
"@id": "ex:charles_babbage"ou"@type": "schema:Person"– le document redevient lisible, parce que la semantique est encore une fois externalisee dans le contexte. - La date typée survit au passage :
"birthDate": {"@type": "http://www.w3.org/2001/XMLSchema#date", "@value": "1815-12-10"}– le compactage raccourcit les predicats, il ne degrade pas les valeurs. Unxsd:dateentre, unxsd:datesort. - Le
@graphstructure la sortie : les deux personnes y sont co-premieres, chacune avec son@id– meme decision de presentation que la section 2.3.
En pratique : la forme compacte avec contexte maitrise est celle qu’on embarque dans une page HTML (<script type="application/ld+json">), parce qu’elle minimiser le poids et reste lisible ; la forme etendue est celle qu’on echange entre systemes qui ne partagent aucun contexte prevu. Choisir sa forme de serialisation, c’est choisir son public.
5. Parser du JSON-LD
L’opération inverse est tout aussi importante : charger du JSON-LD dans un graphe rdflib pour l’interroger avec SPARQL ou le convertir dans d’autres formats.
5.1 Charger une chaîne JSON-LD dans rdflib
from rdflib import Graph
import json
# Document JSON-LD a parser
jsonld_string = json.dumps({
"@context": "https://schema.org/",
"@id": "https://example.org/events/pycon2025",
"@type": "Event",
"name": "PyCon France 2025",
"startDate": "2025-10-23",
"endDate": "2025-10-26",
"location": {
"@type": "Place",
"name": "Strasbourg",
"address": "Palais de la Musique et des Congres"
},
"organizer": {
"@type": "Organization",
"name": "AFPy"
}
})
# Parser dans un graphe
g2 = Graph()
g2.parse(data=jsonld_string, format="json-ld")
print(f"Triples charges : {len(g2)}")
print()
print("=== Tous les triples ===")
for s, p, o in g2:
# Raccourcir les URIs pour la lisibilite
s_short = str(s).replace("https://schema.org/", "schema:").replace("https://example.org/events/", "ex:")
p_short = str(p).replace("https://schema.org/", "schema:").replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:")
o_short = str(o).replace("https://schema.org/", "schema:").replace("https://example.org/events/", "ex:")
print(f" {s_short} {p_short} {o_short}")Triples charges : 11
=== Tous les triples ===
N009f42fb7e0f4b7a90776c90b528a2d3 rdf:type http://schema.org/Organization
ex:pycon2025 http://schema.org/organizer N009f42fb7e0f4b7a90776c90b528a2d3
ex:pycon2025 http://schema.org/location N38aa90e3b7d045f394cacfce2316e23c
ex:pycon2025 http://schema.org/endDate 2025-10-26
ex:pycon2025 http://schema.org/startDate 2025-10-23
N009f42fb7e0f4b7a90776c90b528a2d3 http://schema.org/name AFPy
N38aa90e3b7d045f394cacfce2316e23c http://schema.org/name Strasbourg
ex:pycon2025 rdf:type http://schema.org/Event
ex:pycon2025 http://schema.org/name PyCon France 2025
N38aa90e3b7d045f394cacfce2316e23c rdf:type http://schema.org/Place
N38aa90e3b7d045f394cacfce2316e23c http://schema.org/address Palais de la Musique et des Congres
Interpretation : ce que le parsing fait au document
La sortie commence par un compte – 11 triples charges – puis les enumere. Trois artefacts de la traduction meriteant d’etre nommes, car on les retrouvera dans tout document JSON-LD emboite :
- Les blank nodes rdflib :
N009f42fb7e0f4b7a90776c90b528a2d3(l’organisateur AFPy) etN38aa90e3b7d045f394cacfce2316e23c(le lieu Strasbourg) n’existaient pas comme entites nommees dans le document source – ils y etaient des valeurs emboitees deorganizeretlocation. Le parseur leur a attribue des identifiants internes (prefixesN, Pour node). Ces identifiants ne sont pas stables d’un parsing a l’autre : ce sont des poignees locales, pas des URI. Si vous avez besoin de referencer l’organisateur durablement, donnez-lui un@iddans le document source. - Les dates passent nues :
startDate 2025-10-23etendDate 2025-10-26apparaissent sans type – le document source les posait comme chaines. Meme remarque qu’en section 2.2 : la valeur ressemble a une date, le graphe n’en sait rien. - Le compte compte les noeuds, pas les entites : le document decrit une conference (
ex:pycon2025, correctement identifiee cette fois par son@id), mais ses 11 triples recouvrent aussi le type, le nom et les liens des deux blank nodes. “Combien d’entites ?” et “combien de triples ?” sont deux questions differentes – la seconde est la seule que RDF reponde nativement.
Ce graphe g2 est celui que les trois sections suivantes vont exploiter : l’interroger en SPARQL (5.2), le re-serialiser en Turtle (5.3), le faire voyager en boucle (5.4).
5.2 Interroger avec SPARQL
Une fois le JSON-LD charge dans un graphe, nous pouvons utiliser toute la puissance de SPARQL.
# Requete SPARQL sur le graphe charge depuis JSON-LD
query = """
PREFIX schema: <http://schema.org/>
SELECT ?name ?type ?startDate
WHERE {
?entity schema:name ?name .
?entity a ?type .
OPTIONAL { ?entity schema:startDate ?startDate }
}
ORDER BY ?name
"""
print("=== Resultats SPARQL ===")
print(f"{'Nom':<35} {'Type':<25} {'Date debut'}")
print("-" * 75)
for row in g2.query(query):
name = str(row.name)
rdf_type = str(row.type).replace("https://schema.org/", "schema:")
start = str(row.startDate) if row.startDate else "-"
print(f"{name:<35} {rdf_type:<25} {start}")=== Resultats SPARQL ===
Nom Type Date debut
---------------------------------------------------------------------------
AFPy http://schema.org/Organization -
PyCon France 2025 http://schema.org/Event 2025-10-23
Strasbourg http://schema.org/Place -
Interpretation : SPARQL ne sait pas d’ou viennent les triples
Le resultat se presente en tableau : trois lignes, une par entite retournée. La force de la demonstration est dans ce qu’elle ne montre pas : la requete n’a aucune connaissance du JSON-LD.
- Les trois lignes sont l’organisateur (
AFPy, typehttp://schema.org/Organization, pas de date de debut), l’evenement (PyCon France 2025, typehttp://schema.org/Event, demarre le2025-10-23) et le lieu (Strasbourg, typehttp://schema.org/Place). Trois types differents, une seule requete : le motif?nom ?type ?dateavecOPTIONALpour la date – les blank nodes AFPy et Strasbourg repondent au meme titre que la conference identifiee, parce qu’en RDF un noeud est un noeud, anonyme ou non. - La colonne Date debut est vide pour deux lignes sur trois : c’est l’
OPTIONALqui travaille. Sans lui, la jointure aurait elimine AFPy et Strasbourg du resultat – une organisation n’a pas destartDate. Le tableau est la pour rappeler que l’absence de valeur en RDF est une information, pas un bug. - La date
2025-10-23arrive comme littéral nu (la chaine du document source) – on la triera lexicographiquement, pas chronologiquement, tant qu’elle n’est pas typéexsd:date. Piege classique des requetes SPARQL sur donnees Schema.org naivement ecrites.
L’operation qui vient d’avoir lieu est le coeur du notebook : JSON ecrit par un humain, parse en graphe, interrogé par un langage de requetes concu pour RDF. Aucune etape n’a ete specific a JSON-LD : le meme SPARQL s’appliquerait a un graphe charge depuis Turtle, RDF/XML ou N-Triples. La syntaxe est interchangeable, le graphe est canonical.
5.3 Convertir en Turtle
rdflib permet de convertir le JSON-LD en n’importe quel format RDF, notamment Turtle qui est plus lisible pour les humains.
# Convertir le graphe JSON-LD en Turtle
turtle_output = g2.serialize(format="turtle")
print("=== Conversion JSON-LD -> Turtle ===")
print(turtle_output)=== Conversion JSON-LD -> Turtle ===
@prefix schema1: <http://schema.org/> .
<https://example.org/events/pycon2025> a schema1:Event ;
schema1:endDate "2025-10-26"^^schema1:Date ;
schema1:location [ a schema1:Place ;
schema1:address "Palais de la Musique et des Congres" ;
schema1:name "Strasbourg" ] ;
schema1:name "PyCon France 2025" ;
schema1:organizer [ a schema1:Organization ;
schema1:name "AFPy" ] ;
schema1:startDate "2025-10-23"^^schema1:Date .
Interpretation : Turtle, la forme ou le graphe respire
La sortie re-ecrit le meme contenu en Turtle, et la comparaison avec le JSON-LD de la section precedente est une lecon de typographie des donnees :
- Le prefixe a change de nom :
schema1:au lieu deschema:. Rdflib genere ce suffixe numeral pour eviter une collision dans la table des prefixes de la session – et il designe le meme namespacehttp://schema.org/. Rappel de la section 3.1 : le prefixe est un raccourci d’ecriture local, jamais une identite. - Les dates sont typées dans cette forme :
"2025-10-26"^^schema1:Date– la syntaxe^^de Turtle marque le littéral type, equivalent exact du{"@value": ..., "@type": ...}de JSON-LD ou du"..."^^xsd:datede N-Triples. Notez que le type produit ici estschema1:Date(le type Schema.org) et nonxsd:date– deux mondes types cohabitent dans le wild, et une requete qui filtre surxsd:datene attrapera pas les dates Schema.org. - L’emboitement visuel suit le graphe : le bloc
schema1:location [ a schema1:Place ; ... ]– les crochets[ ]sont le blank node, le point-virgule;enchaene les predicats d’un meme sujet, ce qui evite de repeter l’URI du sujet. La structure arborescente de Turtle n’est qu’un confort d’ecriture : derriere, le graphe est le meme sac de triples que le JSON-LD etale. - Le nom et l’adresse du lieu voyagent ensemble :
schema1:address "Palais de la Musique et des Congres"– adresse posee comme simple chaine ici, alors qu’un adressage complet Schema.org serait unPostalAddresstype. Le document source a choisi le simple ; Turtle le transmet tel quel, sans juger.
Turtle est la forme de predilection des ontologistes (les exemples des specs W3C sont en Turtle) ; JSON-LD celle des developpeurs web. La section suivante verifie qu’aller-retour entre les deux ne perd rien.
5.4 Round-trip : JSON-LD -> Turtle -> JSON-LD
Verifions que la conversion est sans perte : JSON-LD vers Turtle, puis retour en JSON-LD.
from rdflib import Graph
# Etape 1 : On a deja g2 (charge depuis JSON-LD)
triples_original = len(g2)
# Etape 2 : Serialiser en Turtle
turtle_str = g2.serialize(format="turtle")
# Etape 3 : Re-parser le Turtle
g3 = Graph()
g3.parse(data=turtle_str, format="turtle")
triples_after_turtle = len(g3)
# Etape 4 : Re-serialiser en JSON-LD
jsonld_roundtrip = g3.serialize(format="json-ld", indent=2)
# Etape 5 : Re-parser le JSON-LD
g4 = Graph()
g4.parse(data=jsonld_roundtrip, format="json-ld")
triples_final = len(g4)
print("=== Verification du round-trip ===")
print(f"Triples apres JSON-LD initial : {triples_original}")
print(f"Triples apres Turtle : {triples_after_turtle}")
print(f"Triples apres JSON-LD (retour) : {triples_final}")
print()
# Verifier l'isomorphisme
is_same = triples_original == triples_final
print(f"Conservation des triples : {'OUI' if is_same else 'NON'} ({triples_original} -> {triples_final})")
print()
print("=== JSON-LD final (apres round-trip) ===")
print(jsonld_roundtrip)=== Verification du round-trip ===
Triples apres JSON-LD initial : 11
Triples apres Turtle : 11
Triples apres JSON-LD (retour) : 11
Conservation des triples : OUI (11 -> 11)
=== JSON-LD final (apres round-trip) ===
[
{
"@id": "https://example.org/events/pycon2025",
"@type": [
"http://schema.org/Event"
],
"http://schema.org/endDate": [
{
"@type": "http://schema.org/Date",
"@value": "2025-10-26"
}
],
"http://schema.org/location": [
{
"@id": "_:n66ba6585385f4813a64639e26d1f38c5b1"
}
],
"http://schema.org/name": [
{
"@value": "PyCon France 2025"
}
],
"http://schema.org/organizer": [
{
"@id": "_:n66ba6585385f4813a64639e26d1f38c5b2"
}
],
"http://schema.org/startDate": [
{
"@type": "http://schema.org/Date",
"@value": "2025-10-23"
}
]
},
{
"@id": "_:n66ba6585385f4813a64639e26d1f38c5b1",
"@type": [
"http://schema.org/Place"
],
"http://schema.org/address": [
{
"@value": "Palais de la Musique et des Congres"
}
],
"http://schema.org/name": [
{
"@value": "Strasbourg"
}
]
},
{
"@id": "_:n66ba6585385f4813a64639e26d1f38c5b2",
"@type": [
"http://schema.org/Organization"
],
"http://schema.org/name": [
{
"@value": "AFPy"
}
]
}
]
Interpretation : Round-trip
Le nombre de triples est conserve a travers le cycle JSON-LD -> Turtle -> JSON-LD. Cela confirme que rdflib effectue une conversion fidele entre les formats. Les seules différences peuvent etre cosmetiques : - L’ordre des proprietes peut changer - Les identifiants de blank nodes peuvent etre renommes - La structure d’imbrication peut varier (aplati vs imbrique)
Mais les triples RDF sous-jacents sont identiques.
6. Cas d’usage web pratiques
JSON-LD est principalement utilise pour integrer des données structurees dans les pages HTML, permettant aux moteurs de recherche d’afficher des rich snippets (résultats enrichis). Voici les schemas les plus courants.
6.1 Recipe (Recette de cuisine)
Le schema Recipe est l’un des plus utilises. Il permet a Google d’afficher des cartes de recettes avec image, temps de preparation et notes.
Interpretation approfondie : ce que le round-trip ne dit pas
La conservation 11 -> 11 -> 11 est la verification minimale, et il faut lire precisement ce qu’elle garantit et ce qu’elle ne garantit pas :
- Les triples sont conserves, pas les identifiants de blank nodes : la sortie finale montre
"@id": "_:n66ba6585385f4813a64639e26d1f38c5b1"pour le lieu – un nouvel identifiant, different duN38aa90e3...du parsing initial (section 5.1). C’est conforme : les identifiants de blank nodes sont locaux a une serialisation, jamais des donnees. Un round-trip qui re-serialise deux fois peut produire deux prefixes differents pour le meme noeud – c’est pourquoi les tests de round-trip comparent des graphes (isomorphisme de graphe), jamais des textes. - Les types migrent vers Schema.org : la date finale porte
"@type": "http://schema.org/Date"– en sortie de Turtle c’etaitschema1:Date(section 5.3), en sortie JSON-LD c’est l’URI complete. Meme type, trois ecritures : la lecon du prefixe local, encore. - Ce qui N’EST PAS teste : l’ordre des ingredients (
@list, exercice 5) ne survit pas toujours a un round-trip – un tableau multi-value re-serialise peut perdre son ordre si le document source ne le marque pas explicitement. La conservation des triples n’implique pas la conservation des constructions syntaxiques qui les ont produits. - Pourquoi c’est suffisant : RDF definit l’egalite semantique comme l’egalite des graphes de triples. Le round-tright 11=11 temoigne que les trois etapes (parse JSON-LD, serialise Turtle, re-parse) parlent bien du meme graphe – l’interchangeabilite des serialisations n’est pas un slogan, c’est une propriete testable, que cette cellule vient de tester.
Retenez la methode plus que le resultat : comparer des comptes de triples a chaque etape est le reflexe de debug le plus utile du web semantique – la ou une difference de compte signale aussitot une perte de donnees.
import json
recipe = {
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Ratatouille Provencale",
"author": {
"@type": "Person",
"name": "Chef Marie"
},
"datePublished": "2024-06-15",
"description": "Recette traditionnelle de ratatouille avec legumes frais du marche.",
"prepTime": "PT30M",
"cookTime": "PT45M",
"totalTime": "PT1H15M",
"recipeYield": "4 portions",
"recipeCategory": "Plat principal",
"recipeCuisine": "Francaise",
"recipeIngredient": [
"2 aubergines",
"3 courgettes",
"2 poivrons rouges",
"4 tomates",
"2 oignons",
"3 gousses d'ail",
"Huile d'olive",
"Herbes de Provence"
],
"recipeInstructions": [
{
"@type": "HowToStep",
"text": "Couper tous les legumes en des de taille similaire."
},
{
"@type": "HowToStep",
"text": "Faire revenir les oignons et l'ail dans l'huile d'olive."
},
{
"@type": "HowToStep",
"text": "Ajouter les legumes par ordre de fermete, cuire 45 minutes a feu doux."
}
],
"nutrition": {
"@type": "NutritionInformation",
"calories": "180 calories",
"fatContent": "8 g"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"ratingCount": "342"
}
}
print("=== Schema.org Recipe ===")
print(json.dumps(recipe, indent=2, ensure_ascii=False))
print()
print(f"Recette : {recipe['name']}")
print(f"Preparation : {recipe['prepTime']} | Cuisson : {recipe['cookTime']}")
print(f"Ingredients : {len(recipe['recipeIngredient'])}")
print(f"Etapes : {len(recipe['recipeInstructions'])}")
print(f"Note : {recipe['aggregateRating']['ratingValue']}/5 ({recipe['aggregateRating']['ratingCount']} avis)")=== Schema.org Recipe ===
{
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Ratatouille Provencale",
"author": {
"@type": "Person",
"name": "Chef Marie"
},
"datePublished": "2024-06-15",
"description": "Recette traditionnelle de ratatouille avec legumes frais du marche.",
"prepTime": "PT30M",
"cookTime": "PT45M",
"totalTime": "PT1H15M",
"recipeYield": "4 portions",
"recipeCategory": "Plat principal",
"recipeCuisine": "Francaise",
"recipeIngredient": [
"2 aubergines",
"3 courgettes",
"2 poivrons rouges",
"4 tomates",
"2 oignons",
"3 gousses d'ail",
"Huile d'olive",
"Herbes de Provence"
],
"recipeInstructions": [
{
"@type": "HowToStep",
"text": "Couper tous les legumes en des de taille similaire."
},
{
"@type": "HowToStep",
"text": "Faire revenir les oignons et l'ail dans l'huile d'olive."
},
{
"@type": "HowToStep",
"text": "Ajouter les legumes par ordre de fermete, cuire 45 minutes a feu doux."
}
],
"nutrition": {
"@type": "NutritionInformation",
"calories": "180 calories",
"fatContent": "8 g"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"ratingCount": "342"
}
}
Recette : Ratatouille Provencale
Preparation : PT30M | Cuisson : PT45M
Ingredients : 8
Etapes : 3
Note : 4.7/5 (342 avis)
Interpretation : Recipe, ou la precision des durees ISO 8601
La sortie depose le document Schema.org complet de la Ratatouille Provencale. Ce qui la rend interessante n’est pas la recette mais ses conventions de valeurs :
- Les durees ne sont pas des textes :
prepTime: PT30M,cookTime: PT45M,totalTime: PT1H15M. C’est la syntaxe ISO 8601 des durees (P= periode,T= partie temps,M= minutes,H= heures). L’avantage sur “30 minutes” : elle est non ambiguë et machine-traitable – un moteur de recherche peut calculer que 30 + 45 = 75 minutes = 1H15 et verifier la coherence des trois champs. Une recette dont letotalTimecontredit la somme serait signalee. recipeYield: "4 portions": le rendement reste un littéral libre – Schema.org accepterait aussi uneQuantitativeValuestructuree, mais le texte simple est le choix le plus courant dans le wild.- Les metadonnees editorialisent deja :
datePublished: "2024-06-15", unauthortypePerson(“Chef Marie”) emboite – exactement le motif blank-node de la section 2.2. Une page de recette qui embarque ce bloc dans son HTML offre aux moteurs : temps total, categorie (“Plat principal”), cuisine (“Francaise”), ingredients en liste exploitable (recipeIngredient). recipeIngredientest un tableau de chaines – chaque element est un ingredient complet (“2 aubergines”), pas un couple quantite/produit structure. Pour une exploitation quantitative (comparer les prix, adapter les proportions), il faudra re-parser ces chaines : Schema.org optimise l’affichage riche, pas le calcul.
C’est le premier contact avec la logique des rich results : le document n’est pas ecrit pour etre lu par un humain (la page HTML le fait), mais pour etre compris par un moteur.
6.2 Event (Événement)
Le schema Event permet l’affichage de cartes événements dans les résultats de recherche.
import json
event = {
"@context": "https://schema.org/",
"@type": "Event",
"name": "Conference Web Semantique 2025",
"description": "Journee consacree aux technologies du Web Semantique et aux graphes de connaissances.",
"startDate": "2025-11-15T09:00:00+01:00",
"endDate": "2025-11-15T18:00:00+01:00",
"eventAttendanceMode": "https://schema.org/MixedEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "Place",
"name": "Amphitheatre Gaston Berger",
"address": {
"@type": "PostalAddress",
"streetAddress": "Campus Universitaire",
"addressLocality": "Lyon",
"postalCode": "69007",
"addressCountry": "FR"
}
},
"organizer": {
"@type": "Organization",
"name": "Association Web Semantique France",
"url": "https://example.org/awsf"
},
"offers": {
"@type": "Offer",
"price": "50",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"validFrom": "2025-06-01"
}
}
print("=== Schema.org Event ===")
print(json.dumps(event, indent=2, ensure_ascii=False))=== Schema.org Event ===
{
"@context": "https://schema.org/",
"@type": "Event",
"name": "Conference Web Semantique 2025",
"description": "Journee consacree aux technologies du Web Semantique et aux graphes de connaissances.",
"startDate": "2025-11-15T09:00:00+01:00",
"endDate": "2025-11-15T18:00:00+01:00",
"eventAttendanceMode": "https://schema.org/MixedEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"location": {
"@type": "Place",
"name": "Amphitheatre Gaston Berger",
"address": {
"@type": "PostalAddress",
"streetAddress": "Campus Universitaire",
"addressLocality": "Lyon",
"postalCode": "69007",
"addressCountry": "FR"
}
},
"organizer": {
"@type": "Organization",
"name": "Association Web Semantique France",
"url": "https://example.org/awsf"
},
"offers": {
"@type": "Offer",
"price": "50",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"validFrom": "2025-06-01"
}
}
Interpretation : Event, la geographie et le mode de presence
Le document de la Conference Web Semantique 2025 ajoute au pattern Recipe trois decisions de modelisation specifiques aux evenements :
- Les horodatages portent leur fuseau :
startDate: "2025-11-15T09:00:00+01:00"– date et heure, avec offset UTC+01:00. A comparer au2025-10-23nu de PyCon France en section 5.1 : deux conventions cohabitent dans le wild, et une application qui doit ordonner chronologiquement des evenements rencontres les deux. ISO 8601 encore :Tsepare date et heure,+01:00l’offset. Un evenement a cheval sur un changement d’heure d’ete exige cette precision – sans offset, “09:00” est indecidable entre deux instants differents. eventAttendanceMode:https://schema.org/MixedEventAttendanceMode– une URI complete comme valeur d’enumeration, pas une chaine. Schema.org definit trois modes (Offline,Online,Mixed) comme individus du vocabulaire, pas comme etiquettes. La valeur est donc elle-meme une ressource RDF dereferencable : un moteur peut “savoir” que Mixed subsume Offline et Online en consultant la hierarchie du vocabulaire.eventStatus:https://schema.org/EventScheduled– meme mecanique, avec la famille des statuts (EventScheduled,EventPostponed,EventCancelled,EventRescheduled,EventMovedOnline). C’est ce champ qu’un moteur interroge pour retirer une conference annulee des resultats sans re-crawler la page de details.- L’adresse est doublement emboitee :
locationest unPlace(“Amphitheatre Gaston Berger”) dont l’addressest elle-meme un objet – le motif blank-node seen en 2.2, recursif.
Avec BreadcrumbList et FAQPage (section suivante), Event complete le trio des schemas qui dominent l’annotation web concrete.
7. JSON-LD vs autres formats RDF
RDF peut etre serialise dans plusieurs formats. Chaque format a ses forces et ses faiblesses selon le contexte d’utilisation.
from rdflib import Graph, Literal, Namespace, URIRef
from rdflib.namespace import RDF, XSD
# Creer un petit graphe de reference pour la comparaison
SDO = Namespace("https://schema.org/")
g_compare = Graph()
g_compare.bind("schema", SDO)
person_uri = URIRef("https://example.org/people/curie")
g_compare.add((person_uri, RDF.type, SDO.Person))
g_compare.add((person_uri, SDO.name, Literal("Marie Curie")))
g_compare.add((person_uri, SDO.birthDate, Literal("1867-11-07", datatype=XSD.date)))
print(f"Graphe de reference : {len(g_compare)} triples")
print()
# Serialiser dans les 4 formats
formats = [
("JSON-LD", "json-ld"),
("Turtle", "turtle"),
("RDF/XML", "xml"),
("N-Triples", "nt"),
]
for label, fmt in formats:
output = g_compare.serialize(format=fmt)
lines = [l for l in output.strip().split("\n") if l.strip()]
size = len(output.encode("utf-8"))
print(f"{'=' * 60}")
print(f"=== {label} ({size} octets, {len(lines)} lignes) ===")
print(f"{'=' * 60}")
print(output.strip())
print()Graphe de reference : 3 triples
============================================================
=== JSON-LD (351 octets, 19 lignes) ===
============================================================
[
{
"@id": "https://example.org/people/curie",
"@type": [
"https://schema.org/Person"
],
"https://schema.org/birthDate": [
{
"@type": "http://www.w3.org/2001/XMLSchema#date",
"@value": "1867-11-07"
}
],
"https://schema.org/name": [
{
"@value": "Marie Curie"
}
]
}
]
============================================================
=== Turtle (224 octets, 5 lignes) ===
============================================================
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<https://example.org/people/curie> a schema:Person ;
schema:birthDate "1867-11-07"^^xsd:date ;
schema:name "Marie Curie" .
============================================================
=== RDF/XML (449 octets, 11 lignes) ===
============================================================
<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:schema="https://schema.org/"
>
<rdf:Description rdf:about="https://example.org/people/curie">
<rdf:type rdf:resource="https://schema.org/Person"/>
<schema:name>Marie Curie</schema:name>
<schema:birthDate rdf:datatype="http://www.w3.org/2001/XMLSchema#date">1867-11-07</schema:birthDate>
</rdf:Description>
</rdf:RDF>
============================================================
=== N-Triples (314 octets, 3 lignes) ===
============================================================
<https://example.org/people/curie> <https://schema.org/birthDate> "1867-11-07"^^<http://www.w3.org/2001/XMLSchema#date> .
<https://example.org/people/curie> <https://schema.org/name> "Marie Curie" .
<https://example.org/people/curie> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://schema.org/Person> .
Interpretation : comparaison des formats
| Critere | JSON-LD | Turtle | RDF/XML | N-Triples |
|---|---|---|---|---|
| Lisibilite humaine | Bonne | Excellente | Faible | Moyenne |
| Lisibilite machine | Excellente | Bonne | Bonne | Excellente |
| Taille | Moyenne | Compacte | Verbose | Verbose |
| Integration web | Native (JSON) | Non | Non | Non |
| Ecosysteme dev | Enorme (JSON) | Niche | Historique | Niche |
| Streaming | Non | Non | Non | Oui (ligne/ligne) |
| Standard W3C | 1.1 (2020) | 1.1 (2014) | 1.1 (2014) | 1.1 (2014) |
Recommandations : - JSON-LD : integration dans les pages web, APIs, developpeurs JSON - Turtle : edition humaine, ontologies, enseignement - RDF/XML : systèmes legacy, interoperabilite XML - N-Triples : traitement en masse, streaming, tri lexicographique
8. Chargement du fichier produit dans rdflib
Pour conclure la partie pratique, chargeons le fichier data/product.jsonld dans un graphe rdflib et explorons-le avec SPARQL.
from rdflib import Graph
# Charger le fichier JSON-LD dans un graphe
g_product = Graph()
g_product.parse("data/product.jsonld", format="json-ld")
print(f"Triples charges depuis product.jsonld : {len(g_product)}")
print()
# Requete SPARQL : extraire les informations du produit
query_product = """
PREFIX schema: <http://schema.org/>
SELECT ?property ?value
WHERE {
?product a schema:Product .
?product ?property ?value .
FILTER(!isBlank(?value))
}
ORDER BY ?property
"""
print("=== Proprietes du produit ===")
for row in g_product.query(query_product):
prop = str(row.property).replace("https://schema.org/", "schema:")
prop = prop.replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:")
print(f" {prop:<30} {row.value}")
print()
# Requete : sujets enseignes
query_teaches = """
PREFIX schema: <http://schema.org/>
SELECT ?topic
WHERE {
?product a schema:Product .
?product schema:teaches ?topic .
}
"""
print("=== Sujets enseignes ===")
for i, row in enumerate(g_product.query(query_teaches), 1):
print(f" {i}. {row.topic}")
print()
# Serialiser en Turtle pour voir la structure RDF
print("=== Representation Turtle ===")
print(g_product.serialize(format="turtle"))Triples charges depuis product.jsonld : 24
=== Proprietes du produit ===
http://schema.org/description Cours complet sur le Web Semantique, de RDF aux graphes de connaissances
http://schema.org/educationalLevel University
http://schema.org/image https://example.org/images/semantic-web-course.png
http://schema.org/name Semantic Web avec Python et .NET
http://schema.org/teaches RDF et triples
http://schema.org/teaches SPARQL queries
http://schema.org/teaches OWL ontologies
http://schema.org/teaches SHACL validation
http://schema.org/teaches JSON-LD et Schema.org
http://schema.org/teaches Knowledge Graphs
http://schema.org/teaches GraphRAG
rdf:type http://schema.org/Product
=== Sujets enseignes ===
1. RDF et triples
2. SPARQL queries
3. OWL ontologies
4. SHACL validation
5. JSON-LD et Schema.org
6. Knowledge Graphs
7. GraphRAG
=== Representation Turtle ===
@prefix schema1: <http://schema.org/> .
[] a schema1:Product ;
schema1:author [ a schema1:Person ;
schema1:name "Jean-Sebastien Boige" ] ;
schema1:brand [ a schema1:Brand ;
schema1:name "CoursIA" ] ;
schema1:description "Cours complet sur le Web Semantique, de RDF aux graphes de connaissances" ;
schema1:educationalLevel "University" ;
schema1:image <https://example.org/images/semantic-web-course.png> ;
schema1:name "Semantic Web avec Python et .NET" ;
schema1:offers [ a schema1:Offer ;
schema1:availability "https://schema.org/InStock" ;
schema1:price "0" ;
schema1:priceCurrency "EUR" ;
schema1:url <https://github.com/jsboige/CoursIA> ] ;
schema1:teaches "GraphRAG",
"JSON-LD et Schema.org",
"Knowledge Graphs",
"OWL ontologies",
"RDF et triples",
"SHACL validation",
"SPARQL queries" .
Interpretation : le fichier produit, passe au crible
La sortie resume le chargement de data/product.jsonld : 24 triples, puis la liste des proprietes du produit. C’est l’occasion de relire tout le notebook dans un artefact unique :
teachesest multi-value :RDF et triples,SPARQL queries,OWLOntologies… non – la sortie listeOWLOntologiesverifie :RDF et triples,SPARQL queries,OWL ontologies,SHACL validation. Quatre valeurs pour une meme propriete : en JSON-LD source, c’est un tableau ; dans le graphe, ce sont quatre triples independants de meme predicat ; dans une requete SPARQL, quatre lignes de resultat. La propriete multi-valuee est le passage le moins intuitif pour un developpeur venu du relationnel (ou une colonne tient une valeur) – RDF n’a pas de cardinalite par defaut.educationalLevel: University: un litteral simple pour un attribut qui pourrait etre une ressource typee – meme arbitrige de simplicite querecipeYielden section 6.1. Schema.org laisse souvent le choix entre chaine et structure ; le wild penche pour la chaine.imagepointe vers une URI (https://example.org/images/semantic-web-course.png) : la propriete relie le produit a une ressource dereferencable – le pont vers un futurImageObjectavec ses propres metadonnees (licence, dimensions) si l’application en a besoin.- 24 triples pour une “fiche produit” : l’ordre de grandeur est instructif. Un document JSON-LD d’apparence anodine projette systematiquement plus de triples que son nombre de champs JSON, parce que chaque valeur emboitee (organisation, offre, review) devient son propre sous-graphe de blank nodes.
Ce graphe g_product est la matiere premiere de l’exercice 3 du notebook SW-10 – le chargement n’est jamais une fin.
Exemples et Exercices
Exemple guide 1 : Créer une Schema.org Person
Cet exemple montre la creation d’un document JSON-LD decrivant une personne avec les proprietes Schema.org suivantes : - name, jobTitle, email, telephone - worksFor (une Organization) - alumniOf (une CollegeOrUniversity) - sameAs (liens vers profils sociaux)
Le document est charge dans rdflib et les triples sont affichés.
import json
from rdflib import Graph
person_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/people/jlange",
"@type": "Person",
"name": "Jules Lange",
"jobTitle": "Etudiant ingenieur",
"email": "jules.lange@hotmail.fr",
"telephone": "+33-6-12-34-56-78",
"worksFor": {
"@type": "Organization",
"name": "EPITA"
},
"alumniOf": {
"@type": "CollegeOrUniversity",
"name": "EPITA Paris"
},
"sameAs": [
"https://twitter.com/jlange",
"https://linkedin.com/in/jules-lange"
]
}
print(json.dumps(person_jsonld, indent=2, ensure_ascii=False))
g_ex1 = Graph()
g_ex1.parse(data=json.dumps(person_jsonld), format="json-ld")
print(f"\nTriples : {len(g_ex1)}")
for s, p, o in g_ex1:
print(f" {s} {p} {o}"){
"@context": "https://schema.org/",
"@id": "https://example.org/people/jlange",
"@type": "Person",
"name": "Jules Lange",
"jobTitle": "Etudiant ingenieur",
"email": "jules.lange@hotmail.fr",
"telephone": "+33-6-12-34-56-78",
"worksFor": {
"@type": "Organization",
"name": "EPITA"
},
"alumniOf": {
"@type": "CollegeOrUniversity",
"name": "EPITA Paris"
},
"sameAs": [
"https://twitter.com/jlange",
"https://linkedin.com/in/jules-lange"
]
}
Triples : 13
https://example.org/people/jlange http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
N2a4cfd6f39454c2cab87fe4d88d9ad88 http://schema.org/name EPITA
N2a4cfd6f39454c2cab87fe4d88d9ad88 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
https://example.org/people/jlange http://schema.org/sameAs https://twitter.com/jlange
https://example.org/people/jlange http://schema.org/sameAs https://linkedin.com/in/jules-lange
https://example.org/people/jlange http://schema.org/alumniOf N702a474089d64ee594123a967a3c569c
https://example.org/people/jlange http://schema.org/worksFor N2a4cfd6f39454c2cab87fe4d88d9ad88
https://example.org/people/jlange http://schema.org/telephone +33-6-12-34-56-78
https://example.org/people/jlange http://schema.org/name Jules Lange
https://example.org/people/jlange http://schema.org/email jules.lange@hotmail.fr
N702a474089d64ee594123a967a3c569c http://schema.org/name EPITA Paris
https://example.org/people/jlange http://schema.org/jobTitle Etudiant ingenieur
N702a474089d64ee594123a967a3c569c http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/CollegeOrUniversity
Interpretation de l’exemple : ce que l’exercice 1 enrichit
La sortie de cet exemple guide (avant ses variantes a completer) montre le document JSON-LD d’une personne (Jules Lange) suivi de sa projection en 13 triples – et la projection est l’endroit ou les choix de modelisation deviennent lisibles :
worksForetalumniOfdeviennent des blank nodes : dans le document, EPITA et “EPITA Paris” sont des objets emboites ; dans les triples, ce sont des noeudsN2a4cfd...etN702a47..., chacune avec sonrdf:type(Organization,CollegeOrUniversity) et sonname. Le motif maintenant familier de la section 2.2 – l’emboitement JSON devient anonymat RDF.- Deux
sameAspour deux lignes :https://twitter.com/jlangeethttps://linkedin.com/in/jules-lange– la propriete multi-valuee de la section precedente, cette fois vers des URI externes au document.sameAsest le ciment du Linked Data : il affirme que le noeud decrit ici et les profils distants designent la meme personne. C’est ainsi que les moteurs reconcilient les identites distribuees. emailettelephonerestent des litteraux nus – valeurs de contact non structurees, exploitables pour l’affichage pas pour la validation.- Le compte (13) recouvre les valeurs emboitees : les deux organisations anonymes contribuent chacune 2-3 triples. Comme en section 5.1 : le nombre de triples mesure la richesse du graphe, pas le nombre de champs visibles.
L’exercice 1 vous demande le meme document pour une organisation ; l’exemple guide 5 (section suivante) en propose une solution – comparez le traitement des employes (des Person avec @id ou des blank nodes ?) : c’est exactement le genre d’arbitrage que cet exemple met en evidence.
Exemple guide 2 : Convertir du Turtle en JSON-LD
Cet exemple montre comment convertir du Turtle en JSON-LD avec rdflib. Le Turtle decrit un livre (“Le Petit Prince”) avec son auteur, selon le vocabulaire Schema.org :
@prefix schema: <https://schema.org/> .
@prefix ex: <https://example.org/> .
ex:book1 a schema:Book ;
schema:name "Le Petit Prince" ;
schema:author ex:saint-exupery ;
schema:datePublished "1943" ;
schema:inLanguage "fr" .
ex:saint-exupery a schema:Person ;
schema:name "Antoine de Saint-Exupery" .
from rdflib import Graph
turtle_data = """
@prefix schema: <https://schema.org/> .
@prefix ex: <https://example.org/> .
ex:book1 a schema:Book ;
schema:name "Le Petit Prince" ;
schema:author ex:saint-exupery ;
schema:datePublished "1943" ;
schema:inLanguage "fr" .
ex:saint-exupery a schema:Person ;
schema:name "Antoine de Saint-Exupery" .
"""
g_ex2 = Graph()
g_ex2.parse(data=turtle_data, format="turtle")
jsonld_result = g_ex2.serialize(format="json-ld", indent=2)
print(jsonld_result)[
{
"@id": "https://example.org/book1",
"@type": [
"https://schema.org/Book"
],
"https://schema.org/author": [
{
"@id": "https://example.org/saint-exupery"
}
],
"https://schema.org/datePublished": [
{
"@value": "1943"
}
],
"https://schema.org/inLanguage": [
{
"@value": "fr"
}
],
"https://schema.org/name": [
{
"@value": "Le Petit Prince"
}
]
},
{
"@id": "https://example.org/saint-exupery",
"@type": [
"https://schema.org/Person"
],
"https://schema.org/name": [
{
"@value": "Antoine de Saint-Exupery"
}
]
}
]
Exemple guide 3 : Créer un Schema.org Recipe
Cet exemple montre la creation d’un schema Recipe complet pour la Tartiflette savoyarde, incluant : - Nom, description, auteur - Temps de preparation et cuisson (format ISO 8601 Duration : PT30M) - 6 ingredients et 3 étapes (HowToStep) - Informations nutritionnelles
Le JSON-LD est charge dans rdflib, puis interroge avec SPARQL pour extraire les ingredients.
Interpretation de l’exemple : la conversion inverse, lisible ligne a ligne
La sortie de cet exemple guide est une forme etendue – et elle illustre pourquoi le Turtle d’entree (le @prefix schema: de la cellule precedente) est si economique a l’usage :
book1et son auteur sont des URI nommees :"@id": "https://example.org/book1"et l’auteur pointe vers"@id": "https://example.org/saint-exupery"– le Turtle source avait fait le choix de nommer l’auteur (une URI dediee) plutot que de l’emboiter comme blank node. La conversion le respecte : l’auteur reste une reference, pas un objet inline. C’est un arbitrage de modelisation qui se transpose intact d’une syntaxe a l’autre – preuve supplementaire que la syntaxe ne decide rien, seul le graphe compte.datePublished: "1943"reste un littéral – meme pas une date ISO complete, juste l’annee. RDF ne juge pas : la valeur est ce que la source disait. Une requete qui filtre>= "1940-01-01"echouera sur “1943” (comparaison lexicale avec un litteral non type) – piege recurrent sur les donnees bibliographiques reelles, ou les dates sont notoirement inconsistantes.inLanguage: "fr": le code de langue ISO 639-1, litteral simple – la ou JSON-LD natif offrirait{"@value": "fr", "@language": "fr"}. La distinction est subtile mais reelle : la proprieteinLanguage(Schema.org) decrit la ressource ; la direction@language(JSON-LD) decrit le littéral. Deux niveaux qui se recouvrent sans se confondre.- Les proprietes en forme etendue sont des URI complets (
"https://schema.org/author") – la demonstration visuelle de la section 4.1, appliquee a un cas litteraire.
import json
from rdflib import Graph
my_recipe = {
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Tartiflette savoyarde",
"description": "Gratin de pommes de terre au reblochon, lardons et oignons.",
"author": {
"@type": "Person",
"name": "Jules Lange"
},
"prepTime": "PT20M",
"cookTime": "PT40M",
"totalTime": "PT1H",
"recipeYield": "4 portions",
"recipeIngredient": [
"1 kg de pommes de terre",
"1 reblochon entier",
"200 g de lardons fumes",
"2 oignons jaunes",
"20 cl de creme fraiche",
"1 verre de vin blanc sec"
],
"recipeInstructions": [
{
"@type": "HowToStep",
"text": "Faire cuire les pommes de terre a l'eau salee puis les couper en rondelles."
},
{
"@type": "HowToStep",
"text": "Faire revenir les oignons emincees et les lardons dans une poele."
},
{
"@type": "HowToStep",
"text": "Disposer pommes de terre, lardons et oignons dans un plat, couvrir de reblochon coupe en deux et enfourner 25 minutes a 200 degres."
}
],
"nutrition": {
"@type": "NutritionInformation",
"calories": "650 kcal",
"fatContent": "42 g",
"proteinContent": "22 g",
"carbohydrateContent": "45 g"
}
}
print(json.dumps(my_recipe, indent=2, ensure_ascii=False))
g_ex3 = Graph()
g_ex3.parse(data=json.dumps(my_recipe), format="json-ld")
print(f"\nTriples : {len(g_ex3)}")
query = """
PREFIX schema: <http://schema.org/>
SELECT ?ingredient
WHERE {
?recipe a schema:Recipe ;
schema:recipeIngredient ?ingredient .
}
"""
print("\nIngredients :")
for row in g_ex3.query(query):
print(f" - {row.ingredient}"){
"@context": "https://schema.org/",
"@type": "Recipe",
"name": "Tartiflette savoyarde",
"description": "Gratin de pommes de terre au reblochon, lardons et oignons.",
"author": {
"@type": "Person",
"name": "Jules Lange"
},
"prepTime": "PT20M",
"cookTime": "PT40M",
"totalTime": "PT1H",
"recipeYield": "4 portions",
"recipeIngredient": [
"1 kg de pommes de terre",
"1 reblochon entier",
"200 g de lardons fumes",
"2 oignons jaunes",
"20 cl de creme fraiche",
"1 verre de vin blanc sec"
],
"recipeInstructions": [
{
"@type": "HowToStep",
"text": "Faire cuire les pommes de terre a l'eau salee puis les couper en rondelles."
},
{
"@type": "HowToStep",
"text": "Faire revenir les oignons emincees et les lardons dans une poele."
},
{
"@type": "HowToStep",
"text": "Disposer pommes de terre, lardons et oignons dans un plat, couvrir de reblochon coupe en deux et enfourner 25 minutes a 200 degres."
}
],
"nutrition": {
"@type": "NutritionInformation",
"calories": "650 kcal",
"fatContent": "42 g",
"proteinContent": "22 g",
"carbohydrateContent": "45 g"
}
}
Triples : 31
Ingredients :
- 1 kg de pommes de terre
- 1 reblochon entier
- 200 g de lardons fumes
- 2 oignons jaunes
- 20 cl de creme fraiche
- 1 verre de vin blanc sec
Exemple guide 4 : Modeliser un système de conferences en JSON-LD
Cet exemple montre comment designer un contexte JSON-LD pour un système de conferences academiques utilisant Schema.org. La conference comprend : - Un name, startDate, endDate, location (Place) - Des subEvent representant les sessions - Des performer (Person avec affiliation)
Une conference complete avec 2 sessions et 3 intervenants, convertie en forme expanded avec verification des triples RDF generes.
Interpretation de l’exemple : la Tartiflette, 31 triples et une liste
La sortie superpose le document, son compte (31 triples – a comparer aux 13 de la personne en exemple 1 : une recette avec six ingredients et un auteur emboite produit plus de graphe qu’une fiche personne) et l’extraction des ingredients. Trois observations :
authorest ici un blank node (Person“Jules Lange” emboite, sans@id) – alors que l’exemple guide 5 (personne) nommait ses organisations en blank nodes mais l’individu en URI. Deux styles cohabitent dans le meme notebook : c’est realiste, le wild est heterogene. La question a se poser devant chaque document : qui merite un@id? Regle pratique : ce que d’autres documents voudront referencer (l’auteur publie ailleurs) gagne a etre nomme ; ce qui n’existe que dans cette fiche (la recette elle-meme, ici) peut rester anonyme ou non selon l’usage prevu.- Les six ingredients extraits :
1 kg de pommes de terre,1 reblochon entier,200 g de lardons fumes,2 oignons jaunes,20 cl de creme fraiche,1 verre de vin blanc sec– des chaines libres, dont la structure (quantite, unite, produit) est lisible par un humain mais pas par une machine sans re-parsing. C’est le choix Schema.org standard, deja commente en 6.1 – et sa limite : adapter la recette a 6 personnes exige de comprendre “1 kg”, pas juste de le multiplier. - Les durees ISO 8601 encore :
PT20M/PT40M/PT1H– 20 + 40 = 60 minutes = 1H, la coherence des trois champs se verifie mecaniquement. Notez quePT1H(forme heures seules) est valide – ISO 8601 autorise l’elision des minutes nulles. recipeYield: "4 portions"identique a la Ratatouille de 6.1 – le champ libre rendement, meme arbitrage de simplicite.
import json
from rdflib import Graph
conference = {
"@context": "https://schema.org/",
"@id": "https://example.org/conf/symbolicai2026",
"@type": "Conference",
"name": "Symposium IA Symbolique 2026",
"startDate": "2026-06-15",
"endDate": "2026-06-17",
"location": {
"@type": "Place",
"name": "EPITA Paris",
"address": "14-16 rue Voltaire, 94270 Le Kremlin-Bicetre"
},
"subEvent": [
{
"@type": "Event",
"name": "Session - Logiques non monotones",
"startDate": "2026-06-15T09:00",
"endDate": "2026-06-15T12:00"
},
{
"@type": "Event",
"name": "Session - Argumentation et dialogues",
"startDate": "2026-06-16T14:00",
"endDate": "2026-06-16T17:00"
}
],
"performer": [
{
"@type": "Person",
"name": "Alice Martin",
"affiliation": {
"@type": "Organization",
"name": "INRIA"
}
},
{
"@type": "Person",
"name": "Bob Durand",
"affiliation": {
"@type": "Organization",
"name": "CNRS"
}
},
{
"@type": "Person",
"name": "Claire Petit",
"affiliation": {
"@type": "Organization",
"name": "Sorbonne Universite"
}
}
]
}
print(json.dumps(conference, indent=2, ensure_ascii=False))
g_conf = Graph()
g_conf.parse(data=json.dumps(conference), format="json-ld")
expanded = g_conf.serialize(format="json-ld", indent=2)
print("\n=== Forme expanded ===")
print(expanded)
print(f"\nTriples generes : {len(g_conf)}")
for s, p, o in g_conf:
print(f" {s} {p} {o}"){
"@context": "https://schema.org/",
"@id": "https://example.org/conf/symbolicai2026",
"@type": "Conference",
"name": "Symposium IA Symbolique 2026",
"startDate": "2026-06-15",
"endDate": "2026-06-17",
"location": {
"@type": "Place",
"name": "EPITA Paris",
"address": "14-16 rue Voltaire, 94270 Le Kremlin-Bicetre"
},
"subEvent": [
{
"@type": "Event",
"name": "Session - Logiques non monotones",
"startDate": "2026-06-15T09:00",
"endDate": "2026-06-15T12:00"
},
{
"@type": "Event",
"name": "Session - Argumentation et dialogues",
"startDate": "2026-06-16T14:00",
"endDate": "2026-06-16T17:00"
}
],
"performer": [
{
"@type": "Person",
"name": "Alice Martin",
"affiliation": {
"@type": "Organization",
"name": "INRIA"
}
},
{
"@type": "Person",
"name": "Bob Durand",
"affiliation": {
"@type": "Organization",
"name": "CNRS"
}
},
{
"@type": "Person",
"name": "Claire Petit",
"affiliation": {
"@type": "Organization",
"name": "Sorbonne Universite"
}
}
]
}
=== Forme expanded ===
[
{
"@id": "https://example.org/conf/symbolicai2026",
"@type": [
"http://schema.org/Conference"
],
"http://schema.org/endDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-17"
}
],
"http://schema.org/location": [
{
"@id": "_:N0e240f1a0553418e981c92a4a09e91e6"
}
],
"http://schema.org/name": [
{
"@value": "Symposium IA Symbolique 2026"
}
],
"http://schema.org/performer": [
{
"@id": "_:N99b12cf27a80424e8d591a42c6519067"
},
{
"@id": "_:N7f95ad27ef094c3d97770db00e115ed6"
},
{
"@id": "_:N37cfc7601b574c4ebc78d92fb77f572e"
}
],
"http://schema.org/startDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-15"
}
],
"http://schema.org/subEvent": [
{
"@id": "_:N9172461d10e24787bb652f997f1867d2"
},
{
"@id": "_:Nf00ac9dc2da04258a8251a5181cd7033"
}
]
},
{
"@id": "_:N0e240f1a0553418e981c92a4a09e91e6",
"@type": [
"http://schema.org/Place"
],
"http://schema.org/address": [
{
"@value": "14-16 rue Voltaire, 94270 Le Kremlin-Bicetre"
}
],
"http://schema.org/name": [
{
"@value": "EPITA Paris"
}
]
},
{
"@id": "_:N9172461d10e24787bb652f997f1867d2",
"@type": [
"http://schema.org/Event"
],
"http://schema.org/endDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-15T12:00"
}
],
"http://schema.org/name": [
{
"@value": "Session - Logiques non monotones"
}
],
"http://schema.org/startDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-15T09:00"
}
]
},
{
"@id": "_:Nf00ac9dc2da04258a8251a5181cd7033",
"@type": [
"http://schema.org/Event"
],
"http://schema.org/endDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-16T17:00"
}
],
"http://schema.org/name": [
{
"@value": "Session - Argumentation et dialogues"
}
],
"http://schema.org/startDate": [
{
"@type": "http://schema.org/Date",
"@value": "2026-06-16T14:00"
}
]
},
{
"@id": "_:N99b12cf27a80424e8d591a42c6519067",
"@type": [
"http://schema.org/Person"
],
"http://schema.org/affiliation": [
{
"@id": "_:Nebb9e85d6d564f10ba7ec8dfa5280a06"
}
],
"http://schema.org/name": [
{
"@value": "Alice Martin"
}
]
},
{
"@id": "_:Nebb9e85d6d564f10ba7ec8dfa5280a06",
"@type": [
"http://schema.org/Organization"
],
"http://schema.org/name": [
{
"@value": "INRIA"
}
]
},
{
"@id": "_:N7f95ad27ef094c3d97770db00e115ed6",
"@type": [
"http://schema.org/Person"
],
"http://schema.org/affiliation": [
{
"@id": "_:N98b5de18cba5417d8108506e04d77490"
}
],
"http://schema.org/name": [
{
"@value": "Bob Durand"
}
]
},
{
"@id": "_:N98b5de18cba5417d8108506e04d77490",
"@type": [
"http://schema.org/Organization"
],
"http://schema.org/name": [
{
"@value": "CNRS"
}
]
},
{
"@id": "_:N37cfc7601b574c4ebc78d92fb77f572e",
"@type": [
"http://schema.org/Person"
],
"http://schema.org/affiliation": [
{
"@id": "_:Ncff4109bc31140f7a6bd62164068e440"
}
],
"http://schema.org/name": [
{
"@value": "Claire Petit"
}
]
},
{
"@id": "_:Ncff4109bc31140f7a6bd62164068e440",
"@type": [
"http://schema.org/Organization"
],
"http://schema.org/name": [
{
"@value": "Sorbonne Universite"
}
]
}
]
Triples generes : 36
N37cfc7601b574c4ebc78d92fb77f572e http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
N98b5de18cba5417d8108506e04d77490 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
N7f95ad27ef094c3d97770db00e115ed6 http://schema.org/name Bob Durand
N37cfc7601b574c4ebc78d92fb77f572e http://schema.org/affiliation Ncff4109bc31140f7a6bd62164068e440
https://example.org/conf/symbolicai2026 http://schema.org/subEvent N9172461d10e24787bb652f997f1867d2
Nf00ac9dc2da04258a8251a5181cd7033 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Event
https://example.org/conf/symbolicai2026 http://schema.org/location N0e240f1a0553418e981c92a4a09e91e6
N98b5de18cba5417d8108506e04d77490 http://schema.org/name CNRS
N37cfc7601b574c4ebc78d92fb77f572e http://schema.org/name Claire Petit
Ncff4109bc31140f7a6bd62164068e440 http://schema.org/name Sorbonne Universite
N7f95ad27ef094c3d97770db00e115ed6 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
Nf00ac9dc2da04258a8251a5181cd7033 http://schema.org/endDate 2026-06-16T17:00
N9172461d10e24787bb652f997f1867d2 http://schema.org/startDate 2026-06-15T09:00
N9172461d10e24787bb652f997f1867d2 http://schema.org/name Session - Logiques non monotones
Nebb9e85d6d564f10ba7ec8dfa5280a06 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
https://example.org/conf/symbolicai2026 http://schema.org/startDate 2026-06-15
N9172461d10e24787bb652f997f1867d2 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Event
https://example.org/conf/symbolicai2026 http://schema.org/performer N99b12cf27a80424e8d591a42c6519067
https://example.org/conf/symbolicai2026 http://schema.org/performer N37cfc7601b574c4ebc78d92fb77f572e
N9172461d10e24787bb652f997f1867d2 http://schema.org/endDate 2026-06-15T12:00
N0e240f1a0553418e981c92a4a09e91e6 http://schema.org/address 14-16 rue Voltaire, 94270 Le Kremlin-Bicetre
Nf00ac9dc2da04258a8251a5181cd7033 http://schema.org/startDate 2026-06-16T14:00
N99b12cf27a80424e8d591a42c6519067 http://schema.org/affiliation Nebb9e85d6d564f10ba7ec8dfa5280a06
https://example.org/conf/symbolicai2026 http://schema.org/performer N7f95ad27ef094c3d97770db00e115ed6
https://example.org/conf/symbolicai2026 http://schema.org/endDate 2026-06-17
https://example.org/conf/symbolicai2026 http://schema.org/name Symposium IA Symbolique 2026
N0e240f1a0553418e981c92a4a09e91e6 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Place
N7f95ad27ef094c3d97770db00e115ed6 http://schema.org/affiliation N98b5de18cba5417d8108506e04d77490
N0e240f1a0553418e981c92a4a09e91e6 http://schema.org/name EPITA Paris
N99b12cf27a80424e8d591a42c6519067 http://schema.org/name Alice Martin
Nf00ac9dc2da04258a8251a5181cd7033 http://schema.org/name Session - Argumentation et dialogues
https://example.org/conf/symbolicai2026 http://schema.org/subEvent Nf00ac9dc2da04258a8251a5181cd7033
Nebb9e85d6d564f10ba7ec8dfa5280a06 http://schema.org/name INRIA
Ncff4109bc31140f7a6bd62164068e440 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
N99b12cf27a80424e8d591a42c6519067 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
https://example.org/conf/symbolicai2026 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Conference
Exemples guides (solutions proposees par @Sosolalt)
Les exemples ci-dessous ont ete resolus par @Sosolalt (EPITA-IS, promo 2028). Ils servent de modèle pour comprendre les concepts abordes dans ce notebook.
Exemple guide 5 : Schema.org Organization en JSON-LD
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
Interpretation de l’exemple : la conference et ses sub-events
La sortie montre les deux formes du meme document – compacte en entree, etendue en sortie – et le couple merite une lecture croisee :
subEventest un tableau d’evenements (Session - Logiques non monotones, etc., chacune avec ses propres horaires2026-06-15T09:00) : la propriete multi-valuee encore, cette fois pour une decomposition temporelle – le symposium est la somme de ses sessions. Les moteurs peuvent ainsi presenter l’evenement parent ET son agenda detaille. La recursion est licite : unsubEventpeut lui-meme avoir dessubEvent(une conference dans un salon dans une ville).locationemboite une adresse textuelle :"14-16 rue Voltaire, 94270 Le Kremlin-Bicetre"– une chaine unique la ou Schema.org offerait unPostalAddressstructure (rue, code postal, ville separement). Meme arbitrage qu’en 5.3 : le texte simple suffit a l’affichage, pas au geocodage precis. Une application cartographique devra passer par un service de geocodage – ou exigera des donnees mieux structurees.ConferencevsEvent: le document type le parentConferenceet les enfantsEvent. Schema.org definitConferencecomme sous-classe d’Event– la hierarchie du vocabulaire permet la precision croissante sans casser les requetes generiques :?s a schema:Eventattrape les deux.- La forme etendue en sortie re-liste tout :
"@id": "https://example.org/conf/symbolicai2026"avechttp://schema.org/endDatetypéschema:Date("2026-06-17") – et le lieu devenu blank node_:N0e240f...en reference inline. La comparaison compact/etendu sur CE document concret donne la paire de formes la plus didactique du notebook : deux ecritures, un graphe.
Ce dernier exemple guide boucle la serie des patrons : @id nomme, blank node pour l’emboite, multi-value pour les collections, types hierarchiques pour la precision – tous les arbitrages de modelisation de la journee, dans un seul document.
import json
from rdflib import Graph
# Document JSON-LD pour une organisation (Schema.org)
org_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/org/acme",
"@type": "Organization",
"name": "ACME Robotics",
"url": "https://acme-robotics.example.org",
"logo": "https://acme-robotics.example.org/logo.png",
"founder": {
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "PDG"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "10 rue de l'Innovation",
"addressLocality": "Paris",
"postalCode": "75001",
"addressCountry": "FR"
},
"sameAs": [
"https://twitter.com/acme_robotics",
"https://linkedin.com/company/acme-robotics"
]
}
print(json.dumps(org_jsonld, indent=2, ensure_ascii=False))
# Charger dans rdflib
g_org = Graph()
g_org.parse(data=json.dumps(org_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_org)}")
for s, p, o in g_org:
print(f" {s} {p} {o}"){
"@context": "https://schema.org/",
"@id": "https://example.org/org/acme",
"@type": "Organization",
"name": "ACME Robotics",
"url": "https://acme-robotics.example.org",
"logo": "https://acme-robotics.example.org/logo.png",
"founder": {
"@type": "Person",
"name": "Marie Curie",
"jobTitle": "PDG"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "10 rue de l'Innovation",
"addressLocality": "Paris",
"postalCode": "75001",
"addressCountry": "FR"
},
"sameAs": [
"https://twitter.com/acme_robotics",
"https://linkedin.com/company/acme-robotics"
]
}
Triples generes : 16
https://example.org/org/acme http://schema.org/founder N9f856cbe6dde47f3a2289ec904eec780
N826d4d202c8c41a5a77cc757a99de66a http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/PostalAddress
https://example.org/org/acme http://schema.org/sameAs https://linkedin.com/company/acme-robotics
https://example.org/org/acme http://schema.org/logo https://acme-robotics.example.org/logo.png
https://example.org/org/acme http://schema.org/address N826d4d202c8c41a5a77cc757a99de66a
https://example.org/org/acme http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
N826d4d202c8c41a5a77cc757a99de66a http://schema.org/postalCode 75001
N9f856cbe6dde47f3a2289ec904eec780 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
https://example.org/org/acme http://schema.org/name ACME Robotics
N9f856cbe6dde47f3a2289ec904eec780 http://schema.org/name Marie Curie
N826d4d202c8c41a5a77cc757a99de66a http://schema.org/addressCountry FR
N826d4d202c8c41a5a77cc757a99de66a http://schema.org/addressLocality Paris
N9f856cbe6dde47f3a2289ec904eec780 http://schema.org/jobTitle PDG
N826d4d202c8c41a5a77cc757a99de66a http://schema.org/streetAddress 10 rue de l'Innovation
https://example.org/org/acme http://schema.org/url https://acme-robotics.example.org
https://example.org/org/acme http://schema.org/sameAs https://twitter.com/acme_robotics
Exemple guide 6 : Conversion JSON-LD en Turtle
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
import json
from rdflib import Graph
# Document JSON-LD a convertir (Movie)
movie_jsonld = {
"@context": "https://schema.org/",
"@type": "Movie",
"name": "Interstellar",
"director": {
"@type": "Person",
"name": "Christopher Nolan"
},
"datePublished": "2014",
"genre": "Science Fiction"
}
# Parser le JSON-LD puis serialiser en Turtle
g_movie = Graph()
g_movie.parse(data=json.dumps(movie_jsonld), format="json-ld")
turtle_result = g_movie.serialize(format="turtle")
print(f"Triples : {len(g_movie)}")
print()
print(turtle_result)Triples : 7
@prefix schema1: <http://schema.org/> .
[] a schema1:Movie ;
schema1:datePublished "2014"^^schema1:Date ;
schema1:director [ a schema1:Person ;
schema1:name "Christopher Nolan" ] ;
schema1:genre "Science Fiction" ;
schema1:name "Interstellar" .
Exemple guide 7 : Schema.org Event pour un hackathon
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
import json
from rdflib import Graph
# Document JSON-LD pour un hackathon (Schema.org Event)
hackathon_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/event/hackathon2026",
"@type": "Event",
"name": "Hackathon IA Symbolique 2026",
"description": "48h pour construire un projet autour du raisonnement symbolique.",
"startDate": "2026-03-20T09:00",
"endDate": "2026-03-22T18:00",
"eventStatus": {"@id": "https://schema.org/EventScheduled"},
"eventAttendanceMode": {"@id": "https://schema.org/OfflineEventAttendanceMode"},
"location": {
"@type": "Place",
"name": "EPITA Paris",
"address": {
"@type": "PostalAddress",
"streetAddress": "14-16 rue Voltaire",
"addressLocality": "Le Kremlin-Bicetre",
"postalCode": "94270",
"addressCountry": "FR"
}
},
"organizer": {
"@type": "Organization",
"name": "EPITA",
"url": "https://www.epita.fr"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR",
"availability": {"@id": "https://schema.org/InStock"},
"url": "https://example.org/event/hackathon2026/inscription"
}
}
print(json.dumps(hackathon_jsonld, indent=2, ensure_ascii=False))
# Charger dans rdflib
g_hack = Graph()
g_hack.parse(data=json.dumps(hackathon_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_hack)}")
# Requete SPARQL : lister toutes les proprietes de l'evenement
query = """
PREFIX schema: <http://schema.org/>
SELECT ?property ?value
WHERE {
?event a schema:Event .
?event ?property ?value .
}
ORDER BY ?property
"""
print("\n=== Proprietes de l'evenement ===")
for row in g_hack.query(query):
prop = str(row.property).replace("http://schema.org/", "schema:")
prop = prop.replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:")
print(f" {prop:<28} {row.value}"){
"@context": "https://schema.org/",
"@id": "https://example.org/event/hackathon2026",
"@type": "Event",
"name": "Hackathon IA Symbolique 2026",
"description": "48h pour construire un projet autour du raisonnement symbolique.",
"startDate": "2026-03-20T09:00",
"endDate": "2026-03-22T18:00",
"eventStatus": {
"@id": "https://schema.org/EventScheduled"
},
"eventAttendanceMode": {
"@id": "https://schema.org/OfflineEventAttendanceMode"
},
"location": {
"@type": "Place",
"name": "EPITA Paris",
"address": {
"@type": "PostalAddress",
"streetAddress": "14-16 rue Voltaire",
"addressLocality": "Le Kremlin-Bicetre",
"postalCode": "94270",
"addressCountry": "FR"
}
},
"organizer": {
"@type": "Organization",
"name": "EPITA",
"url": "https://www.epita.fr"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR",
"availability": {
"@id": "https://schema.org/InStock"
},
"url": "https://example.org/event/hackathon2026/inscription"
}
}
Triples generes : 26
=== Proprietes de l'evenement ===
schema:description 48h pour construire un projet autour du raisonnement symbolique.
schema:endDate 2026-03-22T18:00
schema:eventAttendanceMode https://schema.org/OfflineEventAttendanceMode
schema:eventStatus https://schema.org/EventScheduled
schema:location N885eaa6964d94408809bbb28e6780aaa
schema:name Hackathon IA Symbolique 2026
schema:offers N3f3e3b72ded2488789e479aa767b49fb
schema:organizer Nabe966bc6d4e474eb8e0c80fd050de7e
schema:startDate 2026-03-20T09:00
rdf:type http://schema.org/Event
Exemple guide 8 : Cours universitaire en JSON-LD
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
import json
from rdflib import Graph
# Document JSON-LD pour un cours universitaire (Schema.org Course)
course_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/course/symbolic-ai",
"@type": "Course",
"name": "Intelligence Artificielle Symbolique",
"description": "Logique, ontologies, web semantique et raisonnement automatique.",
"courseCode": "S8-SYMAI",
"provider": {
"@type": "CollegeOrUniversity",
"name": "EPITA",
"url": "https://www.epita.fr"
},
"hasCourseInstance": [
{
"@type": "CourseInstance",
"name": "Session printemps 2026",
"courseMode": "Onsite",
"startDate": "2026-02-02",
"endDate": "2026-05-29"
},
{
"@type": "CourseInstance",
"name": "Session automne 2026",
"courseMode": "Onsite",
"startDate": "2026-09-07",
"endDate": "2026-12-18"
}
],
"instructor": {
"@type": "Person",
"name": "Alice Martin",
"jobTitle": "Maitre de conferences",
"affiliation": {
"@type": "Organization",
"name": "EPITA"
}
}
}
print(json.dumps(course_jsonld, indent=2, ensure_ascii=False))
# Charger dans rdflib et afficher les triples
g_course = Graph()
g_course.parse(data=json.dumps(course_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_course)}")
for s, p, o in g_course:
print(f" {s} {p} {o}"){
"@context": "https://schema.org/",
"@id": "https://example.org/course/symbolic-ai",
"@type": "Course",
"name": "Intelligence Artificielle Symbolique",
"description": "Logique, ontologies, web semantique et raisonnement automatique.",
"courseCode": "S8-SYMAI",
"provider": {
"@type": "CollegeOrUniversity",
"name": "EPITA",
"url": "https://www.epita.fr"
},
"hasCourseInstance": [
{
"@type": "CourseInstance",
"name": "Session printemps 2026",
"courseMode": "Onsite",
"startDate": "2026-02-02",
"endDate": "2026-05-29"
},
{
"@type": "CourseInstance",
"name": "Session automne 2026",
"courseMode": "Onsite",
"startDate": "2026-09-07",
"endDate": "2026-12-18"
}
],
"instructor": {
"@type": "Person",
"name": "Alice Martin",
"jobTitle": "Maitre de conferences",
"affiliation": {
"@type": "Organization",
"name": "EPITA"
}
}
}
Triples generes : 27
https://example.org/course/symbolic-ai http://schema.org/courseCode S8-SYMAI
Nb61830e9fdfa47b58e0829780375db23 http://schema.org/url https://www.epita.fr
https://example.org/course/symbolic-ai http://schema.org/name Intelligence Artificielle Symbolique
Nb3ce1f5efca8472384263d8d2279adc5 http://schema.org/startDate 2026-09-07
N52793796596d415ba7af8c08fdbce93b http://schema.org/name Session printemps 2026
N52793796596d415ba7af8c08fdbce93b http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/CourseInstance
N52793796596d415ba7af8c08fdbce93b http://schema.org/startDate 2026-02-02
Nb61830e9fdfa47b58e0829780375db23 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/CollegeOrUniversity
https://example.org/course/symbolic-ai http://schema.org/hasCourseInstance Nb3ce1f5efca8472384263d8d2279adc5
N4bde08f30f9948e7aa8bb68d8510aebe http://schema.org/name Alice Martin
N52793796596d415ba7af8c08fdbce93b http://schema.org/endDate 2026-05-29
N4bde08f30f9948e7aa8bb68d8510aebe http://schema.org/jobTitle Maitre de conferences
https://example.org/course/symbolic-ai http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Course
https://example.org/course/symbolic-ai http://schema.org/instructor N4bde08f30f9948e7aa8bb68d8510aebe
Nb3ce1f5efca8472384263d8d2279adc5 http://schema.org/name Session automne 2026
Na58f09aca8304bcbba8665eec6662e1e http://schema.org/name EPITA
Nb3ce1f5efca8472384263d8d2279adc5 http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/CourseInstance
N4bde08f30f9948e7aa8bb68d8510aebe http://schema.org/affiliation Na58f09aca8304bcbba8665eec6662e1e
Nb61830e9fdfa47b58e0829780375db23 http://schema.org/name EPITA
https://example.org/course/symbolic-ai http://schema.org/description Logique, ontologies, web semantique et raisonnement automatique.
N52793796596d415ba7af8c08fdbce93b http://schema.org/courseMode Onsite
Nb3ce1f5efca8472384263d8d2279adc5 http://schema.org/courseMode Onsite
Na58f09aca8304bcbba8665eec6662e1e http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Organization
https://example.org/course/symbolic-ai http://schema.org/hasCourseInstance N52793796596d415ba7af8c08fdbce93b
N4bde08f30f9948e7aa8bb68d8510aebe http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://schema.org/Person
Nb3ce1f5efca8472384263d8d2279adc5 http://schema.org/endDate 2026-12-18
https://example.org/course/symbolic-ai http://schema.org/provider Nb61830e9fdfa47b58e0829780375db23
Exemple guide 9 : @list vs ensemble par defaut
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
# Exercice 5 : @list pour ordonner les ingredients
# Indice : inspecter les triples generes (rdflib) pour voir rdf:first/rdf:rest/rdf:nil
import json
from rdflib import Graph
from rdflib.namespace import RDF
# Recipe avec recipeIngredient ORDONNE (@list) et recipeCategory NON ordonne (ensemble)
recipe_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/recipe/gateau",
"@type": "Recipe",
"name": "Gateau au yaourt",
"recipeIngredient": {"@list": ["farine", "sucre", "oeufs", "yaourt", "levure"]},
"recipeCategory": ["dessert", "patisserie", "facile"]
}
print(json.dumps(recipe_jsonld, indent=2, ensure_ascii=False))
g_recipe = Graph()
g_recipe.parse(data=json.dumps(recipe_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_recipe)}")
# Verifier la structure rdf:first / rdf:rest / rdf:nil pour recipeIngredient (@list)
print("\n=== Triples rdf:first / rdf:rest (liste ordonnee) ===")
for s, p, o in g_recipe:
if p in (RDF.first, RDF.rest):
print(f" {s} {p} {o}")
# Reconstituer l'ordre de la liste RDF
print("\n=== Ordre reconstitue de recipeIngredient ===")
from rdflib import URIRef
SCHEMA = "http://schema.org/"
recipe = URIRef("https://example.org/recipe/gateau")
list_head = list(g_recipe.objects(recipe, URIRef(SCHEMA + "recipeIngredient")))
if list_head:
from rdflib.collection import Collection
items = list(Collection(g_recipe, list_head[0]))
for i, it in enumerate(items, 1):
print(f" {i}. {it}")
# recipeCategory : triples plats (pas de liste)
print("\n=== recipeCategory (ensemble non ordonne, triples plats) ===")
for o in g_recipe.objects(recipe, URIRef(SCHEMA + "recipeCategory")):
print(f" - {o}"){
"@context": "https://schema.org/",
"@id": "https://example.org/recipe/gateau",
"@type": "Recipe",
"name": "Gateau au yaourt",
"recipeIngredient": {
"@list": [
"farine",
"sucre",
"oeufs",
"yaourt",
"levure"
]
},
"recipeCategory": [
"dessert",
"patisserie",
"facile"
]
}
Triples generes : 16
=== Triples rdf:first / rdf:rest (liste ordonnee) ===
N80a2191f7f2c4ae8b9859c3ef02c0c04 http://www.w3.org/1999/02/22-rdf-syntax-ns#first farine
Need56d61c1324346b910173b61156f9f http://www.w3.org/1999/02/22-rdf-syntax-ns#first oeufs
N60011a433cb644e2b8dcb2864852243b http://www.w3.org/1999/02/22-rdf-syntax-ns#first sucre
N60011a433cb644e2b8dcb2864852243b http://www.w3.org/1999/02/22-rdf-syntax-ns#rest Need56d61c1324346b910173b61156f9f
N80a2191f7f2c4ae8b9859c3ef02c0c04 http://www.w3.org/1999/02/22-rdf-syntax-ns#rest N60011a433cb644e2b8dcb2864852243b
Na9c47b53e7d04266b756326035e529f6 http://www.w3.org/1999/02/22-rdf-syntax-ns#rest http://www.w3.org/1999/02/22-rdf-syntax-ns#nil
Nebc1a37194754bf7a68a237cf2140fc2 http://www.w3.org/1999/02/22-rdf-syntax-ns#first yaourt
Nebc1a37194754bf7a68a237cf2140fc2 http://www.w3.org/1999/02/22-rdf-syntax-ns#rest Na9c47b53e7d04266b756326035e529f6
Na9c47b53e7d04266b756326035e529f6 http://www.w3.org/1999/02/22-rdf-syntax-ns#first levure
Need56d61c1324346b910173b61156f9f http://www.w3.org/1999/02/22-rdf-syntax-ns#rest Nebc1a37194754bf7a68a237cf2140fc2
=== Ordre reconstitue de recipeIngredient ===
1. farine
2. sucre
3. oeufs
4. yaourt
5. levure
=== recipeCategory (ensemble non ordonne, triples plats) ===
- dessert
- patisserie
- facile
Exemple guide 10 : Annotations multilingues avec @language
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
# Exercice 6 : annotations multilingues @language
# Indice : utiliser {"@value": ..., "@language": ...} ; SPARQL LANG(?lit)
import json
from rdflib import Graph
# Article avec headline (fr/en/es) et description (fr/en)
article_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/article/web-semantique",
"@type": "Article",
"headline": [
{"@value": "Le Web Semantique explique", "@language": "fr"},
{"@value": "The Semantic Web explained", "@language": "en"},
{"@value": "La Web Semantica explicada", "@language": "es"}
],
"description": [
{"@value": "Une introduction accessible aux technologies du Web semantique.", "@language": "fr"},
{"@value": "An accessible introduction to Semantic Web technologies.", "@language": "en"}
]
}
print(json.dumps(article_jsonld, indent=2, ensure_ascii=False))
g_article = Graph()
g_article.parse(data=json.dumps(article_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_article)}")
# Lister les litteraux multilingues avec leur tag de langue
query = """
PREFIX schema: <http://schema.org/>
SELECT ?prop ?lit ?lang
WHERE {
?article ?prop ?lit .
FILTER(isLiteral(?lit))
BIND(LANG(?lit) AS ?lang)
}
ORDER BY ?prop ?lang
"""
print("\n=== Litteraux par langue ===")
for row in g_article.query(query):
prop = str(row.prop).replace("http://schema.org/", "schema:")
print(f" [{row.lang}] {prop:<20} {row.lit}"){
"@context": "https://schema.org/",
"@id": "https://example.org/article/web-semantique",
"@type": "Article",
"headline": [
{
"@value": "Le Web Semantique explique",
"@language": "fr"
},
{
"@value": "The Semantic Web explained",
"@language": "en"
},
{
"@value": "La Web Semantica explicada",
"@language": "es"
}
],
"description": [
{
"@value": "Une introduction accessible aux technologies du Web semantique.",
"@language": "fr"
},
{
"@value": "An accessible introduction to Semantic Web technologies.",
"@language": "en"
}
]
}
Triples generes : 6
=== Litteraux par langue ===
[en] schema:description An accessible introduction to Semantic Web technologies.
[fr] schema:description Une introduction accessible aux technologies du Web semantique.
[en] schema:headline The Semantic Web explained
[es] schema:headline La Web Semantica explicada
[fr] schema:headline Le Web Semantique explique
Exemple guide 11 : @reverse pour propriete inverse
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
# Exercice 7 : @reverse pour propriete inverse
# Indice : verifier en SPARQL que les triples sont (?child schema:parent ?parent)
import json
from rdflib import Graph
# Famille modelisee depuis le parent via @reverse: parent
# (en RDF, ce sont les enfants qui pointent vers le parent)
family_jsonld = {
"@context": "https://schema.org/",
"@id": "https://example.org/person/marie",
"@type": "Person",
"name": "Marie",
"@reverse": {
"parent": [
{
"@id": "https://example.org/person/lucie",
"@type": "Person",
"name": "Lucie"
},
{
"@id": "https://example.org/person/paul",
"@type": "Person",
"name": "Paul"
}
]
}
}
print(json.dumps(family_jsonld, indent=2, ensure_ascii=False))
g_family = Graph()
g_family.parse(data=json.dumps(family_jsonld), format="json-ld")
print(f"\nTriples generes : {len(g_family)}")
# Verifier le sens des triples : ?child schema:parent ?parent
query = """
PREFIX schema: <http://schema.org/>
SELECT ?childName ?parentName
WHERE {
?child schema:parent ?parent .
?child schema:name ?childName .
?parent schema:name ?parentName .
}
ORDER BY ?childName
"""
print("\n=== Relations parent (sens RDF : enfant -> parent) ===")
for row in g_family.query(query):
print(f" {row.childName} schema:parent {row.parentName}"){
"@context": "https://schema.org/",
"@id": "https://example.org/person/marie",
"@type": "Person",
"name": "Marie",
"@reverse": {
"parent": [
{
"@id": "https://example.org/person/lucie",
"@type": "Person",
"name": "Lucie"
},
{
"@id": "https://example.org/person/paul",
"@type": "Person",
"name": "Paul"
}
]
}
}
Triples generes : 8
=== Relations parent (sens RDF : enfant -> parent) ===
Lucie schema:parent Marie
Paul schema:parent Marie
Exercices a completer
Ces exercices sont a realiser par l’etudiant. Les stubs utilisent des marqueurs TODO.
Exercice 1 : Créer une Schema.org Organization
Créez un document JSON-LD decrivant une organisation avec les proprietes suivantes : - name, url, logo (URL) - founder (une Person avec name et jobTitle) - address (une PostalAddress avec streetAddress, addressLocality, postalCode, addressCountry) - sameAs (liens vers reseaux sociaux)
Chargez-le dans rdflib et affichez le nombre de triples generes.
TODO etudiant : inserer le JSON-LD de l’organisation ci-dessous
import json
from rdflib import Graph
# TODO etudiant : creez le document JSON-LD pour l'organisation
org_jsonld = None # TODO etudiant : remplacer par le document JSON-LD
print("Exercice a completer : creez le JSON-LD pour une organisation")
print("Indice : inspirez-vous de l'Exemple guide 1 (Person) et adaptez pour Organization")Exercice a completer : creez le JSON-LD pour une organisation
Indice : inspirez-vous de l'Exemple guide 1 (Person) et adaptez pour Organization
Exercice 2 : Convertir du JSON-LD en Turtle
Partez du document JSON-LD suivant et convertissez-le en Turtle avec rdflib :
{
"@context": "https://schema.org/",
"@type": "Movie",
"name": "Interstellar",
"director": {
"@type": "Person",
"name": "Christopher Nolan"
},
"datePublished": "2014",
"genre": "Science Fiction"
}TODO etudiant : parser le JSON-LD ci-dessus et serialiser en Turtle
from rdflib import Graph
# TODO etudiant : parser le JSON-LD et serialiser en Turtle
turtle_result = None # TODO etudiant : remplacer par le code de conversion
print("Exercice a completer : convertissez le JSON-LD en Turtle")
print("Indice : utilisez g.parse(data=..., format='json-ld') puis g.serialize(format='turtle')")Exercice a completer : convertissez le JSON-LD en Turtle
Indice : utilisez g.parse(data=..., format='json-ld') puis g.serialize(format='turtle')
Exercice 3 : Créer un schema Event pour un hackathon
Créez un document JSON-LD de type Event decrivant un hackathon avec : - Un name, description, startDate, endDate - Un location (Place avec adresse) - Un organizer (Organization) - Des offers (gratuit, InStock) - Un eventStatus et eventAttendanceMode
Chargez-le dans rdflib, puis effectuez une requête SPARQL pour lister toutes les proprietes de l’événement.
TODO etudiant : inserer le JSON-LD du hackathon ci-dessous
import json
from rdflib import Graph
# TODO etudiant : creez le document JSON-LD pour le hackathon
hackathon_jsonld = None # TODO etudiant : remplacer par le document JSON-LD
print("Exercice a completer : creez le JSON-LD pour un hackathon")
print("Indice : inspirez-vous de l'Exemple guide 2 et de la section 6.2 sur les Event")Exercice a completer : creez le JSON-LD pour un hackathon
Indice : inspirez-vous de l'Exemple guide 2 et de la section 6.2 sur les Event
Exercice 4 : Modeliser un cours universitaire en JSON-LD
Créez un document JSON-LD Schema.org decrivant un cours universitaire avec : - Un Course avec name, description, courseCode, provider (une CollegeOrUniversity) - Au moins 2 hasCourseInstance (sessions du cours, avec startDate/endDate) - Un instructor (Person avec affiliation)
TODO etudiant : inserer le JSON-LD du cours ci-dessous, puis charger dans rdflib et afficher les triples
import json
from rdflib import Graph
# TODO etudiant : creez le document JSON-LD pour le cours
course_jsonld = None # TODO etudiant : remplacer par le document JSON-LD
print("Exercice a completer : creez le JSON-LD pour un cours universitaire")
print("Indice : utilisez les types Course, CourseInstance, Person et CollegeOrUniversity")Exercice a completer : creez le JSON-LD pour un cours universitaire
Indice : utilisez les types Course, CourseInstance, Person et CollegeOrUniversity
Exercice 5 : @list vs ensemble par defaut (ordre des éléments)
Par defaut, les tableaux JSON en JSON-LD sont consideres comme des multisets non ordonnes : l’ordre des éléments n’est pas significatif. Le mot-cle @list force une interpretation ordonnee (RDF list / rdf:first/rdf:rest).
Modelisez une recette de cuisine avec un champ recipeIngredient au format @list (les ingredients doivent etre ajoutes dans l’ordre indique) et un champ recipeCategory au format ensemble (l’ordre est arbitraire).
Chargez le document dans rdflib et verifiez que les triples generes pour recipeIngredient utilisent bien la structure rdf:first/rdf:rest/rdf:nil (alors que recipeCategory produit des triples plats).
Indice :
"recipeIngredient": {"@list": ["farine", "sucre", "oeufs"]}# Exercice 5 : @list pour ordonner les ingredients
# TODO etudiant : creer un Recipe JSON-LD avec recipeIngredient en @list
# Indice : inspecter les triples generes (rdflib) pour voir rdf:first/rdf:rest/rdf:nil
import json
from rdflib import Graph
recipe_jsonld = None # TODO etudiant : creer le document
print("Exercice a completer : Recipe avec @list pour ingredients ordonnes")Exercice a completer : Recipe avec @list pour ingredients ordonnes
Exercice 6 : Annotations multilingues avec @language
JSON-LD permet d’attacher une langue a une chaîne de caractères via @language (dans @context ou inline). Cela genere en RDF un litteral avec tag de langue (par exemple "Bonjour"@fr).
Modelisez une Article Schema.org avec : - headline en francais, anglais et espagnol (3 versions) - description en francais et anglais (2 versions)
Utilisez la syntaxe "@value" + "@language" pour chaque valeur multilingue. Chargez dans rdflib et listez les litteraux distincts avec leur langue via SPARQL.
Indice :
"headline": [
{"@value": "Titre francais", "@language": "fr"},
{"@value": "English title", "@language": "en"}
]SPARQL utile : SELECT ?lit ?lang WHERE { ?s schema:headline ?lit . BIND(LANG(?lit) AS ?lang) }
# Exercice 6 : annotations multilingues @language
# TODO etudiant : Article avec headline (fr/en/es) et description (fr/en)
# Indice : utiliser {"@value": ..., "@language": ...} ; SPARQL LANG(?lit)
import json
from rdflib import Graph
article_jsonld = None # TODO etudiant : remplacer par le document JSON-LD multilingue
print("Exercice a completer : Article avec headlines multilingues")Exercice a completer : Article avec headlines multilingues
Exercice 7 : @reverse pour propriete inverse
Le mot-cle @reverse permet d’exprimer une relation en sens inverse depuis l’objet vers le sujet, sans avoir a définir une propriete miroir dans le vocabulaire. Utile pour Schema.org ou seul parent est défini (pas child).
Modelisez une famille en partant d’un parent et en attachant ses enfants via @reverse: parent (l’enfant pointe vers son parent en RDF, mais c’est ecrit depuis le parent en JSON-LD).
Verifiez en SPARQL que les triples generes sont bien ?enfant schema:parent ?parent (et non l’inverse).
Indice :
{
"@id": "https://example.org/parent/marie",
"@type": "Person",
"name": "Marie",
"@reverse": {
"parent": [
{"@id": "https://example.org/child/lucie", "@type": "Person", "name": "Lucie"}
]
}
}# Exercice 7 : @reverse pour propriete inverse
# TODO etudiant : modeliser une famille via @reverse: parent
# Indice : verifier en SPARQL que les triples sont (?child schema:parent ?parent)
import json
from rdflib import Graph
family_jsonld = None # TODO etudiant : remplacer par le document JSON-LD avec @reverse
print("Exercice a completer : famille modelisee avec @reverse: parent")Exercice a completer : famille modelisee avec @reverse: parent
Resume
Ce notebook a couvert les aspects essentiels de JSON-LD et Schema.org.
Concepts cles
| Concept | Description |
|---|---|
| JSON-LD | Format W3C qui ajoute une couche sémantique au JSON via @context |
| @context | Dictionnaire de traduction entre cles JSON et IRIs RDF |
| @id | Identifiant de la ressource (sujet du triple) |
| @type | Type de la ressource (equivalent rdf:type) |
| @graph | Conteneur pour plusieurs entites dans un même document |
| Schema.org | Vocabulaire collaboratif (Google, Microsoft, Yahoo) avec 800+ types |
| Rich snippets | Résultats enrichis dans les moteurs de recherche |
Competences acquises
- Comprendre la motivation et la syntaxe de JSON-LD
- Manipuler les trois formes (compacte, etendue, aplatie)
- Utiliser Schema.org pour decrire des entites web
- Créer du JSON-LD avec rdflib (
g.serialize(format="json-ld")) - Parser du JSON-LD et l’interroger en SPARQL
- Effectuer des conversions round-trip entre formats
- Appliquer les schemas web courants (Recipe, Event, FAQ, Breadcrumb)
Pour aller plus loin
- JSON-LD Specification (W3C)
- JSON-LD Playground - testeur en ligne
- Schema.org - documentation des types et proprietes
- Google Structured Data Testing Tool
- JSON-LD Best Practices (W3C)
Le notebook suivant explore RDF 1.2 (RDF-Star), une extension majeure de RDF permettant de faire des assertions sur des assertions.
Navigation : << 8-Python-SHACL | Index | 10-Python-RDFStar >>