SW-3b-Python-GraphOperations

Navigation : Index | << SW-3 C# | SW-4b Python >>

Opérations sur les graphes RDF en Python avec rdflib

Ce notebook est le twin Python du notebook SW-3-CSharp-GraphOperations (dotNetRDF). Il manipule des graphes RDF au moyen de la bibliotheque rdflib (open-source, rdflib.readthedocs.io), qui implémente le modèle de données abstrait de RDF 1.1 (Cyganiak, Wood, Lanthaler (eds.), RDF 1.1 Concepts and Abstract Syntax, Recommandation W3C, 25 fevrier 2014). Les formats de sérialisation couverts (Turtle, RDF/XML, N-Triples, Notation 3, JSON-LD, TriG, N-Quads) sont tous des sérialisations normalisées de ce modèle abstrait.

Duree estimee : 50 minutes

A la fin de ce notebook, vous saurez : 1. Lire des fichiers RDF dans différents formats (Turtle, RDF/XML, N-Triples) avec rdflib 2. Ecrire des graphes RDF vers plusieurs formats via g.serialize() 3. Fusionner des graphes RDF avec l’opérateur += et | (sémantique d’ensemble) 4. Sélectionner des triplets selon différents critères avec g.triples() et les accesseurs dedies 5. Manipuler des listes RDF (créer, lire, modifier, supprimer) avec rdflib.collection.Collection

Prerequis

Note : Ce notebook est le miroir Python de SW-3 (dotNetRDF). La comparaison API dotNetRDF vs rdflib est explicitement documentee dans la section 6.


0. Installation et Imports

rdflib est l’equivalent Python direct de dotNetRDF. Elle fournit une API pythonique pour toutes les opérations RDF : parsing, serialization, sélection, listes.

# Dependances pre-provisionnees (rdflib) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.
from rdflib import Graph, URIRef, Literal, BNode, Namespace
from rdflib.namespace import RDF, RDFS, XSD
from rdflib.collection import Collection
import io

try:
    RDFLIB_AVAILABLE = True
    import rdflib
    print(f"rdflib {rdflib.__version__} pret.")
    print()
    print("Modules charges :")
    print(f"  Graph, URIRef, Literal, BNode, Namespace  (noeuds RDF)")
    print(f"  RDF, RDFS, XSD                             (vocabulaires W3C)")
    print(f"  Collection                                (listes RDF)")
except ImportError:
    RDFLIB_AVAILABLE = False
    print("rdflib non disponible. Installez avec : pip install rdflib")
rdflib 7.6.0 pret.

Modules charges :
  Graph, URIRef, Literal, BNode, Namespace  (noeuds RDF)
  RDF, RDFS, XSD                             (vocabulaires W3C)
  Collection                                (listes RDF)

Interpretation : imports

Module rdflib Rôle Equivalent dotNetRDF
Graph Conteneur de triplets (ensemble) VDS.RDF.Graph
URIRef / BNode / Literal Types de noeuds IUriNode / IBlankNode / ILiteralNode
RDF, RDFS, XSD Namespaces W3C pre-configures UriFactory + g.NamespaceMap
Collection Listes RDF (rdf:first / rdf:rest) VDS.RDF.Extensions.GetListItems

Les noeuds rdflib sont independants du graphe (URIRef("...") est autonome), contrairement a dotNetRDF ou les noeuds sont créés via g.CreateUriNode(...) (lies au graphe).


1. Lecture de fichiers RDF

La méthode centrale de lecture est Graph.parse(), qui accepte un chemin de fichier, une URL, un fichier objet ou une chaîne. Formats supportes : turtle, xml, nt, n3, json-ld, trig, nquads. rdflib detecte automatiquement le format si l’extension du fichier est connue ou si un format= explicite est fourni.

Chaque format correspond a une Recommandation W3C distincte de la famille RDF 1.1 : Turtle (Beckett, Berners-Lee, Prud’hommeaux, 2014), N-Triples (Ackaert, Sporny, Kellogg, 2014 - la forme canonique ligne-par-triplet), Notation 3 (Berners-Lee, 2006, note non normative), RDF/XML (Beckett, 2004), JSON-LD (Sporny et al., 2014). TriG et N-Quads etendent ces formats aux datasets (graphes nommes).

# 1.1 Chargement depuis un fichier Turtle
if RDFLIB_AVAILABLE:
    # Methode A : par chemin de fichier (le plus courant)
    g = Graph()
    g.parse("data/Example.ttl", format="turtle")

    # Methode B : par flux (objet fichier en memoire)
    with open("data/Example.ttl", "rb") as f:
        data_bytes = f.read()
    h = Graph()
    h.parse(data=data_bytes, format="turtle")

    print(f"Fichier  : {len(g)} triplets")
    print(f"Flux     : {len(h)} triplets")
    print(f"Identique: {len(g) == len(h)}")
else:
    print("rdflib non disponible : lecture ignoree.")
Fichier  : 4 triplets
Flux     : 4 triplets
Identique: True

Les deux méthodes sont equivalents. parse(data=...) accepte des bytes, utile quand les données RDF proviennent d’une reponse HTTP ou d’un flux in-memory.

# 1.2 Parsing depuis une chaine : format explicite vs auto-detection
if RDFLIB_AVAILABLE:
    triple_nt = b"<http://example.org/a> <http://example.org/b> <http://example.org/c>."

    # Auto-detection : rdflib devine le format (ici nt explicite car URI complets)
    g1 = Graph()
    g1.parse(data=triple_nt, format="nt")
    print(f"Format explicite (nt)  : {len(g1)} triplet")

    # Parsing Turtle depuis une chaine
    g2 = Graph()
    g2.parse(data=b'<http://example.org/a> <http://example.org/b> <http://example.org/c> .', format="turtle")
    print(f"Format explicite (ttl) : {len(g2)} triplet")
else:
    print("rdflib non disponible : parsing chaine ignore.")
Format explicite (nt)  : 1 triplet
Format explicite (ttl) : 1 triplet

Lecture : une seule sémantique, deux syntaxes

La même ligne — un sujet, un prédicat, un objet — est parsée deux fois, une fois comme N-Triples (format="nt"), une fois comme Turtle (format="turtle"), et les deux graphes contiennent exactement 1 triplet. C’est la démonstration la plus courte du principe fondateur de RDF : le triplet est l’unité de sens, la syntaxe n’est qu’un habit. N-Triples est le sous-ensemble minimal de Turtle — une ligne par triplet, URI complètes, point final précédé d’une espace obligatoire ; Turtle ajoute préfixes, points-virgules et regroupements — mais une fois PARSÉ, tout retombe dans le même ensemble de triplets. Deux conséquences pratiques : un graphe chargé depuis .nt et le même chargé depuis .ttl sont indiscernables (même len(), même contenu), et choisir un format de sérialisation est un choix de lisibilité et de compaction — jamais de sémantique. Le format se discute à l’écriture ; à la lecture, seul l’ensemble de triplets compte.

# 1.3 Comptage : rdflib charge puis len(g)
# Note API : contrairement a dotNetRDF qui offre des "Handlers" (CountHandler, PagingHandler...)
# permettant de traiter un flux RDF SANS construire le graphe en memoire, rdflib construit
# toujours le graphe. Le comptage memoire-econome natif n'existe pas.
if RDFLIB_AVAILABLE:
    g_animals = Graph()
    g_animals.parse("data/animals.ttl", format="turtle")
    print(f"animals.ttl : {len(g_animals)} triplets (comptes apres chargement en memoire)")

    # Equivalent "flux sans graphe" : on peut iterer sur un Parser brut avec un sink,
    # mais l'API est bas-niveau et rarement utilisee en pratique.
    from rdflib.parser import Parser
    print("Note : rdflib n'expose pas de Handler haut-niveau equivalent a CountHandler.")
    print("       Pour les tres gros fichiers, voir rdflib.parser.Parser + sink custom.")
else:
    print("rdflib non disponible : comptage ignore.")
animals.ttl : 51 triplets (comptes apres chargement en memoire)
Note : rdflib n'expose pas de Handler haut-niveau equivalent a CountHandler.
       Pour les tres gros fichiers, voir rdflib.parser.Parser + sink custom.

Interpretation : Méthodes de lecture

Méthode rdflib Avantage Equivalent dotNetRDF
g.parse("file.ttl", format="turtle") Format explicite, fiable TurtleParser.Load(g, "file.ttl")
g.parse(data=bytes, format="nt") En memoire, depuis HTTP NTriplesParser.Load(g, StringReader(...))
g.parse("file.ttl") (sans format) Auto-detection par extension StringParser.Parse(g, str)
len(g) après parse() Comptage simple CountHandler (sans chargement)

Différence notable : dotNetRDF offre une API de Handlers (CountHandler, PagingHandler, FilterHandler) qui traitent les triplets en flux SANS construire le graphe en memoire - utile pour les très gros fichiers. rdflib construit systematiquement le graphe ; pour le streaming massif, il faut descendre a l’API Parser + sink bas-niveau.

Les Graph rdflib sont reutilisables et l’opération parse() peut etre appelee plusieurs fois (les triplets s’accumulent).


2. Ecriture de graphes RDF

La méthode centrale de serialization est Graph.serialize(format=...) qui retourne une chaîne. Pour ecrire directement dans un fichier, utiliser g.serialize(destination="file.ttl", format=...). Formats : turtle, nt, xml, json-ld, n3, trig, nquads.

# 2.1 Comparaison de formats de sortie
if RDFLIB_AVAILABLE:
    g = Graph()
    g.parse("data/Example.ttl", format="turtle")

    nt_out = g.serialize(format="nt")
    ttl_out = g.serialize(format="turtle")

    print("=== N-Triples ===")
    print(nt_out)
    print("=== Turtle ===")
    print(ttl_out)
else:
    print("rdflib non disponible : ecriture ignoree.")
=== N-Triples ===
_:nb41b3af932154cea84c8b1211f1ea0c5b1 <http://example.org/stuff/1.0/fullname> "Dave Beckett" .
_:nb41b3af932154cea84c8b1211f1ea0c5b1 <http://example.org/stuff/1.0/homePage> <http://purl.org/net/dajobe/> .
<http://www.w3.org/TR/rdf-syntax-grammar> <http://purl.org/dc/elements/1.1/title> "RDF/XML Syntax Specification (Revised)" .
<http://www.w3.org/TR/rdf-syntax-grammar> <http://example.org/stuff/1.0/editor> _:nb41b3af932154cea84c8b1211f1ea0c5b1 .

=== Turtle ===
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix ex: <http://example.org/stuff/1.0/> .

<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ ex:fullname "Dave Beckett" ;
            ex:homePage <http://purl.org/net/dajobe/> ] ;
    dc:title "RDF/XML Syntax Specification (Revised)" .

Interpretation : Comparaison des formats de sortie

N-Triples : format verbeux - chaque triplet occupe une ligne complete avec les URIs en entier. 4 triplets = 4 lignes. Pas de prefixe, pas de compression.

Turtle : format compact - les prefixes sont définis une fois (@prefix), les noeuds blancs sont regroupes avec ; (même sujet) et les proprietes sont imbriquees avec [...]. Le même graphe tient en moins de lignes.

Le ratio de compression est ici d’environ 2:1 (N-Triples plus long que Turtle). Sur des graphes plus grands avec beaucoup de prefixes, le gain peut atteindre 5:1 ou plus.

# 2.2 RDF/XML : deux methodes pour obtenir une chaine
if RDFLIB_AVAILABLE:
    # Methode 1 : serialize() retourne directement la chaine
    xml_str = g.serialize(format="xml")

    # Methode 2 : serialize() avec destination = fichier, puis relecture
    import tempfile, os
    tmp = os.path.join(tempfile.gettempdir(), "sw3b_example.rdf")
    g.serialize(destination=tmp, format="xml")
    with open(tmp, "r", encoding="utf-8") as f:
        xml_from_file = f.read()

    print(f"Methode 1 (chaine)     : {len(xml_str)} car.")
    print(f"Methode 2 (fichier)    : {len(xml_from_file)} car.")
    print(f"Identiques             : {xml_str == xml_from_file}")
    print(f"\n=== RDF/XML (extrait) ===\n{xml_str[:400]}...")
else:
    print("rdflib non disponible : RDF/XML ignore.")
Methode 1 (chaine)     : 630 car.
Methode 2 (fichier)    : 630 car.
Identiques             : True

=== RDF/XML (extrait) ===
<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:ex="http://example.org/stuff/1.0/"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
  <rdf:Description rdf:nodeID="nb41b3af932154cea84c8b1211f1ea0c5b1">
    <ex:fullname>Dave Beckett</ex:fullname>
    <ex:homePage rdf:resource="http://purl.org/net/dajobe/"/>
  </rdf:Description>
  <r...

Les deux méthodes sont equivalents : serialize() sans destination retourne une chaîne, avec destination= ecrit dans un fichier. dotNetRDF proposait StringWriter.Write(g, writer) vs writer.Save(g, sw) - même distinction.

# 2.3 Configuration avancee des writers
# rdflib expose moins d'options que dotNetRDF (pas de IPrettyPrintingWriter / ICompressingWriter
# equivalents). Les options disponibles : indent (int) et sort_keys (booleen) sur certains formats.
if RDFLIB_AVAILABLE:
    print("=== Turtle standard ===")
    print(g.serialize(format="turtle"))

    print("=== JSON-LD avec indentation 2 ===")
    print(g.serialize(format="json-ld", indent=2))
else:
    print("rdflib non disponible : configuration ignoree.")
=== Turtle standard ===
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix ex: <http://example.org/stuff/1.0/> .

<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ ex:fullname "Dave Beckett" ;
            ex:homePage <http://purl.org/net/dajobe/> ] ;
    dc:title "RDF/XML Syntax Specification (Revised)" .


=== JSON-LD avec indentation 2 ===
[
  {
    "@id": "http://www.w3.org/TR/rdf-syntax-grammar",
    "http://example.org/stuff/1.0/editor": [
      {
        "@id": "_:nb41b3af932154cea84c8b1211f1ea0c5b1"
      }
    ],
    "http://purl.org/dc/elements/1.1/title": [
      {
        "@value": "RDF/XML Syntax Specification (Revised)"
      }
    ]
  },
  {
    "@id": "_:nb41b3af932154cea84c8b1211f1ea0c5b1",
    "http://example.org/stuff/1.0/fullname": [
      {
        "@value": "Dave Beckett"
      }
    ],
    "http://example.org/stuff/1.0/homePage": [
      {
        "@id": "http://purl.org/net/dajobe/"
      }
    ]
  }
]

Interpretation : Writers et configuration

Format rdflib Equivalent dotNetRDF Lisibilite Taille
g.serialize(format="nt") NTriplesWriter Faible Verbose
g.serialize(format="turtle") CompressingTurtleWriter Elevee Compact
g.serialize(format="xml") RdfXmlWriter Moyenne Moyenne
g.serialize(format="json-ld") JsonLdWriter Bonne (dev web) Moyenne

Différence notable : dotNetRDF expose des interfaces de configuration riches (IPrettyPrintingWriter, ICompressingWriter avec CompressionLevel, IHighSpeedWriter). rdflib est plus minimaliste : l’argument indent (JSON-LD) et la convention interne de compression Turtle sont les seuls leviers. Pour un contrôle fin equivalent, il faut descendre vers des sous-modules (ex : rdflib.plugins.serializers.turtle).

Tous les formats rdflib sont accessibles via la méthode unique g.serialize(format=...) - API uniforme, plus simple que dotNetRDF ou chaque writer a sa propre classe.


3. Fusion de graphes

rdflib implemente la sémantique d’ensemble des graphes RDF : l’union (opérateur | ou +=) combine deux graphes sans dupliquer les triplets identiques. Les blank nodes sont isoles par graphe (pas de collision).

# 3.1 Fusion de deux graphes
if RDFLIB_AVAILABLE:
    g1 = Graph()
    g2 = Graph()
    g1.parse("data/Example.ttl", format="turtle")
    g2.parse("data/animals.ttl", format="turtle")
    somme = len(g1) + len(g2)

    print(f"Graphe 1 (Example.ttl) : {len(g1)} triplets")
    print(f"Graphe 2 (animals.ttl) : {len(g2)} triplets")

    # Operateur += : union en place (equivalent de g1.Merge(g2) en dotNetRDF)
    g1 += g2
    print(f"Apres fusion (+=)      : {len(g1)} triplets (somme theorique: {somme})")
    print(f"Doublons supprimes     : {somme - len(g1)}")

    # Variante : operateur | retourne un NOUVEAU graphe (non destructif)
    g_a = Graph(); g_a.parse("data/Example.ttl", format="turtle")
    g_b = Graph(); g_b.parse("data/animals.ttl", format="turtle")
    g_union = g_a | g_b
    print(f"\nOperateur | (new graph): {len(g_union)} triplets (sources intactes: {len(g_a)}, {len(g_b)})")
else:
    print("rdflib non disponible : fusion ignoree.")
Graphe 1 (Example.ttl) : 4 triplets
Graphe 2 (animals.ttl) : 51 triplets
Apres fusion (+=)      : 55 triplets (somme theorique: 55)
Doublons supprimes     : 0

Operateur | (new graph): 55 triplets (sources intactes: 4, 51)

Interpretation : verification manuelle

Somme théorique : 4 (Example.ttl) + 51 (animals.ttl) = 55. Aucun triplet commun (les deux fichiers utilisent des namespaces disjoints : http://example.org/stuff/1.0/ vs http://example.org/animals#), donc 0 doublon supprime. Le résultat 55 confirme la sémantique d’ensemble.

Aspect rdflib dotNetRDF
Union en place g1 += g2 g1.Merge(g2)
Union non destructif g1 \| g2 (nouveau graphe) g1.Merge(g2) copie explicite requise
Dédoublonnage Automatique (set semantics) Automatique
Blank nodes Isolés par graphe Renommés pour éviter collision
  • Les triplets identiques ne sont pas dupliques (sémantique d’ensemble RDF)
  • Les blank nodes sont isoles par graphe d’origine (pas de collision)
  • += modifie g1 en place ; | retourne un nouveau graphe (API plus pythonique que Merge)

4. Sélection de triplets

Graph propose la méthode generique g.triples((s, p, o)) ou chaque composante peut etre None (joker). Des accesseurs dediees existent : g.subjects(p, o), g.predicates(s, o), g.objects(s, p), g.subject_instances, g.value() (singleton). Les résultats sont des générateurs composables avec les comprehensions Python (equivalent LINQ).

# 4.1 Selection par predicat et par sujet
if RDFLIB_AVAILABLE:
    g = Graph()
    g.parse("data/animals.ttl", format="turtle")
    EX = Namespace("http://example.org/animals#")

    # Selection par predicat : tous les triplets rdf:type
    print("=== Triplets rdf:type ===")
    type_count = 0
    for s, p, o in g.triples((None, RDF.type, None)):
        print(f"  {s.n3(g.namespace_manager)} -> {o.n3(g.namespace_manager)}")
        type_count += 1
    print(f"  ({type_count} triplets rdf:type)")

    # Selection par sujet : toutes les proprietes de ex:rex
    rex = URIRef("http://example.org/animals#rex")
    print("\n=== Proprietes de ex:rex ===")
    for s, p, o in g.triples((rex, None, None)):
        print(f"  {p.n3(g.namespace_manager)} -> {o.n3(g.namespace_manager)}")
else:
    print("rdflib non disponible : selection ignoree.")
=== Triplets rdf:type ===
  ex:Animal -> rdfs:Class
  ex:Mammal -> rdfs:Class
  ex:Bird -> rdfs:Class
  ex:Dog -> rdfs:Class
  ex:Cat -> rdfs:Class
  ex:Parrot -> rdfs:Class
  ex:name -> rdf:Property
  ex:age -> rdf:Property
  ex:sound -> rdf:Property
  ex:canFly -> rdf:Property
  ex:rex -> ex:Dog
  ex:buddy -> ex:Dog
  ex:minou -> ex:Cat
  ex:coco -> ex:Parrot
  (14 triplets rdf:type)

=== Proprietes de ex:rex ===
  rdf:type -> ex:Dog
  ex:name -> "Rex"
  ex:age -> "5"^^xsd:integer
  ex:sound -> "Woof"

Interpretation : Résultats de la sélection

Sélection par predicat rdf:type : 14 triplets trouves - 6 declarations de classes (Animal, Mammal, Bird, Dog, Cat, Parrot), 4 declarations de proprietes (name, age, sound, canFly) et 4 instances (rex/Dog, minou/Cat, coco/Parrot, buddy/Dog). Verification manuelle sur animals.ttl : 6 lignes a rdfs:Class + 4 lignes a rdf:Property + 4 lignes a ex:<Classe> = 14. Confirme.

Sélection par sujet ex:rex : 4 proprietes - type (Dog), name (Rex), age (5), sound (Woof). Rex est un chien de 5 ans qui fait “Woof”.

Ces résultats illustrent que g.triples((None, RDF.type, None)) retourne tous les usages d’un predicat (types, instances), tandis que g.triples((rex, None, None)) decrit un noeud complet.

# 4.2 Selection combinee et comprehension Python (equivalent LINQ)
if RDFLIB_AVAILABLE:
    EX = Namespace("http://example.org/animals#")
    name_prop = EX.name
    print("=== Noms des animaux ===")
    for s, p, o in g.triples((None, name_prop, None)):
        print(f"  {s.n3(g.namespace_manager)} -> {o}")

    # Comprehension Python : animaux de plus de 5 ans (equivalent du .Where(...) LINQ)
    age_prop = EX.age
    older = [(s, int(o)) for s, p, o in g.triples((None, age_prop, None)) if int(o) > 5]
    print("\n=== Animaux > 5 ans (comprehension) ===")
    for s, age in older:
        print(f"  {s.n3(g.namespace_manager)} : {age}")

    # Animal le plus age (equivalent du .Max(...) LINQ)
    all_ages = [(s, int(o)) for s, p, o in g.triples((None, age_prop, None))]
    oldest = max(all_ages, key=lambda x: x[1])
    print(f"\n=== Animal le plus age ===")
    print(f"  {oldest[0].n3(g.namespace_manager)} : {oldest[1]} ans")
else:
    print("rdflib non disponible : selection combinee ignoree.")
=== Noms des animaux ===
  ex:rex -> Rex
  ex:minou -> Minou
  ex:coco -> Coco
  ex:buddy -> Buddy

=== Animaux > 5 ans (comprehension) ===
  ex:coco : 12
  ex:buddy : 7

=== Animal le plus age ===
  ex:coco : 12 ans

Interpretation : Méthodes de sélection

Méthode rdflib Pattern Equivalent dotNetRDF Equivalent SPARQL
g.triples((s, None, None)) s ? ? GetTriplesWithSubject(s) SELECT ?p ?o WHERE { s ?p ?o }
g.triples((None, p, None)) ? p ? GetTriplesWithPredicate(p) SELECT ?s ?o WHERE { ?s p ?o }
g.triples((None, None, o)) ? ? o GetTriplesWithObject(o) SELECT ?s ?p WHERE { ?s ?p o }
g.triples((s, p, None)) s p ? GetTriplesWithSubjectPredicate(s,p) SELECT ?o WHERE { s p ?o }
g.triples((None, p, o)) ? p o GetTriplesWithPredicateObject(p,o) SELECT ?s WHERE { ?s p o }
g.value(s, p) singleton (via GetTriplesWithSubjectPredicate) SELECT ?o WHERE { s p ?o } LIMIT 1

Verification manuelle : la sélection rdf:type renvoie 14 triplets (cf. cellule précédente), et le filtre “age > 5” doit retourner coco (12) et buddy (7) - ce que confirme la sortie. L’animal le plus age est coco a 12 ans. Les résultats sont des générateurs (lazy evaluation), composables avec les comprehensions Python exactement comme IEnumerable<Triple> se compose avec LINQ.


5. Listes RDF

Les listes RDF (RDF collections) sont des structures chainees encodees avec rdf:first et rdf:rest, terminant par rdf:nil. rdflib fournit la classe rdflib.collection.Collection qui encapsule cette structure avec une API de liste Python standard (append, index, len, itération, del).

# 5.1 Creer et lire une liste RDF
if RDFLIB_AVAILABLE:
    g = Graph()
    rdf_data = """
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ex: <http://example.org/ns#> .
ex:subj ex:pred _:b1 .
_:b1 rdf:first 1 . _:b1 rdf:rest _:b2 .
_:b2 rdf:first 2 . _:b2 rdf:rest _:b3 .
_:b3 rdf:first 3 . _:b3 rdf:rest rdf:nil .
"""
    g.parse(data=rdf_data, format="turtle")

    # Le predicat ex:pred pointe vers la tete de liste
    predicate = URIRef("http://example.org/ns#pred")
    head = list(g.objects(subject=URIRef("http://example.org/ns#subj"), predicate=predicate))[0]

    # Collection encapsule l'acces haut-niveau
    col = Collection(g, head)
    print(f"Items (Collection) : [{', '.join(str(i) for i in col)}]")
    print(f"Longueur           : {len(col)}")

    # Noeuds intermediaires = cells de la liste chainee
    list_nodes = []
    current = head
    while current != RDF.nil:
        list_nodes.append(current)
        rest = list(g.objects(current, RDF.rest))[0]
        current = rest
    print(f"Noeuds intermediaires : {len(list_nodes)}")

    # Comptage des triplets rdf:first + rdf:rest
    first_triples = list(g.triples((None, RDF.first, None)))
    rest_triples = list(g.triples((None, RDF.rest, None)))
    print(f"rdf:first count    : {len(first_triples)}")
    print(f"rdf:rest count     : {len(rest_triples)}")
    print(f"Total liste        : {len(first_triples) + len(rest_triples)} triplets")
else:
    print("rdflib non disponible : listes RDF ignorees.")
Items (Collection) : [1, 2, 3]
Longueur           : 3
Noeuds intermediaires : 3
rdf:first count    : 3
rdf:rest count     : 3
Total liste        : 6 triplets

Interpretation : Structure interne

head -> [1 | rest] -> [2 | rest] -> [3 | rest] -> rdf:nil
Élément Méthode rdflib Exemple
Valeurs (rdf:first) list(Collection(g, head)) [1, 2, 3]
Noeuds intermediaires parcours manuel via rdf:rest _:b1, _:b2, _:b3
Tous les triplets g.triples((None, RDF.first, None)) + rdf:rest 6 triplets (3 first + 3 rest)

Verification manuelle : une liste de 3 éléments produit 3 rdf:first + 3 rdf:rest (le dernier rdf:rest pointe vers rdf:nil) = 6 triplets. Confirme par la sortie. Chaque élément coute 2 triplets (rdf:first + rdf:rest), sauf le dernier dont le rdf:rest pointe vers rdf:nil.

# 5.2 Ajout et suppression d'elements : Collection.append + del col[index]
if RDFLIB_AVAILABLE:
    # Repartons d'une liste fraiche [1, 2, 3]
    g2 = Graph()
    head2 = BNode()
    Collection(g2, head2, [Literal(1), Literal(2), Literal(3)])
    col2 = Collection(g2, head2)
    def fmt(c): return '[' + ', '.join(str(x) for x in c) + ']'

    print(f"Avant           : {fmt(col2)}")

    # append = AddToList equivalent (ajoute en fin de liste)
    col2.append(Literal(4))
    col2.append(Literal(5))
    print(f"Apres append    : {fmt(col2)}")

    # suppression par valeur : trouver l'index puis del (rdflib n'expose pas .remove(value))
    target = Literal(3)
    items = list(col2)
    if target in items:
        del col2[items.index(target)]
    print(f"Apres del '3'   : {fmt(col2)}")
else:
    print("rdflib non disponible : ajout/suppression ignores.")
Avant           : [1, 2, 3]
Apres append    : [1, 2, 3, 4, 5]
Apres del '3'   : [1, 2, 4, 5]
# 5.3 Cycle complet : creation, ajout, suppression, destruction
if RDFLIB_AVAILABLE:
    g3 = Graph()
    head3 = BNode()
    # AssertList equivalent : creation depuis une liste de valeurs
    Collection(g3, head3, [Literal("Alice"), Literal("Bob"), Literal("Charlie")])
    col3 = Collection(g3, head3)
    def fmt(c): return '[' + ', '.join(str(x) for x in c) + ']'

    print(f"Initial           : {fmt(col3)}")

    # Ajout
    col3.append(Literal("Diana"))
    print(f"Apres ajout Diana : {fmt(col3)}")

    # Suppression par valeur (index lookup)
    items = list(col3)
    del col3[items.index(Literal("Bob"))]
    print(f"Apres del Bob     : {fmt(col3)}")

    # RetractList equivalent : clear() detruit tous les triplets de la liste
    avant = len(g3)
    col3.clear()
    print(f"Apres clear()     : {fmt(col3)} | triplets {avant} -> {len(g3)}")
else:
    print("rdflib non disponible : cycle complet ignore.")
Initial           : [Alice, Bob, Charlie]
Apres ajout Diana : [Alice, Bob, Charlie, Diana]
Apres del Bob     : [Alice, Charlie, Diana]
Apres clear()     : [] | triplets 6 -> 0

Interpretation : API des listes

Opération Méthode rdflib Comportement Équivalent dotNetRDF
Créer Collection(g, head, [items]) Génère blank nodes + triplets rdf:first/rdf:rest g.AssertList(éléments)
Lire list(Collection(g, head)) Itère les valeurs dans l’ordre g.GetListItems(root)
Ajouter col.append(item) Ajoute en fin de liste (gère le chaînage rdf:rest) g.AddToList(root, éléments)
Retirer par valeur del col[col.index(item)] Recherche d’index manuelle puis suppression g.RemoveFromList(root, éléments)
Supprimer tout col.clear() Supprime récursivement tous les triplets g.RetractList(root)

Différence notable : RemoveFromList (dotNetRDF) retire par valeur directement ; rdflib exige de trouver l’index puis del col[index] — visible dans les sorties 5.2 (« Apres del ‘3’ ») et 5.3 (« Apres del Bob »). API légèrement moins ergonomique pour ce cas, mais conforme à l’idiome Python del list[i].

Quand utiliser les listes RDF ? : Collections ordonnées (séquences, étapes). Pour des ensembles non ordonnés, préférez des triplets individuels — la liste RDF est coûteuse (2 triplets par élément) et peu query-friendly (pas de SELECT ?x WHERE { list ?p ?x } direct).


6. Comparaison cross-langage dotNetRDF vs rdflib

Aspect dotNetRDF (C#) rdflib (Python)
Style API OOP verbeux, typage fort, interfaces multiples Pythonique, concis, tuples natifs
Noeuds Lies au graphe (g.CreateUriNode(...)) Independants (URIRef(...))
Triple Objet new Triple(s, p, o) Tuple Python (s, p, o)
Lecture Classes par format (TurtleParser, NTriplesParser) + Handlers API unique g.parse(format="...")
Ecriture Classes par format + interfaces (IPrettyPrintingWriter, ICompressingWriter) API unique g.serialize(format="...") (options limitees)
Fusion g1.Merge(g2) (en place) g1 += g2 (en place) ou g1 \| g2 (nouveau)
Sélection GetTriplesWithXxx(...) (5 méthodes) g.triples((s, p, o)) (une méthode, None = joker)
Listes RDF Extensions.GetListItems, AssertList, AddToList, RetractList rdflib.collection.Collection (API liste Python)
Streaming massif Handlers (CountHandler, PagingHandler) - pas de graphe en memoire Construit systematiquement le graphe (gap)
LINQ / comprehensions IEnumerable<Triple> + LINQ (.Where, .Select, .Max) Générateurs + comprehensions Python ([x for x in ...])

Différences philosophiques

  • dotNetRDF privilegie l’explicite : un type distinct par parser/writer, interfaces de configuration fines, handlers pour le streaming. Avantage : contrôle précis, typage compile. Inconvenient : verbosite.
  • rdflib privilegie l’uniformite : une seule méthode parse / serialize avec un argument format, tuples Python natifs pour les triples. Avantage : concision, courbe d’apprentissage faible. Inconvenient : moins d’options de configuration, pas de streaming memoire-econome natif.

A retenir : Les deux bibliotheques implementent fidelement les mêmes standards W3C (RDF 1.1, Turtle, N-Triples, RDF/XML, JSON-LD). Les fichiers produits par l’un sont consommables par l’autre. Le choix depend de l’ecosysteme : .NET pour l’integration C# typée, Python pour la data science et l’IA.


7. Exercices pratiques

Mettez en pratique les concepts appris. Chaque Exemple guide est une solution complete commentee ; chaque Exercice est un squelette a completer.

Exemple guide 1 : Charger et explorer

Chargez data/animals.ttl et affichez : le nombre total de triplets, les triplets rdf:type, et les proprietes de ex:rex.

# Exemple guide 1 : Charger animals.ttl et explorer
if RDFLIB_AVAILABLE:
    g = Graph()
    g.parse("data/animals.ttl", format="turtle")
    print(f"Total triplets : {len(g)}")

    # Selection de tous les triplets dont le predicat est rdf:type
    # rdf:type indique la classe d'appartenance d'une ressource (ex : rex est de type Dog)
    print("Triplets rdf.type :")
    for s, p, o in g.triples((None, RDF.type, None)):
        print(f"  {s.n3(g.namespace_manager)} -> {o.n3(g.namespace_manager)}")

    # Toutes les proprietes de ex:rex
    rex = URIRef("http://example.org/animals#rex")
    print("Proprietes de rex :")
    for s, p, o in g.triples((rex, None, None)):
        print(f"  {p.n3(g.namespace_manager)} -> {o}")
else:
    print("rdflib non disponible : exemple 1 ignore.")
Total triplets : 51
Triplets rdf.type :
  ex:Animal -> rdfs:Class
  ex:Mammal -> rdfs:Class
  ex:Bird -> rdfs:Class
  ex:Dog -> rdfs:Class
  ex:Cat -> rdfs:Class
  ex:Parrot -> rdfs:Class
  ex:name -> rdf:Property
  ex:age -> rdf:Property
  ex:sound -> rdf:Property
  ex:canFly -> rdf:Property
  ex:rex -> ex:Dog
  ex:buddy -> ex:Dog
  ex:minou -> ex:Cat
  ex:coco -> ex:Parrot
Proprietes de rex :
  rdf:type -> http://example.org/animals#Dog
  ex:name -> Rex
  ex:age -> 5
  ex:sound -> Woof

Exercice 4 : Recherche avancee dans un graphe

Chargez data/animals.ttl et trouvez :

  1. Tous les animaux qui sont des chiens (rdf:type -> ex:Dog) en utilisant g.triples((None, RDF.type, ex:Dog))
  2. Le nombre d’animaux qui peuvent voler (predicat ex:canFly avec valeur True)
  3. L’animal le plus age (maximum de la valeur du predicat ex:age)

Indices : utilisez g.triples((None, RDF.type, URIRef("...Dog"))) pour la question 1, et une comprehension Python avec max(..., key=lambda ...) pour la question 3.

# TODO etudiant : Chargez data/animals.ttl et trouvez :
# 1. Tous les animaux qui sont des chiens (rdf:type -> ex:Dog)
# 2. Le nombre d'animaux qui peuvent voler (predicat ex:canFly avec valeur True)
# 3. L'animal le plus age (maximum de ex:age)
#
# Indice : utilisez g.triples((None, RDF.type, URIRef("http://example.org/animals#Dog"))) pour (1)
# et max(..., key=lambda x: x[1]) sur la liste des (sujet, age) pour (3)
print("Exercice a completer")
Exercice a completer

Exemple guide 2 : Fusionner et serialiser

Chargez data/Example.ttl et data/animals.ttl, fusionnez-les, et ecrivez le résultat en Turtle compact.

# Exemple guide 2 : Fusion + serialization Turtle
if RDFLIB_AVAILABLE:
    g1 = Graph()
    g2 = Graph()
    g1.parse("data/Example.ttl", format="turtle")
    g2.parse("data/animals.ttl", format="turtle")

    print(f"Avant fusion - g1: {len(g1)}, g2: {len(g2)}")
    # Union : les triplets identiques ne sont pas dupliques (semantique d'ensemble RDF)
    # Les blank nodes sont isoles par graphe (pas de collision)
    g1 += g2
    print(f"Apres fusion : {len(g1)} triplets")

    result = g1.serialize(format="turtle")
    print(result)
else:
    print("rdflib non disponible : exemple 2 ignore.")
Avant fusion - g1: 4, g2: 51
Apres fusion : 55 triplets
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix ex: <http://example.org/stuff/1.0/> .
@prefix ns1: <http://example.org/animals#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ns1:Animal a rdfs:Class ;
    rdfs:label "Animal"@fr ;
    rdfs:comment "Classe racine de tous les animaux"@fr .

ns1:Bird a rdfs:Class ;
    rdfs:label "Oiseau"@fr ;
    rdfs:subClassOf ns1:Animal .

ns1:Cat a rdfs:Class ;
    rdfs:label "Chat"@fr ;
    rdfs:subClassOf ns1:Mammal .

ns1:Dog a rdfs:Class ;
    rdfs:label "Chien"@fr ;
    rdfs:subClassOf ns1:Mammal .

ns1:Mammal a rdfs:Class ;
    rdfs:label "Mammifere"@fr ;
    rdfs:subClassOf ns1:Animal .

ns1:Parrot a rdfs:Class ;
    rdfs:label "Perroquet"@fr ;
    rdfs:subClassOf ns1:Bird .

ns1:age a rdf:Property ;
    rdfs:label "age"@fr ;
    rdfs:domain ns1:Animal ;
    rdfs:range xsd:integer .

ns1:buddy a ns1:Dog ;
    ns1:age 7 ;
    ns1:name "Buddy" ;
    ns1:sound "Woof woof" .

ns1:canFly a rdf:Property ;
    rdfs:label "peut voler"@fr ;
    rdfs:domain ns1:Animal ;
    rdfs:range xsd:boolean .

ns1:coco a ns1:Parrot ;
    ns1:age 12 ;
    ns1:canFly true ;
    ns1:name "Coco" ;
    ns1:sound "Coco veut un gateau" .

ns1:minou a ns1:Cat ;
    ns1:age 3 ;
    ns1:name "Minou" ;
    ns1:sound "Miaou" .

ns1:name a rdf:Property ;
    rdfs:label "nom"@fr ;
    rdfs:domain ns1:Animal ;
    rdfs:range xsd:string .

ns1:rex a ns1:Dog ;
    ns1:age 5 ;
    ns1:name "Rex" ;
    ns1:sound "Woof" .

ns1:sound a rdf:Property ;
    rdfs:label "cri"@fr ;
    rdfs:domain ns1:Animal ;
    rdfs:range xsd:string .

<http://www.w3.org/TR/rdf-syntax-grammar> ex:editor [ ex:fullname "Dave Beckett" ;
            ex:homePage <http://purl.org/net/dajobe/> ] ;
    dc:title "RDF/XML Syntax Specification (Revised)" .

Exercice 5 : Fusion avec données en memoire

Chargez data/animals.ttl dans un graphe et la chaîne N-Triples suivante dans un autre graphe avec g.parse(data=..., format="nt") :

<http://example.org/ns#dave> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/ns#Person> .
<http://example.org/ns#dave> <http://example.org/ns#name> "Dave" .
<http://example.org/ns#dave> <http://example.org/ns#age> "40" .

Fusionnez les deux graphes et serialisez le résultat en N-Triples (pas Turtle).

# TODO etudiant : Chargez data/animals.ttl dans un graphe g1
# Chargez la chaine N-Triples ci-dessus dans un graphe g2 avec g2.parse(data=..., format="nt")
# Fusionnez g2 dans g1 (g1 += g2) et affichez le nombre total de triplets
# Serialisez le resultat en N-Triples (pas Turtle) avec g1.serialize(format="nt")
#
# Indice : utilisez g.serialize(format="nt") pour la serialisation
print("Exercice a completer")
Exercice a completer

Exemple guide 3 : Liste RDF

Créez une liste ["Alice", "Bob", "Charlie"] avec Collection, ajoutez "Diana", supprimez "Bob", affichez le résultat.

# Exemple guide 3 : Liste RDF avec Collection
if RDFLIB_AVAILABLE:
    g = Graph()
    head = BNode()
    # Creation d'une liste RDF ordonnee [Alice, Bob, Charlie]
    # Collection genere une chaine de blank nodes relies par rdf:first et rdf:rest
    Collection(g, head, [Literal("Alice"), Literal("Bob"), Literal("Charlie")])
    col = Collection(g, head)

    # Fonction utilitaire pour afficher uniquement la valeur textuelle
    def fmt(c): return '[' + ', '.join(str(n) for n in c) + ']'

    print(f"Initial          : {fmt(col)}")

    # append insere "Diana" en fin de liste
    col.append(Literal("Diana"))
    print(f"Apres ajout      : {fmt(col)}")

    # suppression par valeur : on cherche l'index puis del col[index]
    items = list(col)
    del col[items.index(Literal("Bob"))]
    print(f"Apres suppression: {fmt(col)}")
else:
    print("rdflib non disponible : exemple 3 ignore.")
Initial          : [Alice, Bob, Charlie]
Apres ajout      : [Alice, Bob, Charlie, Diana]
Apres suppression: [Alice, Charlie, Diana]

Exercice 6 : Liste RDF personnalisee

Créez une liste RDF ["Paris", "Lyon", "Marseille"] avec Collection, ajoutez "Toulouse" avec col.append(...), supprimez "Lyon" (index lookup + del). Affichez la liste après chaque opération.

# TODO etudiant : Creez une liste RDF ["Paris", "Lyon", "Marseille"] avec Collection
# Ajoutez "Toulouse" avec col.append(Literal("Toulouse"))
# Supprimez "Lyon" avec del col[list(col).index(Literal("Lyon"))]
# Affichez la liste apres chaque operation avec list(col)
print("Exercice a completer")
Exercice a completer

References savantes

  • RDF 1.1 Concepts and Abstract Syntax - Cyganiak, Wood, Lanthaler (eds.), Recommandation W3C, 25 fevrier 2014. Modèle de données abstrait (triplets sujet-predicat-objet, IRIs, blank nodes, litteraux) dont derivent tous les formats manipules dans ce notebook.
  • RDF 1.1 Turtle - Beckett, Berners-Lee, Prud’hommeaux, Recommandation W3C, 25 fevrier 2014. Format de serialization compact lisible par l’humain, le plus utilise en pratique.
  • RDF 1.1 N-Triples - Ackaert, Sporny, Kellogg, Recommandation W3C, 25 fevrier 2014. Forme canonique ligne-par-triplet, base d’echange interoperaable.
  • Notation 3 (N3) - Berners-Lee, A Rough Guide to Notation 3, note non normative 2006. Sur-ensemble de Turtle ajoutant règles et citerables.
  • rdflib - bibliotheque open-source Python pour la manipulation de graphes RDF (rdflib.readthedocs.io), implemente les parsers/serializers uniformes g.parse() / g.serialize() utilises tout au long de ce notebook.

Resume

Section Concepts cles APIs rdflib principales Equivalent dotNetRDF
1. Lecture parse, formats, gap Handlers g.parse(format=...), len(g) TurtleParser, CountHandler
2. Ecriture serialize, multi-formats g.serialize(format=...) NTriplesWriter, CompressingTurtleWriter
3. Fusion Union, deduplication g1 += g2, g1 \| g2 IGraph.Merge()
4. Sélection triples pattern, comprehensions g.triples((s,p,o)), g.value() GetTriplesWithSubject, LINQ
5. Listes Collections ordonnees Collection(g, head, items) AssertList, GetListItems, RetractList

Prochaine étape

Dans SW-4b-Python-SPARQL, nous apprendrons a interroger les graphes RDF avec le langage SPARQL cote Python (equivalent de SW-4 C#).


Navigation : Index | << SW-3 C# | SW-4b Python >>

Retour au sommet