# Dependances pre-provisionnees (rdflib) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.SW-4b-Python-SPARQL
Navigation : Index | << SW-4 C# | SW-5 C# >>
SPARQL en Python avec rdflib
Ce notebook est un sidetrack optionnel qui presente l’equivalent Python des concepts SPARQL du notebook SW-4 (dotNetRDF). Vous y decouvrirez comment executer des requêtes SPARQL sur des graphes en memoire avec rdflib.
Objectifs d’apprentissage
A la fin de ce notebook, vous saurez : 1. Executer des requêtes SPARQL SELECT sur des graphes rdflib 2. Utiliser FILTER, OPTIONAL, UNION et ORDER BY 3. Interroger des hiérarchies de classes RDFS 4. Faire la correspondance entre dotNetRDF et rdflib pour SPARQL
Prerequis
- SW-4-CSharp-SPARQL recommande (pour la comprehension conceptuelle)
- Python 3.10+
Duree estimee : 25 minutes
Note : Ce notebook se concentre sur SPARQL local. Pour les endpoints distants (DBpedia, Wikidata), voir le sidetrack SW-5b-Python-LinkedData.
1. Installation et Preparation
rdflib integre un moteur SPARQL complet qui permet d’interroger les graphes en memoire.
Importation de rdflib et chargement du graphe d’animaux depuis le fichier Turtle.
try:
from rdflib import Graph, Namespace
RDFLIB_AVAILABLE = True
except ImportError:
RDFLIB_AVAILABLE = False
print("rdflib non disponible. Installez avec : pip install rdflib")
if RDFLIB_AVAILABLE:
# Load animals.ttl - a graph with RDFS class hierarchy
g = Graph()
g.parse("data/animals.ttl", format="turtle")
print(f"Triples charges depuis animals.ttl : {len(g)}")
print()
# Show namespaces
print("Namespaces utilises :")
for prefix, uri in g.namespaces():
if prefix and prefix in ['ex', 'rdfs', 'rdf']:
print(f" @prefix {prefix}: <{uri}> .")
else:
print("rdflib non disponible : chargement du graphe ignore.")Triples charges depuis animals.ttl : 51
Namespaces utilises :
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <http://example.org/animals#> .
Interpretation
Le fichier animals.ttl contient : - Une hiérarchie de classes : Animal > Mammal/Bird > Dog/Cat/Parrot - Des instances avec proprietes : name, age, sound, canFly
C’est le même fichier utilise dans SW-4 (dotNetRDF), ce qui nous permet de comparer directement les syntaxes.
2. Requête SELECT Simple
Commencons par lister tous les animaux avec leur nom et leur cri.
if RDFLIB_AVAILABLE:
# Simple SELECT query - list all animals with name and sound
query = """
PREFIX ex: <http://example.org/animals#>
SELECT ?animal ?name ?sound
WHERE {
?animal ex:name ?name .
?animal ex:sound ?sound .
}
ORDER BY ?name
"""
results = g.query(query)
print(f"Resultats : {len(results)} animaux trouves")
print(f"{'Animal':<35s} {'Nom':<12s} {'Cri':<25s}")
print("-" * 72)
for row in results:
print(f"{str(row.animal):<35s} {str(row.name):<12s} {str(row.sound):<25s}")
else:
print("rdflib non disponible : requete SELECT ignoree.")Resultats : 4 animaux trouves
Animal Nom Cri
------------------------------------------------------------------------
http://example.org/animals#buddy Buddy Woof woof
http://example.org/animals#coco Coco Coco veut un gateau
http://example.org/animals#minou Minou Miaou
http://example.org/animals#rex Rex Woof
Interpretation : Requête SELECT
La requête retourne les 4 animaux définis dans animals.ttl.
| Élément | Syntaxe SPARQL | Description |
|---|---|---|
| PREFIX | PREFIX ex: <...> |
Declare un prefixe (equivalent de @prefix en Turtle) |
| Variables | ?animal, ?name |
Variables SPARQL (commencent par ? ou $) |
| Pattern | ?animal ex:name ?name . |
Triple pattern a matcher |
| ORDER BY | ORDER BY ?name |
Tri ascendant |
rdflib vs dotNetRDF :
| Opération | rdflib | dotNetRDF |
|---|---|---|
| Executer requête | g.query("SELECT...") |
g.ExecuteQuery("SELECT...") |
| Type de retour | Result (iterable) |
SparqlResultSet |
| Acces valeur | row.name ou row[1] |
row["name"] |
| Compter résultats | len(results) |
results.Count |
3. Filtrage avec FILTER
Le mot-cle FILTER applique des conditions aux résultats.
if RDFLIB_AVAILABLE:
# FILTER: find animals older than 4 years
query_filter = """
PREFIX ex: <http://example.org/animals#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT ?name ?age
WHERE {
?animal ex:name ?name .
?animal ex:age ?age .
FILTER (?age > 4)
}
ORDER BY DESC(?age)
"""
print("=== Animaux de plus de 4 ans ===")
for row in g.query(query_filter):
print(f" {row.name} : {row.age} ans")
else:
print("rdflib non disponible : requete FILTER ignoree.")=== Animaux de plus de 4 ans ===
Coco : 12 ans
Buddy : 7 ans
Rex : 5 ans
Interpretation : FILTER
Le FILTER (?age > 4) ne retient que les animaux dont l’age depasse 4 ans.
| Opérateur | Description | Exemple |
|---|---|---|
| Comparaisons | =, !=, <, >, <=, >= |
FILTER (?age > 4) |
| Logique | &&, \|\|, ! |
FILTER (?age > 4 && ?age < 10) |
| Tests de type | isURI(), isBlank(), isLiteral() |
FILTER (isURI(?x)) |
| Regex | REGEX() |
FILTER (REGEX(?name, "^C")) |
| Existence | BOUND() |
FILTER (BOUND(?optional)) |
Equivalent dotNetRDF : Même syntaxe SPARQL, mais SparqlParameterizedString pour les requêtes parametrees.
4. Joins Optionnels avec OPTIONAL
Le mot-cle OPTIONAL inclut un pattern même s’il n’est pas satisfait (equivalent de LEFT JOIN en SQL).
if RDFLIB_AVAILABLE:
# OPTIONAL: list all animals with flight capability (if known)
query_optional = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?name ?type_label ?canFly
WHERE {
?animal ex:name ?name .
?animal a ?type .
?type rdfs:label ?type_label .
OPTIONAL { ?animal ex:canFly ?canFly . }
}
ORDER BY ?name
"""
print("=== Animaux avec capacite de vol (OPTIONAL) ===")
print(f"{'Nom':<12s} {'Type':<15s} {'Peut voler':<12s}")
print("-" * 39)
for row in g.query(query_optional):
fly_str = str(row.canFly) if row.canFly is not None else "non renseigne"
print(f"{str(row.name):<12s} {str(row.type_label):<15s} {fly_str:<12s}")
else:
print("rdflib non disponible : requete OPTIONAL ignoree.")=== Animaux avec capacite de vol (OPTIONAL) ===
Nom Type Peut voler
---------------------------------------
Buddy Chien non renseigne
Coco Perroquet true
Minou Chat non renseigne
Rex Chien non renseigne
Interpretation : OPTIONAL
Seul Coco (perroquet) a la propriete ex:canFly. Grace a OPTIONAL, les autres animaux apparaissent quand même dans les résultats.
| Mot-cle SPARQL | Equivalent SQL | Comportement |
|---|---|---|
FILTER |
WHERE condition |
Exclut les lignes ne respectant pas la condition |
OPTIONAL |
LEFT JOIN |
Inclut la ligne même si le pattern optionnel n’est pas satisfait |
ORDER BY |
ORDER BY |
Tri ascendant (defaut) ou DESC() |
Principe du monde ouvert : L’absence d’information ne signifie pas que l’information est fausse.
OPTIONALrespecte ce principe.
5. Combinaison de Patterns avec UNION
Le mot-cle UNION combine les résultats de plusieurs patterns (equivalent de UNION ALL en SQL).
if RDFLIB_AVAILABLE:
# UNION: mammals OR birds
query_union = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?name ?type_label
WHERE {
?animal ex:name ?name .
?animal a ?type
{
?type rdfs:subClassOf ex:Mammal .
ex:Mammal rdfs:label ?type_label .
} UNION {
?type rdfs:subClassOf ex:Bird .
ex:Bird rdfs:label ?type_label .
}
}
ORDER BY ?name
"""
print("=== Mammiferes OU Oiseaux ===")
for row in g.query(query_union):
print(f" {row.name} - {row.type_label}")
else:
print("rdflib non disponible : requete UNION ignoree.")=== Mammiferes OU Oiseaux ===
Buddy - Mammifere
Coco - Oiseau
Minou - Mammifere
Rex - Mammifere
Interpretation : UNION
UNION combine les résultats de deux patterns. C’est utile quand une propriete peut etre exprimee de plusieurs facons différentes.
Equivalent dotNetRDF : La syntaxe SPARQL est identique, seul le wrapping change.
6. Exploration de la Hiérarchie de Classes
Le fichier animals.ttl contient une hiérarchie RDFS. Interrogeons-la pour comprendre les relations entre classes.
if RDFLIB_AVAILABLE:
# Query class hierarchy
query_classes = """
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ex: <http://example.org/animals#>
SELECT ?class ?label ?parent ?parent_label
WHERE {
VALUES ?class { ex:Animal ex:Mammal ex:Bird ex:Dog ex:Cat ex:Parrot }
?class rdfs:label ?label .
OPTIONAL {
?class rdfs:subClassOf ?parent .
?parent rdfs:label ?parent_label .
}
}
ORDER BY ?parent_label ?label
"""
print("=== Hierarchie de classes ===")
print(f"{'Classe':<15s} {'Parent':<15s}")
print("-" * 30)
for row in g.query(query_classes):
parent = str(row.parent_label) if row.parent_label else "(racine)"
print(f"{str(row.label):<15s} {parent:<15s}")
else:
print("rdflib non disponible : requete hierarchie ignoree.")=== Hierarchie de classes ===
Classe Parent
------------------------------
Animal (racine)
Mammifere Animal
Oiseau Animal
Chat Mammifere
Chien Mammifere
Perroquet Oiseau
Interpretation : Hiérarchie de classes
La hiérarchie est visible : Animal est la racine, Mammal et Bird sont des sous-classes directes.
| Propriete RDFS | Description |
|---|---|
rdfs:subClassOf |
Hiérarchie de classes (transitive) |
rdfs:label |
Nom lisible pour une ressource |
rdf:type / a |
Relation instance-classe |
7. Pagination avec LIMIT et OFFSET
Pour les gros résultats, LIMIT et OFFSET permettent la pagination.
if RDFLIB_AVAILABLE:
# Pagination: first 2 animals, then next 2
query_page1 = """
PREFIX ex: <http://example.org/animals#>
SELECT ?name ?age
WHERE {
?animal ex:name ?name .
?animal ex:age ?age .
}
ORDER BY ?name
LIMIT 2
"""
query_page2 = """
PREFIX ex: <http://example.org/animals#>
SELECT ?name ?age
WHERE {
?animal ex:name ?name .
?animal ex:age ?age .
}
ORDER BY ?name
LIMIT 2
OFFSET 2
"""
print("=== Page 1 (LIMIT 2) ===")
for row in g.query(query_page1):
print(f" {row.name} : {row.age} ans")
print()
print("=== Page 2 (LIMIT 2 OFFSET 2) ===")
for row in g.query(query_page2):
print(f" {row.name} : {row.age} ans")
else:
print("rdflib non disponible : pagination ignoree.")=== Page 1 (LIMIT 2) ===
Buddy : 7 ans
Coco : 12 ans
=== Page 2 (LIMIT 2 OFFSET 2) ===
Minou : 3 ans
Rex : 5 ans
Interpretation : Pagination
LIMIT 2: Retourne au maximum 2 résultatsOFFSET 2: Saute les 2 premiers résultats
C’est essentiel pour les endpoints publics qui peuvent avoir des millions de triples.
8. Tableau de Correspondance dotNetRDF / rdflib (SPARQL)
Ce tableau recapitule les equivalences pour les requêtes SPARQL.
| Opération | dotNetRDF (C#) | rdflib (Python) |
|---|---|---|
| Executer SELECT | g.ExecuteQuery("SELECT...") |
g.query("SELECT...") |
| Type de retour | SparqlResultSet |
Result (iterable) |
| Iterer résultats | foreach (SparqlResult row in results) |
for row in results: |
| Acces valeur | row["name"] |
row.name ou row[0] |
| Compter | results.Count |
len(results) |
| Requête parametree | SparqlParameterizedString |
f-string ou format Python |
| Endpoint distant | SparqlRemoteEndpoint |
SPARQLWrapper (voir SW-5b) |
Syntaxe SPARQL - Identique dans les deux bibliotheques
La syntaxe SPARQL elle-même est identique - seuls changent les wrappers d’exécution :
PREFIX ex: <http://example.org/>
SELECT ?s ?p ?o
WHERE {
?s ?p ?o .
FILTER (?o > 10)
OPTIONAL { ?s ex:optional ?opt . }
}
ORDER BY ?s
LIMIT 10
Exemples guides (issus de contributions etudiantes)
Les exercices ci-dessous ont ete resolus et valides a partir de contributions etudiantes ; ils sont conserves comme exemples guides de mise en pratique des concepts SPARQL. De nouveaux exercices a completer sont proposes a la fin du notebook.
Exemple guide 1 : Trouver les mammiferes
Cette requête retourne tous les animaux qui sont des mammiferes (instances de ex:Mammal ou de ses sous-classes ex:Dog, ex:Cat), avec leur nom et leur age.
Approche : rdfs:subClassOf* (chemin de propriete transitif) remonte automatiquement la hiérarchie des classes, sans avoir a enumerer chaque sous-classe.
if RDFLIB_AVAILABLE:
# Exemple guide 1 : trouver tous les mammiferes dans animals.ttl
query_mammals = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?name ?age ?type_label
WHERE {
# Type de l'animal = sous-classe (transitive) de ex:Mammal via rdfs:subClassOf*
?animal a ?type .
?type rdfs:subClassOf* ex:Mammal .
?type rdfs:label ?type_label .
?animal ex:name ?name .
?animal ex:age ?age .
}
ORDER BY ?name
"""
for row in g.query(query_mammals):
print(f"{row.name} ({row.type_label}) : {row.age} ans")
else:
print("rdflib non disponible : exemple guide 1 ignore.")Buddy (Chien) : 7 ans
Minou (Chat) : 3 ans
Rex (Chien) : 5 ans
Lecture : rdfs:subClassOf* — le chemin transitif qui remonte l’arbre
La requête ne demande PAS « les animaux typés Mammal » : elle demande les animaux dont le type satisfait rdfs:subClassOf* ex:Mammal — zéro, un ou plusieurs sauts de sous-classe au-dessus de Mammal. La sortie montre ce que ça change : Buddy (Chien) : 7 ans, Minou (Chat) : 3 ans, Rex (Chien) : 5 ans, dans l’ordre alphabétique demandé par le ORDER BY ?name. Aucun animal n’est typé ex:Mammal directement dans le fichier : Buddy est typé ex:Dog, et le property path remonte Dog vers Mammal (un saut) pour satisfaire la contrainte. Deux lectures fines. D’abord, la variable ?type reste liée au type CONCRET déclaré sur l’instance — le label rendu est Chien, pas « Mammifère » : le chemin ne fait que VÉRIFIER l’ancêtre, il ne remplace pas le type. Ensuite, l’étoile (* = zéro ou plus sauts) est le réflexe « est-un » complet : elle accepterait aussi une instance typée ex:Mammal directement (zéro saut), ce que la forme sans étoile exclurait. Et Coco le perroquet brille par son absence : ex:Bird descend d’Animal, jamais de ex:Mammal — aucun chemin ne mène à l’ancêtre exigé, la ligne disparaît. C’est la mécanique exacte de l’héritage de classe interrogé par motif, sans une ligne de code impératif.
Exemple guide 2 : Animaux sans age défini
Cette requête trouve tous les animaux qui n’ont PAS de propriete ex:age définie.
Approche : FILTER NOT EXISTS { ... } elimine les solutions pour lesquelles le motif existe. Le code gere aussi le cas ou tous les animaux ont un age (résultat vide) avec un message explicite.
if RDFLIB_AVAILABLE:
# Exemple guide 2 : animaux sans age defini
query_no_age = """
PREFIX ex: <http://example.org/animals#>
SELECT ?name
WHERE {
# On garde les animaux pour lesquels aucun triplet ex:age n'existe
?animal ex:name ?name .
FILTER NOT EXISTS { ?animal ex:age ?age . }
}
"""
results = list(g.query(query_no_age))
if results:
for row in results:
print(f" {row.name} n'a pas d'age defini")
else:
print("Aucun animal sans age dans le graphe (tous renseignes).")
else:
print("rdflib non disponible : exemple guide 2 ignore.")Aucun animal sans age dans le graphe (tous renseignes).
Lecture : la négation par échec — prouver une absence
Aucun animal sans age dans le graphe (tous renseignes). — cette ligne est le résultat d’un FILTER NOT EXISTS { ?animal ex:age ?age . }, et elle est plus informative qu’elle n’y paraît. SPARQL ne sait pas interroger ce qui n’existe pas : un motif ordinaire (?animal ex:age ?age) liste les liaisons présentes, mais « les animaux SANS âge » exige la négation — et le standard l’exprime par l’échec : pour chaque candidat, si le motif entre accolades trouve ne serait-ce qu’une solution, le candidat est écarté ; s’il n’en trouve aucune, il passe. La sortie vide n’est donc pas un échec de la requête mais une PREUVE : les quatre animaux du graphe (Buddy, Minou, Rex, Coco) portent tous un ex:age, l’inventaire est complet. C’est la négation par échec de la programmation logique, avec sa force — concise, applicable à n’importe quel motif, y compris complexes — et sa limite épistémologique : elle confond « absence de déclaration dans CE graphe » et « propriété fausse » ; un animal dont l’âge existerait dans une autre source resterait « sans âge » ici. Pour se convaincre du comportement, l’expérience minimale : retirer le triplet d’âge de rex dans data/animals.ttl et relancer — rex apparaît, et lui seul. La forme cousine mérite d’être connue : OPTIONAL { ?animal ex:age ?a } FILTER(!BOUND(?a)) rend le même résultat ICI, mais NOT EXISTS reste la forme canonique pour « aucun triplet ne matche » — l’autre exprime « la variable n’est pas liée », une distinction qui compte dès que le motif négatif porte plusieurs variables ou plusieurs solutions.
SPARQL Update : la seconde moitié du langage
L’objectif n°1 du notebook canonique de la série annonce SPARQL comme « le SQL du Web sémantique ». SQL a deux moitiés — interrogation (SELECT) et manipulation (INSERT, DELETE). Tout ce qui précède couvrait la première ; cette section couvre la seconde : SPARQL 1.1 Update, Recommandation W3C publiée le même jour que SPARQL 1.1 Query (2013).
Jusqu’ici, le graphe g a été muté par l’API Python (graph.add(...), graph.remove(...)) — propre à rdflib. L’équivalent côté C# (graph.Assert(...)) est propre à dotNetRDF. SPARQL Update est la forme déclarative commune :
| Mutation | API rdflib (locale) | SPARQL Update (standard) |
|---|---|---|
| Ajouter des triplets connus | g.add((s, p, o)) |
INSERT DATA { s p o } |
| Supprimer des triplets exacts | g.remove((s, p, o)) |
DELETE DATA { s p o } |
| Supprimer par motif | boucle sur g.triples(...) |
DELETE WHERE { motif } |
| Dériver de nouveaux triplets | boucle + add (point fixe) |
INSERT { } WHERE { } |
La différence décisive n’est pas la concision : la colonne de droite traverse une frontière réseau. Le même texte INSERT ... s’exécute en mémoire dans rdflib, ou part vers l’endpoint /update d’un serveur Fuseki, Virtuoso ou GraphDB — les triplestores nommés par la série comme destination naturelle des données à l’échelle. La colonne de gauche ne quitte jamais le processus Python.
if RDFLIB_AVAILABLE:
# Copie fraiche : la section Update travaille sur g_upd, pas sur g
# (les cellules suivantes doivent retrouver le graphe d'origine intact).
g_upd = Graph()
g_upd.parse("data/animals.ttl", format="turtle")
print(f"Triplets avant : {len(g_upd)}")
g_upd.update("""
PREFIX ex: <http://example.org/animals#>
INSERT DATA {
ex:filou a ex:Cat ;
ex:name "Filou" ;
ex:age 2 ;
ex:sound "Miaou" .
}
""")
print(f"Triplets apres INSERT DATA : {len(g_upd)}")
# Le triplet insere est immediatement interrogeable -- par la moitie Query du langage.
noms = list(g_upd.query("""
PREFIX ex: <http://example.org/animals#>
SELECT ?nom WHERE { ex:filou ex:name ?nom }
"""))
print("SELECT sur le triplet insere :", [str(r[0]) for r in noms])
else:
print("rdflib non disponible : INSERT DATA ignore.")Triplets avant : 51
Triplets apres INSERT DATA : 55
SELECT sur le triplet insere : ['Filou']
Interprétation : INSERT DATA
Quatre triplets déclarés, quatre triplets ajoutés — et surtout : ils sont immédiatement interrogeables par SELECT, sans réimporter ni reconstruire le graphe. Les deux moitiés du langage se répondent au sein de la même ressource.
DELETE DATA et DELETE WHERE : l’exact et le motif
DELETE DATA retire des triplets exacts — c’est le symétrique de INSERT DATA. DELETE WHERE retire tout ce qui matche un motif : la clause WHERE est une requête, et les triplets de chaque solution trouvée sont supprimés. C’est la puissance de la forme déclarative : une seule instruction supprime une famille entière de triplets, sans écrire la boucle.
if RDFLIB_AVAILABLE:
# DELETE DATA : suppression exacte des triplets insere ci-dessus.
g_upd.update("""
PREFIX ex: <http://example.org/animals#>
DELETE DATA {
ex:filou a ex:Cat ;
ex:name "Filou" ;
ex:age 2 ;
ex:sound "Miaou" .
}
""")
print(f"Apres DELETE DATA : {len(g_upd)} triplets (retour a l'etat d'origine)")
# DELETE WHERE : suppression par motif -- ici, tous les ages.
g_upd.update("""
PREFIX ex: <http://example.org/animals#>
DELETE WHERE { ?animal ex:age ?age . }
""")
print(f"Apres DELETE WHERE : {len(g_upd)} triplets")
restants = list(g_upd.query(
"PREFIX ex: <http://example.org/animals#> SELECT ?a WHERE { ?a ex:age ?v }"))
print(f"Triplets d'age restants : {len(restants)}")
else:
print("rdflib non disponible : DELETE ignore.")Apres DELETE DATA : 51 triplets (retour a l'etat d'origine)
Apres DELETE WHERE : 47 triplets
Triplets d'age restants : 0
Interprétation : DELETE WHERE
DELETE WHERE a retiré les 4 triplets d’âge en une instruction — le motif a joué le rôle de la boucle. C’est la suppression par requête : même grammaire que SELECT, effet de bord de suppression.
La même instruction, côté serveur
Tout ce qui précède s’exécute en mémoire. La même syntaxe s’applique à un triplestore distant — c’est ce qui fait d’Update la compétence transférable de la série. La forme rdflib vers un serveur Fuseki (non exécutée ici : aucune cellule de ce notebook ne dépend d’un serveur joignable) :
from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore
store = SPARQLUpdateStore(
queryEndpoint="http://localhost:3030/dataset/sparql",
update_endpoint="http://localhost:3030/dataset/update", # endpoint d'ecriture
)
g_distant = Graph(store)
g_distant.update("INSERT DATA { ... }") # le meme texte part vers le serveurCôté C#, le pendant de LeviathanQueryProcessor déjà utilisé par le jumeau est LeviathanUpdateProcessor (dotNetRDF). L’instruction INSERT/DELETE ne change pas : seul le processeur qui la reçoit diffère.
L’opération qui justifie Update : INSERT { } WHERE { }
Un INSERT DATA de triplets constants n’apprend rien que trois g.add() ne feraient — c’est du déplacement de syntaxe. L’opération qui discrimine est la dérivation : INSERT { template } WHERE { motif } insère le template instancié pour chaque solution du motif. Le cas d’usage central de la série en est exactement l’idiome : la matérialisation d’inférences.
Le notebook SW-6b-Python-RDFS (cellule « rdfs9 à la main ») implémente l’héritage de type par un point fixe : itérer jusqu’à ce qu’aucun nouveau triplet n’apparaisse. Reprenons la même instance — rex est un Dog, Dog ⊑ Mammal ⊑ Animal — et traitons-la deux fois : le point fixe à la main, puis une instruction avec le chemin de propriété transitif rdfs:subClassOf+ (déjà rencontré dans les requêtes de ce notebook) :
if RDFLIB_AVAILABLE:
from rdflib import URIRef
schema = [
("http://example.org/animals#Dog", "http://www.w3.org/2000/01/rdf-schema#subClassOf", "http://example.org/animals#Mammal"),
("http://example.org/animals#Mammal", "http://www.w3.org/2000/01/rdf-schema#subClassOf", "http://example.org/animals#Animal"),
("http://example.org/animals#rex", "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://example.org/animals#Dog"),
]
def charger():
gr = Graph()
for s, p, o in schema:
gr.add((URIRef(s), URIRef(p), URIRef(o)))
return gr
# --- Forme 1 : le point fixe manuel (rdfs9), comme SW-6b ---
g_rdfs9_manual = charger()
iteration = 0
while True:
iteration += 1
nouveaux = []
for x, _, c1 in list(g_rdfs9_manual.triples((None, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), None))):
for _, _, c2 in g_rdfs9_manual.triples((c1, URIRef("http://www.w3.org/2000/01/rdf-schema#subClassOf"), None)):
t = (x, URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), c2)
if t not in g_rdfs9_manual:
nouveaux.append(t)
if not nouveaux:
break
for t in nouveaux:
g_rdfs9_manual.add(t)
print(f"point fixe, iteration {iteration} : {len(nouveaux)} triplet(s) infere(s)")
print(f"Forme manuelle : {len(g_rdfs9_manual)} triplets au total")
# --- Forme 2 : une seule instruction SPARQL Update ---
g_rdfs9_update = charger()
g_rdfs9_update.update("""
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
INSERT { ?x rdf:type ?c2 }
WHERE { ?x rdf:type ?c1 . ?c1 rdfs:subClassOf+ ?c2 }
""")
print(f"Forme INSERT..WHERE : {len(g_rdfs9_update)} triplets au total")
# --- Egalite programmatique des deux resultats ---
print()
print("Les deux formes produisent le meme graphe :", set(g_rdfs9_manual) == set(g_rdfs9_update))
rex = URIRef("http://example.org/animals#rex")
types_rex = sorted(str(o).split("#")[-1] for s, p, o in g_rdfs9_update
if s == rex and "type" in str(p))
print("Types de rex apres derivation :", types_rex)
else:
print("rdflib non disponible : rdfs9 ignore.")point fixe, iteration 1 : 1 triplet(s) infere(s)
point fixe, iteration 2 : 1 triplet(s) infere(s)
Forme manuelle : 5 triplets au total
Forme INSERT..WHERE : 5 triplets au total
Les deux formes produisent le meme graphe : True
Types de rex apres derivation : ['Animal', 'Dog', 'Mammal']
Lecture : pourquoi subClassOf+ dispense du point fixe
Les deux formes produisent le même graphe — l’égalité est vérifiée programmatiquement, pas seulement affichée. La différence est structurelle : le point fixe itére parce que l’implication rdfs9 ne propage qu’un cran à la fois (Dog ⊑ Mammal, puis Mammal ⊑ Animal) ; le chemin de propriété rdfs:subClassOf+ désigne la fermeture transitive en une expression, et INSERT { } WHERE { } matérialise toutes les solutions d’un coup. C’est le prix de la forme déclarative : on décrit ce qui doit être vrai (tout type hérité), pas comment y arriver (itérer jusqu’à stabilisation).
Cette opération — persister une clôture RDFS/OWL dans le graphe — est celle que la série appelle matérialisation, et INSERT { } WHERE { } en est l’idiome standard : la même instruction écrit dans un graphe en mémoire ou dans un triplestore de production.
Resume
Ce sidetrack a presente l’exécution de requêtes SPARQL en Python avec rdflib.
Concepts cles
| Concept | Ce que vous avez appris |
|---|---|
| SELECT | Projeter des variables depuis un graphe |
| FILTER | Appliquer des conditions aux résultats |
| OPTIONAL | Jointures externes (valeurs manquantes autorisees) |
| UNION | Combiner plusieurs patterns |
| ORDER BY / LIMIT / OFFSET | Trier et paginer les résultats |
| Correspondance | Equivalences dotNetRDF / rdflib |
Prochaines étapes
- SW-5-CSharp-LinkedData : Données liees en .NET (DBpedia, Wikidata)
- SW-5b-Python-LinkedData : Interroger des endpoints distants avec SPARQLWrapper
- SW-6-CSharp-RDFS : Schema et inference en .NET
Retour au sommaire : Index SemanticWeb
Resume et perspectives
Ce sidetrack a couvert les principales fonctionnalites du langage SPARQL appliquees a rdflib : les requêtes SELECT pour projeter des variables, FILTER pour le filtrage conditionnel, OPTIONAL pour les jointures externes respectant le principe du monde ouvert, UNION pour combiner plusieurs patterns, et LIMIT/OFFSET pour la pagination. L’exemple guide de synthese a également introduit CONSTRUCT pour generer de nouveaux graphes RDF et les agregations GROUP BY/COUNT.
La comparaison systématique entre dotNetRDF et rdflib a montre que la syntaxe SPARQL est identique dans les deux bibliotheques – seuls différent les wrappers d’exécution (g.query() en Python, g.ExecuteQuery() en C#). Les exercices proposent d’explorer des fonctionnalites avancees : requêtes ASK (booleennes), BIND/CONCAT pour la construction de chaînes, les chemins de propriete transitifs (rdfs:subClassOf+), et les agregations avec HAVING.
Dans le notebook suivant (SW-5-CSharp-LinkedData), nous decouvrirons les données liees en interrogeant des endpoints publics comme DBpedia et Wikidata, puis le sidetrack SW-5b-Python-LinkedData presentera l’equivalent Python avec SPARQLWrapper.
Exemple guide de synthese : Requêtes SPARQL avancees
Combinaison de plusieurs fonctionnalites SPARQL sur le graphe animals.ttl.
Partie A - CONSTRUCT : genere un nouveau graphe RDF ne contenant que les mammiferes.
Partie B - Agregation : GROUP BY + COUNT pour compter le nombre d’animaux par type.
if RDFLIB_AVAILABLE:
# Exemple guide de synthese : requetes SPARQL avancees (CONSTRUCT + agregation)
from rdflib import Graph
# Charger le graphe data/animals.ttl
g_animals = Graph()
g_animals.parse("data/animals.ttl", format="turtle")
# Partie A : ecrire une requete CONSTRUCT filtrant les mammiferes
construct_query = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
CONSTRUCT {
?animal ex:name ?name .
?animal a ?type .
}
WHERE {
?animal a ?type .
?type rdfs:subClassOf* ex:Mammal .
?animal ex:name ?name .
}
"""
g_mammals = g_animals.query(construct_query).graph
print("=== Partie A : CONSTRUCT (mammiferes uniquement) ===")
print(g_mammals.serialize(format="turtle"))
# Partie B : ecrire une requete d'agregation GROUP BY + COUNT par type
group_query = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?type_label (COUNT(?animal) AS ?nb)
WHERE {
?animal a ?type .
?type rdfs:label ?type_label .
}
GROUP BY ?type_label
ORDER BY DESC(?nb)
"""
print("=== Partie B : Nombre d'animaux par type ===")
for row in g_animals.query(group_query):
print(f" {row.type_label} : {row.nb}")
else:
print("rdflib non disponible : synthese ignoree.")=== Partie A : CONSTRUCT (mammiferes uniquement) ===
@prefix ns1: <http://example.org/animals#> .
ns1:buddy a ns1:Dog ;
ns1:name "Buddy" .
ns1:minou a ns1:Cat ;
ns1:name "Minou" .
ns1:rex a ns1:Dog ;
ns1:name "Rex" .
=== Partie B : Nombre d'animaux par type ===
Chien : 2
Chat : 1
Perroquet : 1
Lecture : CONSTRUCT projette, GROUP BY résume — deux lectures du même graphe
La partie A extrait un sous-graphe : trois triplets exactement — ns1:buddy a ns1:Dog ; ns1:name "Buddy", et symétriquement pour minou (Cat) et rex (Dog). Deux choses se lisent dans ce fragment. D’abord la PROJECTION : le template du CONSTRUCT ne retient que les variables qu’il cite — le type et le nom ; toute autre liaison possible du WHERE disparaît du résultat. CONSTRUCT est un filtre ET une sélection de colonnes, jamais une copie du graphe source. Ensuite la syntaxe : le préfixe ns1: est AUTO-GÉNÉRÉ par rdflib — le graphe construit n’hérite d’aucune déclaration de préfixe de la requête — et le point-virgule regroupe les propriétés d’un même sujet : la Turtle de sortie réorganise canoniquement ce que la requête a assemblé. Et le graphe construit est un objet comme les autres : il peut être interrogé à son tour par SPARQL, sérialisé, fusionné — le CONSTRUCT est le pont entre le monde des requêtes et le monde des graphes, et l’exercice 6 en donnera la réciproque : détruire par un motif exactement ce qu’une construction a matérialisé. La partie B change de posture : au lieu d’extraire des triplets, elle COMPTE — Chien : 2, Chat : 1, Perroquet : 1, via COUNT(?animal) AS ?nb et GROUP BY ?type_label. Le détail qui enseigne : ni Animal, ni Mammal, ni Bird n’apparaissent — aucune instance n’est typée au niveau abstrait (rex est ex:Dog, pas ex:Mammal), exactement la mécanique du property path de l’exemple 1. Le GROUP BY compte les types DÉCLARÉS, pas l’ascendance : pour compter « les mammifères », il faudrait le chemin transitif DANS la requête d’agrégation. Ensemble, les deux parties illustrent la dualité centrale de SPARQL : le même graphe se lit en extension (CONSTRUCT — quels triplets satisfont le motif) et en mesure (GROUP BY — quelle statistique résume les solutions). Une habitude d’écriture s’en dégage : formuler d’abord la question en extension (« quels triplets ? ») pour la vérifier à l’œil sur un petit graphe, puis la reformuler en mesure (« combien par groupe ? ») — le même motif, lu deux fois, vaut un contrôle et une synthèse.
Exercices
A votre tour. Completez les exercices ci-dessous (les solutions ne sont pas fournies). Le notebook doit pouvoir s’executer de bout en bout même si les exercices ne sont pas encore completes.
Exercice 1 : Requête ASK
Ecrivez une requête SPARQL ASK qui renvoie True s’il existe au moins un animal de plus de 4 ans dans animals.ttl, False sinon.
Indice : remplacez SELECT par ASK, et utilisez FILTER(?age > 4). Le résultat booléen se lit via bool(g.query(votre_requete).askAnswer).
# Exercice 1 : requete ASK - existe-t-il un animal de plus de 4 ans ?
# TODO etudiant : ecrire une requete ASK avec FILTER(?age > 4)
# Indice : le resultat booleen se lit via bool(g.query(votre_requete).askAnswer)
print("Exercice a completer")Exercice a completer
Exercice 2 : Description textuelle avec BIND et CONCAT
Pour chaque animal, produisez une chaîne de caractères du type "Rex fait Woof" en combinant son nom (ex:name) et son cri (ex:sound).
Indice : utilisez BIND(CONCAT(?name, " fait ", ?sound) AS ?description) dans le WHERE.
# Exercice 2 : description textuelle avec BIND / CONCAT
# TODO etudiant : produire ?description = CONCAT(?name, " fait ", ?sound)
# Indice : ?animal ex:name ?name ; ex:sound ?sound . puis BIND(...)
print("Exercice a completer")Exercice a completer
Exercice 3 : Property paths transitifs (rdfs:subClassOf+)
Pour l’animal ex:rex, listez tous ses types ancestraux dans la hiérarchie de classes (ex: Dog -> Mammal -> Animal). Utilisez le chemin de propriete transitif + (au moins un saut, sans la classe elle-même).
Indice : ?type rdfs:subClassOf+ ?ancestor part du type direct de Rex et remonte la hiérarchie. Utilisez ?ancestor rdfs:label ?label pour recuperer le libelle francais.
Résultat attendu (3 types ancestraux pour Rex) : Mammifere, Animal (et Chien lui-même si on utilise * au lieu de +).
# Exercice 3 : property paths transitifs rdfs:subClassOf+
# TODO etudiant : ecrire une requete SELECT qui liste les ancetres de ex:rex
# Indice : ex:rex a ?type . ?type rdfs:subClassOf+ ?ancestor . ?ancestor rdfs:label ?label .
print("Exercice a completer")Exercice a completer
Exercice 4 : GROUP BY + HAVING (types frequents)
Listez les types d’animaux qui ont au moins 2 instances dans le graphe. Affichez le libelle du type et le nombre d’animaux.
Indice : groupez sur ?type_label, comptez avec COUNT(?animal), puis filtrez avec HAVING(?nb >= 2). Notez que HAVING agit après GROUP BY (contrairement a FILTER).
Résultat attendu : seul Chien (2 instances : Rex + Buddy).
# Exercice 4 : agregation GROUP BY + HAVING
# TODO etudiant : compter les animaux par type et garder seulement ceux >= 2
# Indice : SELECT ?type_label (COUNT(?animal) AS ?nb) ... GROUP BY ?type_label HAVING(?nb >= 2)
print("Exercice a completer")Exercice a completer
Exercice 5 : VALUES + BIND/IF (categoriser par age)
Pour chaque animal du graphe, calculez une catégorie d’age : - jeune si age < 4 - adulte si 4 <= age < 8 - senior si age >= 8
Affichez ?name, ?age, ?catégorie.
Indice : utilisez BIND avec IF(condition1, "jeune", IF(condition2, "adulte", "senior")) AS ?catégorie. Vous pouvez aussi tester VALUES (?seuil1 ?seuil2) { (4 8) } pour rendre les seuils parametrables.
# Exercice 5 : BIND + IF pour categoriser par age
# TODO etudiant : pour chaque animal, calculer ?categorie via BIND(IF(?age < 4, "jeune", IF(?age < 8, "adulte", "senior")) AS ?categorie)
# Indice : SELECT ?name ?age ?categorie WHERE { ?animal ex:name ?name ; ex:age ?age . BIND(...) }
print("Exercice a completer")Exercice a completer
Exercice 6 : Annuler la matérialisation (DELETE par motif)
Le § Update a matérialisé l’héritage de type : rex porte désormais les types Dog, Mammal et Animal, dont deux sont inférés. Écrivez l’opération Update qui retire uniquement les types inférés pour retrouver le graphe d’origine (rex ne garde que son type direct Dog).
Indices : - Indice 1 : un type est inféré s’il est atteignable par rdfs:subClassOf+ depuis un autre type que porte déjà l’individu — motif : ?x rdf:type ?c1 . ?c1 rdfs:subClassOf+ ?c2. - Indice 2 : la forme raccourcie DELETE WHERE { ... FILTER(...) } est refusée par rdflib (un FILTER n’a pas sa place dans le template de suppression). Utilisez la forme explicite DELETE { triplets a retirer } WHERE { motif + FILTER }. - Etape 1 : construire le graphe et matérialiser ; Etape 2 : écrire le DELETE ; Etape 3 : vérifier qu’il reste exactement 3 triplets et que rex ne garde que Dog.
# Exercice 6 : annuler la materialisation (retirer uniquement les types infers)
# TODO etudiant : ecrire l'operation UPDATE qui retire les rdf:type infers
# (atteignables par rdfs:subClassOf+ depuis un autre type porte par l'individu),
# en gardant le type direct -- rex doit garder uniquement rdf:type ex:Dog.
# Indice : forme explicite "DELETE { ... } WHERE { ... }" avec FILTER(?c1 != ?c2).
# Etape 1 : g_ex6 est deja materialise ci-dessous ; Etape 2 : votre update ; Etape 3 : decompte.
if RDFLIB_AVAILABLE:
from rdflib import URIRef
g_ex6 = Graph()
schema_ex6 = [
("http://example.org/animals#Dog", "http://www.w3.org/2000/01/rdf-schema#subClassOf", "http://example.org/animals#Mammal"),
("http://example.org/animals#Mammal", "http://www.w3.org/2000/01/rdf-schema#subClassOf", "http://example.org/animals#Animal"),
("http://example.org/animals#rex", "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://example.org/animals#Dog"),
]
for s, p, o in schema_ex6:
g_ex6.add((URIRef(s), URIRef(p), URIRef(o)))
g_ex6.update("""
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
INSERT { ?x rdf:type ?c2 }
WHERE { ?x rdf:type ?c1 . ?c1 rdfs:subClassOf+ ?c2 }
""")
# TODO etudiant : votre DELETE ici
# g_ex6.update("...")
# Test (a decommenter une fois complete) :
# rex = URIRef("http://example.org/animals#rex")
# types_rex = sorted(str(o).split("#")[-1] for s, p, o in g_ex6 if s == rex and "type" in str(p))
# print("Types restants de rex :", types_rex) # attendu : ['Dog']
# print("Retour au graphe d'origine :", len(g_ex6) == 3)
print("Exercice a completer : annuler la materialisation")Exercice a completer : annuler la materialisation
Conclusion
Ce sidetrack a presente l’exécution de requêtes SPARQL en Python avec rdflib, en couvrant les mots-cles fondamentaux du langage : SELECT, FILTER, OPTIONAL, UNION, ORDER BY, LIMIT et OFFSET. Vous avez appris a interroger des graphes RDF en memoire, a explorer les hiérarchies de classes RDFS, et a utiliser les chemins de propriete transitifs (rdfs:subClassOf*) pour des requêtes avancees.
Les points cles a retenir sont la correspondance systématique entre dotNetRDF et rdflib (syntaxe SPARQL identique, seuls les wrappers d’exécution différent), le principe du monde ouvert respecte par OPTIONAL, et les fonctionnalites avancees comme CONSTRUCT pour generer de nouveaux graphes et les agregations GROUP BY/COUNT. Ces competences preparent a l’interrogation d’endpoints publics (DBpedia, Wikidata) dans les notebooks suivants.
La section SPARQL Update a complété cette couverture par la seconde moitié du langage : INSERT DATA, DELETE DATA/DELETE WHERE et surtout INSERT { } WHERE { } — l’idiome standard de la matérialisation d’inférences, démontré sur la règle rdfs9 en concurrence avec le point fixe manuel de SW-6b-Python-RDFS.