Jean-Hugues Roy

Expériences | réflexions | scrapbook

Les recettes du prof: comment fabriquer un robot

Tout a commencé par un clavardage avec Naël Shiab, un de mes anciens étudiants aujourd’hui journaliste avec Radio-Canada, à Sudbury.

Messenger-NShiab-1

Ça, c’est BCCourtBot, un robot qui envoie un tweet dès qu’une nouvelle décision des tribunaux de Colombie-britannique est rendue publique. Le robot a été programmé par Chad Skelton, journaliste au Vancouver Sun. Naël disait que Chad avait rendu public son script, codé en python. En voyant cela, je me suis dit qu’il y avait certainement moyen de faire la même chose en ruby.

Messenger-NShiab-2

« Ruby vs Python », a répliqué Naël. « Le choc des titans »!

Un sympathique défi venait de démarrer entre Naël et moi. Le premier qui écrivait un robot faisant la même chose que le BCCourtBot, Naël pour les tribunaux ontariens, moi pour les tribunaux québécois; Naël en python, moi en ruby.

Je vais vous vendre le punch tout de suite: Naël a gagné avec son Ontario Court Bot. Il a même eu le temps de faire un 2e robot qui tweete les décisions des tribunaux fédéraux, CanadaCourtBot.

Mon robot, que j’ai baptisé RoboTribunauxQC, est venu plus tard. J’en suis malgré tout assez content. En voici la genèse.


PREMIÈRE ÉTAPE: CHOISIR UNE SOURCE

Il existe deux sites qui diffusent les décisions des tribunaux québécois:

celui de la Société québécoise d’information juridique (SOQUIJ);

celui de l’Institut canadien d’information judirique (connu sous son acronyme anglais, CanLII).

Le premier diffuse les décisions plus rapidement, mais il est difficile à scraper et ses politiques d’utilisations ne sont pas sympathiques aux robots…

Le deuxième a le grand avantage d’offrir des fils RSS pour chaque tribunal (celui de la Cour d’appel, par exemple), ce qui est facile à scraper et n’exclut pas les robots puisque par définition, un fil RSS est consulté régulièrement pour voir ce qu’il propose de nouveau.

L'icône, à droite, est celle qui représente un fil RSS.

L’icône, en bleu à droite, est celle qui représente un fil RSS.

J’ai donc choisi de construire mon robot à partir des fils RSS de CanLII. Il y en a 78 en tout: de la Cour d’appel au Conseil de presse du Québec, en passant par les cours municipales, les Conseils de discipline de plusieurs ordres professionnels et différents tribunaux administratifs.

J’en ai pris d’abord une vingtaine (pour ensuite réduire ce nombre à 17) dont les décisions sont souvent la source d’histoires dans les médias, car c’est d’abord et avant tout à des journalistes que le robot s’adresse.

Le robot devra consulter le fil RSS de chacun de ces tribunaux, l’un après l’autre. Une façon de faire cela est d’utiliser une structure qu’on appelle en ruby un hash. Il s’agit d’une variable qui contient une liste de paires: des clés et des valeurs.

J’ai donc créé la variable rss dans laquelle j’ai mis 17 paires. Dans chacune de ces paires, la première partie, la «clé», est le nom du tribunal (en fait, le nom tel qu’il sera tweeté) et la seconde, la «valeur», est l’adresse du fil RSS.

rss = {
     "de la Cour d'appel" => "http://www.canlii.org/fr/qc/qcca/rss_new.xml",
     "de la Cour supérieure" => "http://www.canlii.org/fr/qc/qccs/rss_new.xml",
     "de la Cour du Québec" => "http://www.canlii.org/fr/qc/qccq/rss_new.xml",
     "du Trib. des droits de la pers." => "http://www.canlii.org/fr/qc/qctdp/rss_new.xml",
     "du Tribunal des professions" => "http://www.canlii.org/fr/qc/qctp/rss_new.xml",
     "d'une cour municipale" => "http://www.canlii.org/fr/qc/qccm/rss_new.xml",
     "de l'Autorité des marchés fin." => "http://www.canlii.org/fr/qc/qcamf/rss_new.xml",
     "du Comité de déonto. policière" => "http://www.canlii.org/fr/qc/qccdp/rss_new.xml",
     "de la Comm. d'accès à l'info." => "http://www.canlii.org/fr/qc/qccai/rss_new.xml",
     "de la CSST" => "http://www.canlii.org/fr/qc/qccsst/rss_new.xml",
     "de la Comm. de prot. du terr. agricole" => "http://www.canlii.org/fr/qc/qccptaq/rss_new.xml",
     "de la Comm. des rel. du travail" => "http://www.canlii.org/fr/qc/qccptaq/rss_new.xml",
     "de la Comm. municipale" => "http://www.canlii.org/fr/qc/qccmnq/rss_new.xml",
     "disciplinaire du Coll. des médecins" => "http://www.canlii.org/fr/qc/qccdcm/rss_new.xml",
     "disciplinaire du Barreau" => "http://www.canlii.org/fr/qc/qccdbq/rss_new.xml",
     "du Conseil de presse" => "http://www.canlii.org/fr/qc/qccpq/rss_new.xml",
     "du Cons. des serv. essentiels" => "http://www.canlii.org/fr/qc/qccse/rss_new.xml",
     "du Tribunal adm. du Québec" => "http://www.canlii.org/fr/qc/qctaq/rss_new.xml"
}
DEUXIÈME ÉTAPE: EXTRAIRE

Avec notre variable rss, il est maintenant facile de construire une boucle. En ruby, on écrit:

rss.each do |tribunal, url|

À l’intérieur de cette boucle, on va extraire le fil RSS de chacun des tribunaux, fil dont l’adresse sera contenue dans la variable url.

On va ensuite invoquer la librairie Nokogiri pour faciliter l’extraction des données, tout en s’identifiant auprès des serveurs de CanLII, ce qui est toujours une bonne pratique.

requete = Nokogiri::XML(open(url, "User-Agent" => "Jean-Hugues Roy, UQAM (roy.jean-hugues@uqam.ca)"))

Puis, on va compter le nombre de décisions qui se trouvent dans le fil. Chaque décision est contenue dans une balise item. Il suffit donc de lire à combien de reprises cette balise est répétée dans le fil et de placer ce nombre dans la variable n.

n = requete.xpath("//item").count

On va ensuite créer une autre boucle qui va nous aider à extraire chacune des n décisions.

(0..n-1).each do |item|

Puis, à chacune de ces décisions, on va extraire différentes données, comme le titre, l’URL ou la description, qu’on va placer dans un autre hash appelé decision.

decision["Titre"] = requete.xpath("//item/decision:casename")[item].text
decision["URL"] = requete.xpath("//item/link")[item].text
decision["Description"] = requete.xpath("//item/description")[item].text

Après avoir extrait une décision, on la place ensuite dans une variable-tableau appelé tout.

tout.push decision

Quand on a extrait toutes les décisions de tous les tribunaux, on enregistre l’ensemble (c’est notre variable tout) dans un fichier CSV.

Ici, le code prend toutes les clés qu’il voit et en fait les entêtes de colonnes, puis il place toutes les valeurs associées dans une ligne correspondant à une décision donnée.

CSV.open("tribunaux.csv", "wb") do |csv|
     csv << tout.first.keys
     tout.each do |hash|
          csv << hash.values
     end
end

On fait rouler le script une fois et on a un beau fichier CSV de toutes les décisions les plus récentes des tribunaux québécois.

Mais le travail n’est pas fini.

TROISIÈME ÉTAPE: GAZOUILLER

Il faudra faire rouler notre script régulièrement pour vérifier si les fils RSS contiennent de nouveaux jugements.

Pour ce faire, on ajoute, au début du script, une nouvelle variable qu’on va appeler ancien et qui va lire notre fichier CSV pour en copier le contenu.

ancien = CSV.read("tribunaux.csv", headers:true)

On vérifie ensuite, à l’intérieur de la boucle qui extrait les données d’une décision, si celle-ci est déjà contenue dans notre CSV.

Si elle s’y trouve, la valeur d’une variable de contrôle qu’on aura appelé x croîtra de 1. Cela veut dire que si la décision qu’on vient d’extraire ne se trouve pas dans les décisions qu’on a déjà extraites auparavant, la valeur de x restera à zéro.

x = 0

ancien.each do |ancienneDecision|
     if decision["Numero"] == ancienneDecision[1]
          x += 1
     end
 end

Si x égale zéro après qu’on ait extrait une décision, cela signifie qu’on vient d’extraire une nouvelle décision. On peut donc la tweeter.

Pour ce faire, il faut d’abord invoquer, au début de notre script, une librairie (gem) appelée twitter.

require "twitter"

Plus loin, dans notre script, il faut ensuite configurer une variable qui prendra en quelque sorte notre identité, sur Twitter. Pour lui permettre de faire cela, il faudra aller chercher des valeurs d’autorisation sur la section «apps» de Twitter, puis les copier dans notre script en utilisant la syntaxe suivante:

client = Twitter::REST::Client.new do |config|
     config.consumer_key = "<entrez vos infos>"
     config.consumer_secret = "<entrez vos infos>"
     config.access_token = "<entrez vos infos>"
     config.access_token_secret = "<entrez vos infos>"
end

Une fois cela fait, on sera en mesure de tweeter la nouvelle décision en utilisant la méthode update de la librairie twitter.

if x == 0
     tweet = "Nouvelle décision " + tribunal + ": " + decision["Titre_court"] + " " + decision["URL"]
     client.update(tweet)
end

On pourrait même envoyer un second tweet avec plus d’infos en répondant à notre premier tweet en utilisant la fonction in_reply_to_status et une méthode (home_timeline[0]) qui nous donne le numéro d’identification du tweet qu’on vient tout juste d’envoyer:

 tweet2 = "@RoboTribunauxQC \nCette décision (" + decision["Numero"] + "), rendue le " + date(decision["Date_de_decision"]) + " traite de:\n" + decision["Description"]
 client.update(tweet2, in_reply_to_status: client.home_timeline[0])

Ce script fonctionne très bien.

Mais voilà: il réside sur mon ordinateur et pour qu’il fonctionne, je dois le faire rouler manuellement. Pas très pratique si je veux tenir les abonnés du compte, sur Twitter, informés à toutes les heures.

Heureusement, il y a ScraperWiki!

QUATRIÈME ÉTAPE: PELLETER TOUT ÇA DANS LE NUAGE

ScraperWiki-logo

ScraperWiki est un service web qui permet, entre autres, de faire rouler des scripts dans le nuage. Ils offrent à tout le monde trois instances (datasets) gratuites. Mais si vous leur écrivez et que vous leur dites que vous êtes journaliste, ils vous en donnent 20!

J’ai donc commencé par copier mon script dans ScraperWiki. Mais voilà: ce site ne sauvegarde pas les données qu’on extrait dans un CSV. Il les met dans une base de données SQL (il utilise en fait sqlite comme version). Et je n’ai que très peu d’expérience avec SQL. Heureusement, ScraperWiki nous donne un peu de documentation.

Ainsi, il m’a fallu, dans le code hébergé sur SW, enlever tous les accents de mes noms de variables. Il m’a également fallu changer deux sections:

– celle où j’enregistre dans ma base de données le résultat de mon scraping:

ScraperWiki.save_sqlite(["Numero"],decision,table_name="TribunauxQC")

– et celle où je copie le contenu actuel de ma base de données dans la variable ancien (ici, en fait, je ne copie pas tout, mais uniquement la colonne «Numero» qui contient le numéro, unique, de chaque décision):

ancien = ScraperWiki.sqliteexecute("select Numero from TribunauxQC")

J’ai éprouvé beaucoup plus de difficultés avec l’envoi automatisé des tweets. Cela fonctionnait pourtant super bien dans mon script, en mode local. Mais dans ScraperWiki, j’avais toujours cette erreur.:

uninitialized constant Twitter::REST (NameError)

Il m’a fallu un certain temps avant de trouver ce qui se passait. Finalement, je me suis rendu compte que ScraperWiki utilisait une version antérieure de la librairie ‘twitter’. J’en ai donc changé la syntaxe et je pouvais tweeter. Mais je n’ai pas encore trouvé la façon d’envoyer un second tweet comme je le faisais dans mon script local. Pour l’instant, donc, mon robot n’envoie donc qu’un seul tweet. À un moment donné, il faut savoir lâcher prise!


 DERNIÈRE ÉTAPE: PARTAGER

Au final, donc, j’ai deux scripts que j’ai rendus publics sur Github.

En terminant: abonnez-vous à mon robot des tribunaux québécois, ainsi qu’à ceux créés par Naël pour l’Ontario et pour les tribunaux fédéraux.


 

2 commentaires

Commentez
  1. J’ai fabriqué ce que j’appelle un robot drone journaliste car cet appareil est capable de capter les fréquences de toutes les stations police d’une région délimité et les ambulances,les pompiers et de se rendre au de l’événement tout seul (drone donc aerien ) et de filmer les images en HD ,ce robot peut être autonome ou diriger.J’essais maintenant de créer une application pour délimiter la region et piloter le drone et gérer les autres fonctionalités mais juste sur IOs bref j’ai 15 ans toute aide est la bienvenue

    1. Whoa! Impressionnant!
      L’ennui, c’est que le Règlement de l’aviation canadien interdit probablement un appareil comme celui-là:
      http://www.tc.gc.ca/fra/aviationcivile/normes/normes-4179.html
      Je serais malgré tout curieux d’en savoir plus sur votre drone automatique. Dans quelle région êtes-vous situé?

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Jean-Hugues Roy [CC] BY-NC-SA, 1988-aujourd'hui Frontier Theme