lucidiot's cybrecluster

Explorer l'Internet des années 2000 avec Wikidata

Lucidiot Informatique 2021-06-09
Comme d'habitude, je vais un peu trop loin pour des blagues que seules quelques personnes comprendront entièrement.


Je cherchais un peu d'inspiration pour le design d'un site volontairement vieux ; un site qui allait disposer d'un magnifique badge "conçu avec Microsoft FrontPage", et qui allait parodier les universités américaines dans les années 2000. Je voulais donc consulter les sites desdites universités américaines à l'époque, parce que je n'en avais jamais vu auparavant. Fort heureusement, la Wayback Machine est là pour ça, mais je ne connais pas beaucoup d'universités américaines ; comment en trouver ?

J'ai d'abord tenté d'utiliser la liste des universités et facultés américaines, mais la liste est en réalité une liste de listes (et pourrait donc rentrer dans la liste de listes de listes) et liste des listes d'universités par état américain. De plus, en discutant avec les amis avec lesquels j'ai eu l'idée farfelue de créer mon site, on m'a suggéré de prendre des universités fondées aux alentours des années 2000, car elles étaient un peu plus vides de sens que les autres et vendaient un peu du vent.

Devoir ouvrir une cinquantaine de listes, filtrer pour ne prendre que les universités d'une certaine date, ouvrir chaque article pour trouver son site, etc. allait me prendre un temps considérable. Il y a forcément un moyen d'automatiser ça…

Wikidata

Depuis 2012, la fondation Wikimedia essaie de rendre les données de ses wikis plus accessibles aux machines en fournissant Wikidata, une grande base de données orientée graphe qui contient des données structurées sur à peu près tout. Et on y retrouve pas mal de données utiles pour ma tâche :

Ce n'est visiblement pas sur Wikidata qu'on aura des rumeurs de pénurie de PQ. Mais comment exploiter toutes ces propriétés ? Je ne peux pas vraiment taper "Le P856 des éléments de P31 valant Q3918, de P571 aux alentours de l'an 2000, et de P30 du P17 valant Q49" dans la barre de recherche, et un humain me prendrait d'ailleurs pour un fou.

RDF et SPARQL

En fait, plus qu'une "simple" base de données graphe, Wikidata prend en charge le RDF, un standard qui est maintenant parfois appelé Linked Data pour faire moins peur (comme dans JSON-LD) et qui est au cœur du "Web 3.0". Le Web 1.0 est celui des sites statiques, le 2.0 celui des applications, et le 3.0 est le Web sémantique, une toile de connaissances lisibles par les humains et aussi les machines. RDF propose en fait d'uniformiser l'accès aux données par les machines de façon distribuée ; par exemple, si vous aviez un site Internet qui propose comme OpenStreetMap des liens vers Wikidata, il serait possible de déclencher une recherche sur OpenStreetMap qui filtre des points sur la carte par des valeurs dont seul Wikidata dispose, et la requête causerait une connexion à OpenStreetMap mais aussi à Wikidata.

Le W3C définit RDF, et fournit aussi SPARQL, à la fois un language de requête et un protocole permettant d'exécuter ces requêtes distribuées. Wikidata fournit gratuitement un service de requête SPARQL qu'on peut utiliser pour faire des requêtes n'importe où, pas seulement sur Wikidata. Mais dans notre cas, nous n'allons l'utiliser que pour Wikidata.

La syntaxe du langage de SPARQL est un peu étrange, surtout parce qu'elle est à la fois proche et éloignée de SQL. Si on est habitué à SQL, on peut facilement être confus, parce que des mots-clés comme SELECT ou ORDER BY existent mais d'autres parties de la syntaxe sont complètement différentes.

Construisons une requête

Commençons par essayer de juste trouver toutes les universités connues par Wikidata :

SELECT ?item
WHERE {
  ?item wdt:P31 wd:Q3918.
}

Et maintenant, déchiffrons tout ce charabia...

D'abord, wd et wdt sont un peu comme des espaces de noms XML ; ils donnent accès respectivement aux propriétés et aux éléments de Wikidata. Le service de requêtes de Wikidata ajoute automatiquement ces espaces de noms, mais sinon il faudrait les mettre nous-mêmes. En utilisant ces noms, on sait qu'on causera automatiquement des requêtes sur le serveur de Wikidata.

Ensuite, ce qui se trouve au sein du WHERE est appelé un trigramme ; ce sont deux opérandes et un opérateur binaire. Chaque trigramme est séparé par un point. Tous les trigrammes doivent être vrais pour qu'on sélectionne un élément. Ici, on prend tous les éléments qui sont une instance de (P31) l'élément "université" (Q3918).

Enfin, on renvoie ?item, qui est un IRI, une version compatible Unicode des URI. On connaît plus souvent les URL, mais les URI sont une version plus générique des URL qui regroue aussi les URN. Les URL servent à localiser une ressource, et les URN servent à les nommer. La version internationalisée pour tous ces acronymes est IRL et IRN. Par exemple, on pourrait nommer un livre par son ISBN : urn:isbn:0-544-27299-4. Ça ne nous dira pas où trouver ce livre, mais ça l'identifie de façon unique.

Dans le cas de SPARQL, on peut s'attendre à ce que toutes les IRI sont en fait des IRL, car on sait quel serveur nous renvoie les identifiants, donc on sait où les trouver. Wikidata en tous cas renvoie toujours des URL. C'est important de savoir qu'on a affaire avec des IRI car elles sont distinctes des chaînes de caractères classiques. Si on exécute la requête, on n'aura que des séries d'éléments commençant par Q et on ne connaîtra par exemple pas leur nom.

Pour ajouter le nom lisible par les humains de l'université, il va nous falloir introduire la notion de service.

Le service de libellés

SELECT ?item ?itemLabel
WHERE {
  ?item wdt:P31 wd:Q3918.
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}

Il faut voir un service un peu comme une fonction qui serait fournie par un serveur externe, qui n'est pas définie par SPARQL et sur laquelle on n'a pas forcément envie d'appliquer un filtre avec un trigramme. Ici, on utilise le service wikibase:label qui est fourni par Wikidata mais qui pourrait aussi bien être fourni par un tout autre serveur, et on lui donne un paramètre pour lui indiquer que notre language préféré serait l'option automatique, qui choisit le langage le plus approprié pour l'élément (par exemple utiliser le nom français pour une université française), et sinon se rabattre sur l'anglais.

Ce service causera l'apparition d'une nouvelle variable, ?itemLabel, qui correspond au libellé donné à l'élément Wikidata qu'on a désigné par notre IRI ?item. Pour toutes les IRI de Wikidata, on pourra rajouter "Label" en suffixe et le service de libellé s'appliquera tout seul, un peu comme un appel de fonction mais en moins explicite. Vous noterez aussi la bizarrerie de rajouter ce service à l'intérieur de la clause WHERE alors qu'il n'a rien à voir avec du filtrage.

Avec ça, on a maintenant deux colonnes dans notre résultat : un lien vers l'élément, et son nom. Et on se rend compte qu'on a toutes les universités du monde, alors on va filtrer un peu plus.

Filtrer par date et continent

SELECT ?item ?itemLabel ?site
WHERE {
  ?item
      wdt:P31 wd:Q3918;
      wdt:P17/wdt:P30 wd:Q49;
      wdt:P571 ?date;
      wdt:P856 ?site.
  FILTER("1995-01-01"^^xsd:dateTime <= ?date && ?date <= "2005-01-01"^^xsd:dateTime)
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}

On commence à introduire plusieurs notions un peu plus complexes. D'abord, on remarque le point-virgule apparaître ; c'est un raccourci qui permet d'appliquer plusieurs trigrammes en même temps sur ?item. Ensuite, on applique wdt:P17/wdt:P30 : P17 étant le pays et P30 le continent, on accède au continent du pays de l'élément, et on cherche Q49, l'Amérique du Nord. Et enfin, on prend la date de création de l'université, P571, et on l'assigne à ?date sans rien faire dessus pour l'instant, et on fait de même pour P856, le site officiel, mais on le sélectionne pour avoir une troisième colonne de sortie.

Mais on ajoute ensuite une nouvelle clause, FILTER. FILTER permet de filtrer "localement", c'est-à-dire sans faire appel à des fonctionalités fournies par des serveurs comme avec le service de libellé ou avec wdt. On peut appliquer des comparaisons simples sur des types basiques de SPARQL, qui se basent sur les types définis par XSD. Les dates dans Wikidata sont de type xsd:dateTime, d'où les casts explicites avec ^^ pour pouvoir comparer. Je filtre les universités pour ne prendre que celles qui ont été fondées entre 1995 et 2005 inclus.

Une discrimination ?

Avec cette requête, je me rends alors compte d'un couac : une bonne partie des universités que j'obtiens ont des noms en espagnol. Je m'intéressais surtout aux universités anglophones, d'abord parce que je comprends l'anglais et ensuite parce que ce sont celles dont mes amis américains et canadiens parlaient. Je me rends compte d'une certaine discrimination que j'ai faite accidentellement en ce qui concerne la notion de "américain" ; ça désigne les États-Unis et pas l'Amérique du Nord et du Sud, et quand bien même on l'utiliserait au sens large, ça désigne les États-Unis et le Canada dans le langage courant. Pas même le Mexique, alors qu'il fait partie de l'Amérique du Nord.

J'ai découvert que cette discrimination existe sous la forme d'un élément Wikidata, Q2017699, "l'Amérique du Nord du nord" en traduction littérale mais appelée plus correctement "Amérique septentrionale", qui inclut tout sauf le Mexique et les Caraïbes. L'existence de cet élément implique probablement une haute fréquence de cette distinction dans des travaux de recherche ou des sources courantes de Wikipédia. J'aurai pu du coup à la place filtrer par langue officielle du pays par exemple, pour ne prendre que les universités de pays anglophones, mais je n'avais pas réalisé le problème avant d'écrire cet article, du coup c'est ce filtre que j'ai utilisé.

Il n'existe pas de propriété indiquant directement qu'un pays fait partie d'une région autre qu'un continent, mais on peut utiliser un trigramme "à l'envers" :

?item wdt:P17 ?country.
wd:Q2017699 wdt:P527 ?country.

On utilise toujours la propriété de pays, mais on stocke sa valeur dans ?country. Le vrai filtre se fait avec un nouveau trigramme, où on vérifie que l'Amérique septentrionale contient (P527) le pays. Utiliser un trigramme ainsi permet de traverser des propriétés en sens inverse, ce qui est souvent utile dans un graphe orienté comme celui dessiné par les données du Web 3.0.

Fabriquer d'autres URLs

Je pourrais m'arrêter là, et mettre ces sites officiels dans la Wayback Machine pour trouver leurs versions initiales, mais c'était encore un peu trop de clics pour moi ; je devais trouver la version initiale moi-même en sélectionnant l'année, puis le jour, puis l'heure. Je voulais un moyen d'ouvrir un lien dans la Wayback Machine qui serait au plus près de la date de création. En plus, avec mon habitude de faire des requêtes SQL farfelues, je sais que je peux toujours aller plus loin dans le fait de laisser une base de données travailler à ma place et à celle d'un script classique ; on devrait bien pouvoir faire la même chose avec SPARQL non ?

Je connais pas mal de formats d'URLs pour la Wayback Machine à présent, au point que je tape tout directement dans la barre d'adresse du navigateur au lieu d'utiliser la barre de recherche du site, et je devrais probablement les documenter dans mon wiki. On peut indiquer une date totalement arbitraire dans l'URL, et le site essaiera de nous rediriger à la version la plus proche. On peut donc construire une URL avec la date exacte de création, et laisser l'archive se débrouiller ensuite. On veut donc obtenir une URL comme ceci :

https://web.archive.org/web/[année][mois][jour]000000/[url originale]

On peut récupérer des composantes de la date avec les fonctions YEAR, MONTH et DAY. Pour pouvoir les concaténer à des chaînes de caractères, il faudra les convertir explicitement, parce qu'il n'existe pas de conversions implicites dans SPARQL, avec STR. On utilisera la fonction CONCAT, comme en SQL, pour faire la concaténation. On devra aussi convertir l'URL du site, qui est une IRI et pas une chaîne de caractères. On aurait donc quelque chose comme ceci :

CONCAT(
  "https://web.archive.org/web/*",
  STR(YEAR(?date)),
  STR(MONTH(?date)),
  STR(DAY(?date)),
  "000000/",
  STR(?site)
)

Le problème, c'est qu'on risque d'avoir des chiffres manquants ; en janvier, on devrait avoir 01 et non seulement 1. SPARQL est loin d'être aussi riche en fonctions que SQL, donc pour rajouter des zéros, le plus simple est d'utiliser un IF.

CONCAT(
  "https://web.archive.org/web/*",
  STR(YEAR(?date)),
  IF(STRLEN(STR(MONTH(?date))) < 2, "0", ""),
  STR(MONTH(?date)),
  IF(STRLEN(STR(DAY(?date))) < 2, "0", ""),
  STR(DAY(?date)),
  "000000/",
  STR(?site)
)

Ça commence à faire un peu long, et répétitif. On pourra rajouter quelques variables tout à l'heure dans la requête complète. Les deux dernières choses à faire seront de reconvertir cette chaîne de caractères en une IRI, pour faire comprendre à l'interface de Wikidata que c'est un lien cliquable car je pousse la flemmardise jusqu'au bout, et d'assigner à tout ça un nom car SPARQL nous l'oblige, donc (IRI(…) AS ?url). Et on obtient la requête finale :

SELECT
  ?item
  ?itemLabel
  (IRI(CONCAT(
    "https://web.archive.org/web/",
    STR(YEAR(?date)),
    IF(STRLEN(?month) < 2, "0", ""),
    ?month,
    IF(STRLEN(?day) < 2, "0", ""),
    ?day,
    "000000*/",
    STR(?site)
  )) AS ?url)
WHERE {
  ?item
    wdt:P31 wd:Q3918;
    wdt:P17 ?country;
    wdt:P571 ?date;
    wdt:P856 ?site.
  wd:Q2017699 wdt:P527 ?country.
  FILTER("1995-01-01"^^xsd:dateTime <= ?date && ?date <= "2005-01-01"^^xsd:dateTime)
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
  BIND(STR(MONTH(?date)) AS ?month).
  BIND(STR(DAY(?date)) AS ?day).
}

Comme je venais de le dire, on a pu assigner des alias avec BIND, donc le mois et le jour de la date en tant que chaînes de caractères sont assignés à ?month et ?day. Je suis encore bien loin d'avoir les connaissances et l'expérience pour dire si ces assignations font aussi que les fonctions ne sont pas appelées deux fois, ou si les serveurs SPARQL sont déjà assez intelligents pour bien gérer ça. Une note rapide au passage : si vous faites une erreur quelque part dans la fonction CONCAT, elle renvoie une valeur vide et il n'y aura aucune erreur. Pas très pratique quand on ne sait pas vraiment ce qu'on fait…

Conclusion

Le résultat de cette aventure dans un language de requêtes plutôt mal documenté, qui n'est pas mon premier voyage dans le pays de Wikidata, est que les sites de l'époque étaient vraiment peu travaillés. Ils étaient parfois hors service pendant plusieurs années avant que les universités ne se disent peut-être que leurs étudiants potentiels pourraient plus facilement les trouver avec Internet, bien après l'explosion de celui-ci. J'ai donc produit quelque chose d'assez simple, et qui utilise des <table> pour la mise en page comme il se doit, sur https://friends.m455.casa/fuck/, et tout ça pour une bonne vieille private joke.

Vous pouvez massacrer les serveurs de Wikidata avec ma requête en cliquant ici, essayer de comprendre quelque chose à SPARQL en utilisant le tutoriel fort peu exhaustif de Wikidata ou le Wikilivre sur le sujet. Et j'ai quelques idées d'articles sur du SQL à écrire…


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !