Reconnaissance de liens


#1


Lors de la nuit du code citoyen (voir ce compte-rendu), j’ai tenté avec d’autres de reconnaître les liens dans le texte. Je pensais que ça serait direct car la base LEGI a les liens, mais elle ne donne pas les correspondances directes dans le texte, le problème reste donc entier.

Après avoir essayé d’écrire des expressions rationnelles, il est apparu que ça serait laborieux. Peu avant la fin du hackathon, Pierre-Yves a proposé d’utiliser les PEG (Parsing Expression Grammar) qui en résumé sont des expressions rationnelles bien plus puissantes et plus faciles à lire où chaque partie de l’expression est nommée et peut être composée avec d’autres expressions nommées. En Python, il y a la librairie Parsimonious.

Je viens de tester si ça peut effectivement fonctionner : le prototype fonctionne :blush: et le code est court et maintenable. Ci-après la grammaire que j’ai utilisée et les quelques lignes de Python pour tester, et enfin ce qu’il reste à faire pour aboutir au meilleur résultat.

Grammaire de reconnaissance de liens

lien = lienarticle? denature? numtexte? date?
lienarticle = "article " numarticle (" " cies)?
lientexte = nature numtexte? date?

numarticle = ~r"([LDR]\.?|LO)? *[0-9-]+"i
cies = ~r"((quinqu|sex|sept|oct|non)ies|(un|duo|ter|quater|quin|sex|sept|octo|novo)?(dec|vic|tric|quadrag|quinquag|sexag|septuag|octog|nonag)ies|semel|bis|ter|quater)"

nature = "arrêté" / "code" / "décret" / "loi" / "ordonnance"
denature = " "+ ( "de l'arrêté" / "de l’arrêté" / "du code" / "du décret" / "de la loi" / "de l'ordonnance" / "de l’ordonnance" )
numtexte = ~r" *n° *(1[789]|20)?[0-9]{2}-[0-9]+"

jour = ~r"(1er|[12][0-9]|3[01]|[1-9])"
mois = "janvier" / "février" / "mars" / "avril" / "mai" / "juin" / "juillet" / "août" / "septembre" / "octobre" / "novembre" / "décembre"
annee = ~r"(1[56789]|20)[0-9]{2}"
date = " "* "du" " "* jour " " mois " " annee

Code Python de test

  1. Utiliser Python 3 car il y a des problèmes d’Unicode avec Python 2.7
  2. Installer Parsimonious avec pip, par exemple pip3 install parsimonious
  3. Écrire la grammaire ci-dessus dans un fichier nommé grammaire.txt
  4. En Python 3 :
    from parsimonious.grammar import Grammar
    f = open( 'grammaire.txt', 'r' );
    grammaire = f.read();
    f.close();
    grammar = Grammar( grammaire )
    print( grammar.parse( "article 5-5-4 quatertricies de l'ordonnance n° 96-28 du 27 août 2018" ) )
    
  5. Cela donne l’AST (Abstract Syntax Tree) suivant :
    <Node called "lien" matching "article 5-5-4 quatertricies de l'ordonnance n° 96-28 du 27 août 2018">
        <Node matching "article 5-5-4 quatertricies">
            <Node called "lienarticle" matching "article 5-5-4 quatertricies">
                <Node matching "article ">
                <RegexNode called "numarticle" matching "5-5-4">
                <Node matching " quatertricies">
                    <Node matching " quatertricies">
                        <Node matching " ">
                        <RegexNode called "cies" matching "quatertricies">
        <Node matching " de l'ordonnance">
            <Node called "denature" matching " de l'ordonnance">
                <Node matching " ">
                    <Node matching " ">
                <Node matching "de l'ordonnance">
                    <Node matching "de l'ordonnance">
        <Node matching " n° 96-28">
            <RegexNode called "numtexte" matching " n° 96-28">
        <Node matching " du 27 août 2018">
            <Node called "date" matching " du 27 août 2018">
                <Node matching " ">
                    <Node matching " ">
                <Node matching "du">
                <Node matching " ">
                    <Node matching " ">
                <RegexNode called "jour" matching "27">
                <Node matching " ">
                <Node called "mois" matching "août">
                    <Node matching "août">
                <Node matching " ">
                <RegexNode called "annee" matching "2018">
    
  6. Si vous essayez avec d’autres expressions, il faut remarquer que la variable “lien” de la grammaire capturera toujours l’expression étant donné que toutes ses sous-variables sont optionnelles. Parsimonious envoit toutefois une exception parsimonious.exceptions.IncompleteParseError: Rule 'lien' matched in its entirety, but it didn't consume all the text. The non-matching portion of the text begins with 'tricies de l'ordonna' (line 1, column 21).

Reste à faire

EDIT: la méthode ci-dessus est implémentée dans la librairie metslesliens avec une grammaire PEG qui s’est un peu améliorée. Les coches :heavy_check_mark: ci-dessous sont désormais implémentées et les ✓… sont en cours.

C’est donc un prototype ci-dessus, il reste :

  1. :heavy_check_mark: lire l’AST pour en extraire les informations importantes : le numéro de l’article (ici 5-5-4 quatertricies) et le nom du texte (ici ordonnance 96-28 du 27 août 2018 – je sais, le numéro 96-28 ne respecte pas la légistique étant donné que l’année est 2018, mais il n’y a pas forcément besoin de rajouter des contraintes, d’autant que ça pourrait reconnaître des erreurs éventuelles dans les numérotations)
  2. aligner/mettre en correspondance les candidats de liens avec une base de données réelles des articles existants dans les textes (par exemple la base legi.py), à la bonne date de vigueur
  3. Lorsqu’il n’y a pas d’alignement, c’est soit que le programme a un problème, soit que la base a un problème ; dans le 2e cas, ça peut valoir le coup de faire une liste des problèmes qui serait vérifiée par des humains (voire proposée à la DILA dans un second temps une fois que la plupart des faux-positifs seront éliminés)
  4. ✓… Améliorer et optimiser la grammaire ; par exemple j’ai remarqué qu’il y avait vraiment de tout dans les noms d’articles (pas seulement des “28-3-1 quavicies”, mais aussi des “1609 nonies A ter” (coucou le CGI) et des vraies expressions comme “Annexe : Agent de constatation” ou “Annexe II habitats humides” ou “Tableau des abréviations” ou “Cotation des épreuves hommes” ou cet article “Execution” sans mauvais jeu de mots), il faut arriver donc à capter de telles expressions, peut-être en utilisant des expressions non-gloutonnes
  5. Lorsqu’une table de correspondance est fournie par la DILA, comme dans la base LEGI, mettre en correspondance les expressions candidates avec les liens fournis (on a le choix ici d’aligner soit avec les liens fournis soit avec une BDD externe, soit les deux et vérifier qu’on a le même résultat)
  6. Contribuer à Parsimonious, par exemple en écrivant une doc, et/ou chercher d’autres librairies
  7. :heavy_check_mark: Écrire une librairie (Python ?) pour packager cette reconnaissance de liens
  8. :heavy_check_mark: Ajouter la reconnaissance de contexte, par exemple les liens “article 7 du présent code”
  9. ✓… Ajouter la reconnaissance des alinéas, des paragraphes, des points dans les numérotations, par exemple les liens “le point c. de l’antépénultimème alinéa du troisième paragraphe de l’article 7-3-1 A sexvicies-0 du code général des impôts” (noter qu’il faut ici créer le lien vers l’alinéa, il n’est pas forcément besoin de reconnaître l’alinéa dans le texte cible, ce qui est un autre problème avec sa propre complexité) (issue #5)

#2

Noter que ce topic peut être déplacé dans une section plus générale étant donné que le problème concerne plusieurs outils du BO.


#3

Bravo pour cette initiative; c’est très bien d’isoler cette fonctionnalité :+1:

Est-ce que vous avez quelque part une liste des différents cas possibles pour pouvoir créer les tests qui vont bien ?


#4

Il y a aussi une discussion active sur l’issue #2 de Archéo Lex où il vient d’être demandé la même chose.

Du coup, j’ai publié ce midi la liste des noms d’articles spécifiques et j’ai publié deux résultats du programme prototype metslesliens qui implémente la grammaire ci-dessus : loi 78-17, code civil.


#5

La sortie de la version 1.0.0 est imminente, disons dans une semaine. Je ne vais plus modifier le code, et peut-être juste à la marge la grammaire. J’ai mis à jour le README si vous voulez tester ou participer au développement.

Pour participer au développement, j’ai rangé les tâches dans des tableaux par thématiques : grammaire, code python, autre.

  • La thématique grammaire doit pouvoir se faire sans être trop programmeur, ou au moins se commencer sans programmer, il faut toutefois bien maîtriser les regex et comprendre la notion de grammaire (j’ai fait une petite doc pour mieux appréhender la grammaire).
  • La thématique code python demande de discuter et d’implémenter les fonctionnalités futures :
    1. niveau de qualité capturée avec par exemple les accents, espaces manquantes après des virgules,etc (= dégrade-t-on la qualité de la grammaire et/ou prévoit-on plusieurs qualités ? si oui, comment architecturer le code et designer les entrées-sorties) ;
    2. qualifier les expressions reconnues contre des bases de données, ça sera la version 2.0 ;
    3. à encore plus long terme, réfléchir à intégrer d’une façon ou d’une autre DuraLex et metslesliens puisque les deux programmes se complètent.

#6

J’ai sorti la version 1.0.0 deux jours après le message précédent, la version 1.0.1 le lendemain :slight_smile: et je viens de sortir la version 1.1.0. Dans cette version 1.1, il y a :

  • un nouveau format “debug” pour rechercher plus facilement les faux négatifs,
  • diverses améliorations sur la grammaire – notamment une meilleure reconnaissance des divisions supra-articles (titres, chapitres, etc),
  • et une méthode pour dégrader automatiquement, et sur demande explicite, en introduisant des erreurs possibles sur les accents (manquants ou mauvais).

Sinon j’ai regardé et comparé hier le code de Parsimonious et de DuraLex, et à ce que je comprends, les idées sont similaires aux différences près :

  • DuraLex utilise un lexer préalablement à l’étape de parsing (je comprend mieux maintenant la distinction entre lexer et parser) alors que Parsimonious opère au niveau du caractère (il est aussi possible d’utiliser un texte pré-lexé, mais ça ne semble pas très abouti, et il n’y a pas possibilité d’utiliser des regex sur chacun des tokens)
  • DuraLex comprend des “caractères de contrôle” pour se déplacer dans le texte/les tokens, comme “aller à la fin de la ligne” ou “aller au token untel”, ce que n’a pas vraiment (de base) Parsimonious, mais
    1. il est possible d’utiliser une expression rationnelle r"[^\n]*" pour aller en fin de ligne (et ignorer ensuite cette capture lors de la lecture de l’arbre), et
    2. il est possible d’implémenter des extensions d’expressions avec une fonction à préciser pour faire des traitements encore plus complexes.

Parsimonious ne fait que avancer (par design je crois), c’est parfois gênant car cela peut mener dans des impasses (genre ça capture tout ce que ça peut et ça se rend compte ensuite que la suite de l’expression ne matche pas, alors qu’en ayant capturé une fois de moins la suite de l’expression aurait matché, détails, 3e point), ça m’oblige à utiliser des assertions négatives comme ici pour capturer “ladite loi” et “même loi” dans “ladite loi” et “la même loi” mais ça rend moins lisible la grammaire

En tous cas, l’avantage principal que je vois à une grammaire autonome est que ça permet d’avoir une vision d’ensemble et peut-être plus compréhensible lorsqu’on est moins informaticien.

Je vais essayer de plugger metslesliens avec DuraLex, a priori en introduisant un pré-traitement avec metslesliens qui renverra un format structuré pour les expressions reconnues, puis en insérant les expressions reconnues après avoir annoté les tokens DuraLex avec l’index dans la string initiale afin que je fasse matcher avec les index revoyés par metslesliens. Je verrai si ça mène quelque part. En tous cas, je pense que l’arbre sémantique de DuraLex est actuellement un bon format (tu disais, Jean-Marc, qu’il faudrait peut-être choisir une norme comme LegalDocML mais je pense qu’il vaut mieux garder un format interne léger, quitte à avoir une fonction d’export), mais il faudrait le documenter et peut-être l’enrichir un peu, je pense aux expressions “aux e et f du 2°” ou pire aux expressions “au 1° des I et II” (loi 78-17 pour les deux exemples).