CXS : Filtre Texter

Prenons le cas d’une extraction d’informations dans un texte qui serait structuré mais de manière non explicite. Il peut s’agir de titres de paragraphes, de blocs qui se démarquent les un des autres, comme l’alternance entre un paragraphe et une table, ou l’alternance entre différents éléments d’un code informatique … Le cas d’un code informatique est intéressant, car nous n’avons pas de tags explicites (comme le seraient des balises XML) mais des signaux dans tout le texte. Par exemple, en Python, une fonction est identifiée par un mot clé def et nous pouvons nous en servir comme signal de départ, reste après à identifier ce bloc (il n’y a pas de délimiteurs explicites) et en extraire ce qui nous intéresse.

Publication initiale : buidez.net (2011) – revu et mis à jour en 2025.

1. Le choix d’une approche

L’objectif peut être l’interprétation, l’extraction de données, ou le nettoyage d’informations dans un texte. Dans un contexte qui se limite au filtrage (sans analyse lexicale, syntaxique ou sémantique) nous avons plusieurs approches pour réaliser un code chargé de rendre ce texte utilisable pour un autre processus, qui lui peut essayer d’interpréter les données comme un un parseur.

La première option serait de construire un code spécifique basé sur un jeu de règles. Mais plus le problème sera complexe, plus les règles seront compliquées et difficiles à comprendre, voire risquées à maintenir par un tiers (il peut y avoir des effets de bord). L’exemple typique est celui de filtres constitués par des séries d’expressions régulières, qui ne sont comprises que par ceux qui les ont écrites, même si un effort a été entrepris utiliser des règles plus élémentaires et itératives.

Une autre option est d’utiliser des bibliothèques spécialisées, j’ai perdu un peu le contact dans le domaine, mais nous utilisions des outils d’origine eGenix (par exemple eGenix mxBase et mxTextTools). Ce qui implique aussi un certain niveau de compréhension, la tagging engine peut correspondre à une approche trop lourde pour des problèmes qui ne le nécessitent pas.

Enfin, nous pouvons aussi réduire la complexité par plusieurs passes successives de simplification (des filtres) puis d’appliquer la fonction (forcément allégée) de traitement proprement dite. Qui sera moins compliquée vu qu’elle travaillera sur un texte simplifié et parfois optimisé pour rendre les règles plus faciles à appliquer, ou faciliter le travail d’un parseur ou s’en passer. Nous pouvons également appliquer cette démarche dans le cas d’un texte avec balises, par exemple en enlevant des blocs XML qui sont inutiles, le reste étant soumis à un parseur ou à une fonction d’interprétation.

Il reste à mettre au point ce premier filtre, l’intérêt c’est qu’il soit générique et réutilisable. Donc du code paramétré via des règles externes et gros grain. Une table au format CSVM suffit car il n’y a pas de grammaire associée aux règles, juste un opérateur qui définit une règle et des opérandes qui correspondent, dont le nombre dépendra de l’opérateur.

Nous utilisons la première colonne du fichier CSVM pour les opérateurs, sous la forme de mots clés. Pour chaque ligne (une règle par ligne) nous avons un mot clé, puis les colonnes suivantes servent à définir les opérandes. Chaque opérateur n’ayant pas le même nombre d’opérande, les cellules qui ne sont pas concernées sont marquées par un caractère blank, usuellement ‘-‘.

2. Principe des opérations

Prenons un cas tout simple, nous voulons définir une recherche remplacement dans le texte. Par exemple nous voulons éliminer les deux points ‘:‘ à la fin d’une ligne, l’objectif est une étape dans la simplification du nom d’une fonction en Python. Nous décidons d’un mot clé ‘STR‘ avec deux opérandes : le texte à trouver ‘:_LF‘ et le texte à remplacer ‘_LF‘. Dans ce cas nous avons une convention, ‘_LF‘ est un code signifiant un signal de fin de ligne, par exemple ‘\n‘. Ce qui peut s’encoder dans une liste Python ['STR', ':_LF', '_LF'] correspondant à une ligne DATA d’un fichier CSVM. Lorsque le filtre va passer sur cette ligne, soit il active la reconnaissance de ‘_LF‘ en ‘\n‘, soit il ne l’active pas et fera une recherche remplacement en traitant ‘_LF‘ en tant que chaine de caractères standard.

Nous pouvons étendre cette stratégie à d’autres caractères dont nous avons besoin et qui risquent de poser un problème de compréhension entre la syntaxe du fichier CSVM et les règles du filtre, par exemple les tabulations. Ces information doivent être passées à la fonction qui va réaliser le paramétrage, un exemple avec le prototype de la fonction texter du module csx.texter :

Il s’agit de la fonction de plus haut niveau du module, qui utilise un objet csvm_ptr  (argument self) correspondant à la table CSVM. Il ne s’agit pas d’un dictionnaire CSVM au sens strict, juste un objet CSVM, c’est un abus de langage. La chaine str correspond au fichier à traiter, dict_blank, dict_tab et dict_lf aux conventions utilisées dans le ‘dictionnaire’ pour gérer les trois types de caractères.

3. Plus de règles

Une fois que nous avons acquis ce principe, nous pouvons encoder plusieurs types de règles qui sont généralement invoquées quand nous avons à faire ce type de travail. Nous avons besoin d’opérateurs qui travaillent sur l’ensemble du fichier (c’est le cas de STR) ou plutôt de la chaine (string) str.

Nous pouvons éclater str en lignes, nous allons avoir alors des opérateurs (de type VEC) qui travaillent sur chaque ligne. Nous pouvons avoir aussi des opérateurs (de type BLK) travaillant sur des blocs de ligne. Nous pouvons faire des recherche remplacement mais utiliser aussi des marqueurs, dans ce cas nous aurons 3 opérandes (op1, o2, op3) : si op1 est présent dans la ligne on appliquera la transformation définie par l’opérateur et op2 + op3.

Nous pouvons avoir aussi des opérateurs de délétion ou d’ajout (en début – fin de fichier, en début – fin de ligne). L’ensemble des opérateurs utilisables par la fonction texter est définie ici :

Et bien sur il y a un opérateur DEBUG pour afficher chaque transformation pendant l’exécution de la fonction texter.

4. Un exemple d’application

L’interface de test du module texter fournit un petit exemple, le ‘dictionnaire’ CSVM étant directement encodé sous la forme d’une chaine de caractères s plutôt qu’un fichier. Avant de traiter s il faut la nettoyer des tabulations (début de lignes) et des fins de ligne (‘\n‘) car nous avons fait l’input via une chaine de caractères s’étendant sur plusieurs lignes. En interne la chaine s embarque les caractères ‘§‘ et ‘£‘ qui implémentent les tabulations et les fin de lignes pour délimiter le CSVM.

La fonction csvm_ptr_get_csvm lit s et génère un objet csvm_ptr servant de dictionnaire, le bloc DATA correspondant est :

Nous avons donc 9 règles, couvrant les opérateurs BLKDEL, BLKSTR, STR, VECDEL. Par exemple la première ligne correspond à une délétion de bloc, tout ce qui est entre if __name__ == '__main__': et cxs.texter done. sera supprimé. Le premier opérande est vide, car nous ne supprimons pas des lignes du bloc ou un signal serait présent, nous supprimons toutes les lignes.

Puis nous lisons un texte à filtrer, par exemple le module lui même (texter.py) sous la forme d’une chaine st :

Puis nous appliquons le filtre avec la fonction texter, et c’est tout :

Ces règles servent à éliminer le contenu des fonctions, nous ne gardons que le prototype de celles ci. Prenons le cas du début du fichier ou nous avons des commentaires (en vert), des lignes d’import, une définition de fonction, une description de la fonction (doc string en rouge brique), puis le code :

Après application du filtre sur cette partie, nous avons :

J’ai ajouté une partie de la suite (après *** 1.03 nous voyons arriver la fonction suivante texter_prep). Le texte initial est donc simplifié via un filtre lisible et maintenable. Après cette étape, il sera plus facile de récupérer les fonctions et leurs descriptions, car il s’agit de blocs délimités par des marqueurs def, sans saut de lignes. Nous pourrons nous concentrer sur la constitution d’un index de fonctions, ce qui correspond à la charge utile du code.

Un code à deux (ou n) étages, par exemple [présentation (filtrage) des données] puis [interprétation des données], est souvent plus simple à concevoir et à maintenir, par rapport à un code monolithique qui essaie de prévoir tous les cas de figure et se peut perdre dans les détails.

5. Quelques subtilités

Dans la réalité les tables de filtrage peuvent être plus complexes et regrouper une bonne centaine de règles. Si nous voulions construire un générateur d’API comme le suggère l’exemple précédent il y aurait beaucoup plus à faire pour prendre en compte toutes les anomalies. Mais on y arrive, un composant buildez.autodoc est basé sur ce principe, nettoyage des codes, interprétation, transformation en HTML cliquable et analyse de l’ensemble : nombre de paquetages, de modules, de lignes de commentaires, de fonctions …

Structure du dictionnaire

Chaque règle est codée sur un mot clé et 8 à 10 opérandes possibles (pour utilisation future) même si 4 opérandes correspond à la version stable. Le bloc métadonnées utilise les titres CODE (pour la première colonne) et TAG (pour les opérateurs), par exemple :

Quel ordre pour les règles ?

Il reste maintenant à définir l’ordre de passage de ces règles pendant le filtrage. Soit celui ci est défini par l’ordre d’apparition dans le fichier CSVM, soit il est gelé dans la fonction de filtrage.  Dans un premier temps la fonction texter utilisait l’ordre DEBUG, STR, VEC, BLK, FIL pour optimiser les temps d’exécution. Puis c’est l’ordre d’apparition dans le fichier CSVM qui a été utilisé, ce qui est beaucoup plus intéressant (self correspond à l’objet csvm_ptr implémentant le ‘dictionnaire’) :

Le défaut c’est que cette option se paye par de nombreuses opérations d’éclatement (split) pour transformer str en liste et de concaténation pour reconstruire str pour appliquer la règle suivante.

Marquage, protection et déprotection

Pour certaines opérations, afin d’éliminer des effets de bords, nous pouvons marquer des lignes avec un tag, ou même remplacer des données du texte, puis faire une transformation en bloc sur ces lignes modifiées. Le bloc suivant élimine les doc strings délimitée par des simples ou doubles guillemets en appliquant cette stratégie avec le tag intermédiaire _BLOCK_ :

Dans d’autres cas, nous faisons ce type de marquage, mais la modification interviendra plus loin dans les règles et non en conclusion du bloc de règles. Parfois la modification interviendra dans une autre étape, dirigée via un autre dictionnaire. Dans ce cas nous parlons de protection : une modification pour mettre certaines lignes à l’abri des transformations, en particulier si elles incluent des termes pouvant être affectés par les transformations successives. Puis une déprotection, nous restaurons les valeurs initiales, pour les garder ou les transformer. Ce type de stratégie, pas très ‘informatique’, est particulièrement efficace sur le plan opérationnel.

Annotation

Le regroupement des règles est utile pour la compréhension de différentes opérations combinées. Comme nous utilisons CSVM comme support de dictionnaire, les blocs de règles peuvent être largement annotés via des commentaires CSVM (lignes non métadonnées et commençant par des caractères ‘#‘). En conséquence, nous pouvons aussi désactiver des règles par un ‘#’ tout en les gardant dans le fichier. L’exemple précédent le montre, avec une annotation pour identifier le bloc et une règle DEBUG désactivée.

6. Conclusion

Pour des fichiers de moins de 5000 lignes et des dictionnaires de 200 lignes, sur une configuration poste de travail, le filtre est efficace (moins de 10 s) et fonctionne sans trop de latence dans un processus scripté (par exemple un fichier batch). Mais clairement, ce type de filtre n’est pas fait pour ‘nettoyer’ des livres entiers, dans ce cas il faut une approche avec buffering (mémoires tampon). Ceci dit si on sait gérer les transitions entre chaque buffer, c’est possible. Par exemple, nous pouvons simplifier chaque partie avec des règles indépendantes, puis réassocier ces portions de document, avant d’utiliser une autre collection de règles. Le module texter n’a pas été prévu pour ces tâches, pour lesquelles il faudra se tourner vers des approches optimisés (1).

Cet outil faisait partie d’un paquetage plus large (CXS) destiné à traiter des documents XML-DocBook. Le filtre texter nettoyait les fichiers XML  de tout ce qui ne nous intéressait pas dans ce type de recherches, sans avoir à parser le XML, de manière à alléger les étapes suivantes. Puis le composant a été recyclé car il est parfaitement générique et peut fonctionner en se passant de balises.

Pour être honnête, si nous l’appliquons à du code, il vaut mieux que celui ci soit écrit d’une manière normalisée sur l’ensemble des modules. Ce qui est le cas de build/buildez depuis les versions Perl, une convention très stricte s’applique, dont un des objectifs était aussi de faciliter la réécriture dans d’autres langages. Dans ce cadre, l’efficacité de texter est maximum. Au final, il s’agit d’un outil  lowcost de tout les jours, mais dont on se réapproprie facilement les règles, même lorsque il n’est pas utilisé depuis longtemps, ce qui est l’essentiel.

Liens et lectures
Retour en haut