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


Installation des dependances

# Dependances pre-provisionnees (rdflib) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.

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 @type et name ne 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 – name pointe vers http://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 de homepage : 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_Turing resterait un simple texte.
  • Contexte combine : un tableau qui melange la delegation distante et un mapping local (foaf vers http://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 de rdf: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 @id parlent de la meme ressource : c’est le mecanisme par lequel le Linked Data agrège des sources disparates.
  • @type devient rdf:type : schema:Person n’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 : birthPlace ne peut pas etre un littéral (une place est une entite avec ses propres proprietes), et la sortie le materialise – le triple schema:birthPlace _:b0 pointe vers un noeud anonyme, lui-meme type schema:Place et nomme "London". Le prefixe _:b0 est 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 triple schema: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 en xsd: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 @id internes 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 :

  1. 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.
  2. Types et proprietes vivent dans le meme namespace – contrairement a OWL ou T-Box et A-Box sont souvent separees. https://schema.org/name n’est pas schema: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.
  3. Le prefixe schema: n’a rien d’officiel – c’est une convention d’ecriture locale. On croise aussi schema1: (la section 5.3 en produira un automatiquement), qui designe exactement le meme namespace http://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 plus name ou jobTitle mais 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).
  • @type est 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_babbage avec son jobTitle “Inventor”, ada_lovelace avec sa date typée, la relation knows liant 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 ex vers https://example.org/people/, les proprietes raccourcies ("jobTitle": "schema:jobTitle"), et pour knows la structure {"@id": ..., "@type": "@id"} – exactement la forme qu’on ecrivait a la main pour homepage en 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. Un xsd:date entre, un xsd:date sort.
  • Le @graph structure 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) et N38aa90e3b7d045f394cacfce2316e23c (le lieu Strasbourg) n’existaient pas comme entites nommees dans le document source – ils y etaient des valeurs emboitees de organizer et location. Le parseur leur a attribue des identifiants internes (prefixes N, 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 @id dans le document source.
  • Les dates passent nues : startDate 2025-10-23 et endDate 2025-10-26 apparaissent 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, type http://schema.org/Organization, pas de date de debut), l’evenement (PyCon France 2025, type http://schema.org/Event, demarre le 2025-10-23) et le lieu (Strasbourg, type http://schema.org/Place). Trois types differents, une seule requete : le motif ?nom ?type ?date avec OPTIONAL pour 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’OPTIONAL qui travaille. Sans lui, la jointure aurait elimine AFPy et Strasbourg du resultat – une organisation n’a pas de startDate. Le tableau est la pour rappeler que l’absence de valeur en RDF est une information, pas un bug.
  • La date 2025-10-23 arrive comme littéral nu (la chaine du document source) – on la triera lexicographiquement, pas chronologiquement, tant qu’elle n’est pas typée xsd: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 de schema:. Rdflib genere ce suffixe numeral pour eviter une collision dans la table des prefixes de la session – et il designe le meme namespace http://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:date de N-Triples. Notez que le type produit ici est schema1:Date (le type Schema.org) et non xsd:date – deux mondes types cohabitent dans le wild, et une requete qui filtre sur xsd:date ne 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 un PostalAddress type. 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 du N38aa90e3... 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’etait schema1: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 le totalTime contredit la somme serait signalee.
  • recipeYield: "4 portions" : le rendement reste un littéral libre – Schema.org accepterait aussi une QuantitativeValue structuree, mais le texte simple est le choix le plus courant dans le wild.
  • Les metadonnees editorialisent deja : datePublished: "2024-06-15", un author type Person (“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).
  • recipeIngredient est 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 au 2025-10-23 nu 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 : T separe date et heure, +01:00 l’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 : location est un Place (“Amphitheatre Gaston Berger”) dont l’address est 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.

Interpretation : BreadcrumbList et FAQPage, les deux piliers discrets

La sortie imprime les deux structures en forme lisible :

BreadcrumbList – le fil d’Ariane a trois niveaux : Accueil (https://example.org/), Cours IA (https://example.org/cours-ia/), Web Semantique (https://example.org/cours-ia/semantic-web/). Deux choses a noter :

  • Le schema n’est pas une liste plate mais une chaine ordonnee d’objets ListItem, chacun avec sa position (1, 2, 3) – l’ordre est une donnee explicite, pas un effet de presentation. Un moteur peut afficher le chemin dans la page de resultats au-dessus du titre (les “rich snippets” de navigation), et la position typee evite toute ambiguite si le tableau etait reordonne.
  • Chaque item porte a la fois un name et une URL : le fil d’Ariane est aussi un plan de site local, exploitable par les crawlers independamment de l’affichage.

FAQPage – les trois questions affichees (“Qu’est-ce que JSON-LD ?”, “Pourquoi utiliser Schema.org ?”, “JSON-LD remplace-t-il Turtle ou RDF/XML ?”) suivent le meme principe : des objets Question contenant des Answer, structures et non du texte libre. L’enjeu est double :

  • Extension conversationnelle : les assistants et moteurs indexent les paires question/reponse pour repondre directement – la FAQ bien balisee est ce qui remonte dans les encarts “questions assocées”.
  • La troisieme reponse de la sortie merite lecture : elle rappelle que JSON-LD est une serialisation de RDF parmi d’autres – exactement la demonstration des sections 5.3-5.4. Le contenu pedagogique et l’annotation marchent ici du meme pas.

Ces deux schemas ne decrivent rien de “vrai” dans le monde (aucune recette, aucun evenement) : ils decrivent la page elle-meme et son usage – le tour hermeneutique du web semantique applique a sa propre interface.

Ces schemas sont integres dans les pages HTML via la balise :

<script type="application/ld+json">
{ ... le JSON-LD ici ... }
</script>
Schema Rich snippet Google Impact SEO
Recipe Carte avec image, temps, note Fort (carousels recettes)
Event Date, lieu, prix Moyen a fort
BreadcrumbList Chemin hiérarchique dans les SERP Moyen
FAQPage Questions depliables sous le résultat Fort (visibilite accrue)

Conseil pratique : Utilisez l’outil Google Rich Results Test pour valider vos schemas avant deploiement.


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 :

  • teaches est multi-value : RDF et triples, SPARQL queries, OWLOntologies… non – la sortie liste OWLOntologies verifie : 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 que recipeYield en section 6.1. Schema.org laisse souvent le choix entre chaine et structure ; le wild penche pour la chaine.
  • image pointe vers une URI (https://example.org/images/semantic-web-course.png) : la propriete relie le produit a une ressource dereferencable – le pont vers un futur ImageObject avec 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 :

  • worksFor et alumniOf deviennent des blank nodes : dans le document, EPITA et “EPITA Paris” sont des objets emboites ; dans les triples, ce sont des noeuds N2a4cfd... et N702a47..., chacune avec son rdf:type (Organization, CollegeOrUniversity) et son name. Le motif maintenant familier de la section 2.2 – l’emboitement JSON devient anonymat RDF.
  • Deux sameAs pour deux lignes : https://twitter.com/jlange et https://linkedin.com/in/jules-lange – la propriete multi-valuee de la section precedente, cette fois vers des URI externes au document. sameAs est 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.
  • email et telephone restent 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 :

  • book1 et 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 propriete inLanguage (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 :

  • author est 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 que PT1H (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 :

  • subEvent est un tableau d’evenements (Session - Logiques non monotones, etc., chacune avec ses propres horaires 2026-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 : un subEvent peut lui-meme avoir des subEvent (une conference dans un salon dans une ville).
  • location emboite une adresse textuelle : "14-16 rue Voltaire, 94270 Le Kremlin-Bicetre" – une chaine unique la ou Schema.org offerait un PostalAddress structure (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.
  • Conference vs Event : le document type le parent Conference et les enfants Event. Schema.org definit Conference comme sous-classe d’Event – la hierarchie du vocabulaire permet la precision croissante sans casser les requetes generiques : ?s a schema:Event attrape les deux.
  • La forme etendue en sortie re-liste tout : "@id": "https://example.org/conf/symbolicai2026" avec http://schema.org/endDate typé 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

  1. Comprendre la motivation et la syntaxe de JSON-LD
  2. Manipuler les trois formes (compacte, etendue, aplatie)
  3. Utiliser Schema.org pour decrire des entites web
  4. Créer du JSON-LD avec rdflib (g.serialize(format="json-ld"))
  5. Parser du JSON-LD et l’interroger en SPARQL
  6. Effectuer des conversions round-trip entre formats
  7. Appliquer les schemas web courants (Recipe, Event, FAQ, Breadcrumb)

Pour aller plus loin


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 >>

Retour au sommet