Show HN : Expressions régulières transductives pour l’édition de texte
(github.com/c0stya)- TRRE est une extension du langage des expressions régulières qui ajoute l’opérateur
:pour exprimer directement des transformations de texte, proposée avec l’outil CLI expérimentaltrre, proche degrep -E - La forme de base est une paire transductive du type
a:b, qui transforme un motif d’entrée en motif de sortie ; la suppression s’écritx:, l’insertion:x, comme une transformation avec la chaîne vide - Comme avec les expressions régulières classiques, on peut utiliser les alternatives, la répétition et les transformations de plages de caractères ; des exemples incluent
cat:dog,[a:A-z:Z]et un chiffrement de César - L’implémentation interne construit un Finite State Transducer (FST), qui manipule des paires entrée-sortie au lieu du FSA des expressions régulières ordinaires, avec une déterminisation expérimentale à la volée
- Il n’existe pas encore de binaire précompilé ; il faut compiler le projet soi-même, et la stabilisation du DFT, la prise en charge complète d’Unicode, la finalisation des fonctionnalités ERE et une gestion efficace des plages restent dans la TODO list
Le problème que TRRE cherche à résoudre
- Les expressions régulières classiques sont utiles pour trouver des motifs dans du texte, mais pour l’édition de texte, la logique de traitement des groupes peut devenir complexe car elle agit comme un post-traitement
- TRRE étend le langage des expressions régulières pour réunir l’appariement de motifs et la modification de texte dans une seule expression
- La syntaxe centrale est de la forme
motif-à-apparier:motif-à-générer, l’exemple le plus simple étanta:b, qui remplaceaparb - L’outil CLI
trreest une implémentation qui illustre ce concept et fonctionne dans un esprit proche degrep -E
Syntaxe de transformation de base
- Une substitution de chaîne s’écrit comme
cat:dogecho 'cat' | ./trre 'cat:dog'affichedog- On peut obtenir le même résultat avec une transformation caractère par caractère comme
(c:d)(a:o)(t:g)
- Cela peut servir, comme
sed, à remplacer toutes les occurrences dans une chaîne- En appliquant
lamb:catàMary had a little lamb., on obtientMary had a little cat.
- En appliquant
- La suppression s’exprime en laissant la partie droite vide, sous la forme
chaîne_à_supprimer:(x:)orsupprimexdansxorpour produireora:remplace tous lesapar le symbole vide dans le mode scan par défaut, ce qui les supprime- On peut utiliser une expression entre crochets comme
[aie]:pour supprimer plusieurs caractères
- L’insertion s’exprime en laissant la partie gauche vide, sous la forme
:chaîne_à_insérer(:x)orinsèrexdevantorpour produirexorhad a (:little )lambinsèrelittledans ce contexte
Transformations au-dessus des expressions régulières
- TRRE prend en charge les alternatives avec
|, comme les expressions régulières classiques(c:b)at|(d:h)ogtransformecat dogenbat hog
- Les opérateurs de répétition peuvent aussi s’appliquer aux transformations
(cat:dog)*transformecatcatcatendogdogdog- En mode scan par défaut,
cat:dogsuffit aussi à appliquer la transformation de façon répétée et à produire le même résultat
- Quand on utilise une répétition dans le motif de gauche, plusieurs entrées peuvent être consommées pour produire une seule sortie
(cat)*:dogtransformecatcatcatendog
- Utiliser
*ou+dans le motif de droite peut provoquer une boucle infinie- Il faut éviter des expressions comme
:a* - Si une répétition finie est nécessaire, on peut préciser le nombre d’occurrences avec
:(repeat-10-times){10}
- Il faut éviter des expressions comme
Transformations de plages et générateur
- Une transformation de plage de caractères s’écrit comme
[a:A-z:Z]- Cela permet de transformer
regular expressionsenREGULAR EXPRESSIONS
- Cela permet de transformer
- Un exemple de chiffrement de César est inclus
[a:b-y:zz:a]transformecaesar cipherendbftbs djqifs[a:zb:a-z:y]permet de revenir àcaesar cipher
- On peut aussi produire plusieurs sorties à partir d’une seule entrée, comme avec un generator
- Par défaut, la première correspondance possible est utilisée
- Avec l’option
-a, toutes les sorties possibles sont générées
- Par exemple, appliquer
:(0|1){3}à une entrée vide permet de générer toutes les séquences binaires sur 3 bits, de000à111 - En utilisant
:(0|1){,3}?avec-ma, on peut générer des sorties de type sous-ensembles de longueur inférieure ou égale à 3
Spécification du langage et priorité des opérateurs
- De manière informelle, TRRE est défini comme une paire
motif-à-apparier:motif-à-générer - Le
motif-à-apparierde gauche peut être une chaîne ou une expression régulière - Le
motif-à-générerde droite est généralement une chaîne, mais peut aussi être une expression régulière - L’opérateur
:est actuellement traité comme non associatif, et une formeTRRE:TRREn’est pas autorisée par la grammaire- Cette forme aurait un sens naturel comme composition des relations définies par TRRE, mais elle est encore exclue car la complexité pourrait devenir importante
- La priorité des opérateurs, du plus fort au plus faible, est la suivante
- caractère d’échappement
\ - expression entre crochets
[] - groupement
() - répétition
* + ? {m,n} - concaténation
- Transduction
: - alternative
|
- caractère d’échappement
Modes et avidité
trreprend en charge deux modes- Scan Mode : mode par défaut, qui applique les transformations séquentiellement
- Match Mode : activé avec l’option
-m, qui vérifie si toute la chaîne correspond à l’expression
- L’option
-agénère toutes les sorties possibles - Le modificateur
?rend les opérateurs*,+,{,}non-greedy<(.:)*>produit<>à partir de<cat><dog><(.:)*?>produit<><>à partir de la même entrée
- Des exemples sont aussi donnés pour modifier le contenu à l’intérieur de balises ou de parenthèses
<(.*?:cat)>transforme<dog> <mouse>en<cat> <cat>
Implémentation fondée sur les FST et déterminisation
- TRRE construit en interne un Finite State Transducer (FST)
- Un FST est proche d’un Finite State Automaton (FSA) utilisé pour les expressions régulières classiques, mais il manipule des paires entrée-sortie au lieu de simples chaînes
- Les différences essentielles de TRRE sont les suivantes
- il définit une relation binaire entre deux langages réguliers
- il utilise des FST plutôt que des FSA pour l’inférence
- il prend en charge une déterminisation expérimentale à la volée pour améliorer les performances
- Dans les moteurs d’expressions régulières classiques, la déterminisation consiste à transformer un automate non déterministe en automate déterministe afin de permettre une inférence en temps linéaire par rapport à la longueur de la chaîne d’entrée
- Une approche similaire est possible avec TRRE, mais tous les transducteurs non déterministes NFT ne peuvent pas être convertis en transducteurs déterministes DFT
- s’il existe deux cycles « bad » portant la même étiquette d’entrée, la génération d’états peut entrer dans une boucle infinie
- il existe des méthodes pour détecter ce type de boucle, mais elles sont coûteuses
Performances et état de l’installation
- La version non déterministe de base est donnée comme légèrement plus lente que
sedsur une substitution simple./trre '(vodka):(VODKA)': real0m0.046ssed 's/vodka/VODKA/': real0m0.024s
- Sur des tâches plus complexes, la version déterministe
trre_dftest montrée comme plus rapide quesedsed -e 's/\(.*\)/\U\1/': real0m0.508s./trre_dft '[a:A-z:Z]': real0m0.131s
- Aucun binaire précompilé n’est encore fourni
- L’installation consiste à cloner le dépôt puis à compiler et tester avec
make && sh test.sh - La TODO list contient encore les éléments suivants
- une version DFT stable
- la prise en charge complète d’Unicode
- la finalisation des fonctionnalités ERE
- négation
^dans[] - classes de caractères
- symboles d’ancrage
$^
- négation
- une gestion efficace des plages
Approches de référence
- L’approche de mise en correspondance par expressions régulières est fortement inspirée de Regular Expression Matching Can Be Simple And Fast de Russ Cox
- L’idée de déterminisation des transducteurs vient de Finitely Subsequential Transducers de Cyril Allauzen et Mehryar Mohri
- L’approche de parsing utilise l’algorithme Double-E d’Erik Eidt, proche de l’algorithme de Shunting Yard classique
1 commentaires
Avis de Hacker News
Je suis curieux de voir où ce projet va aller. Cela dit, la priorité des opérateurs semble peu naturelle, et d’autres personnes dans ce fil semblent avoir eu la même impression
On s’attend naturellement à ce que
cat:dogsoit équivalent à(cat):(dog), et non àca(t:d)ogLe fait que
cat:dogsoit interprété commeca(t:d)ogplutôt que(cat):(dog)m’a aussi dérouté, mais en repensant au fait que nous utilisons tous les expressions régulières un peu de travers, ça s’explique. Les expressions régulières ne devraient “à l’origine” pas être vues comme des matchers, mais comme des générateurs de chaînes ; ainsi,cat|dogpeut formellement être vu comme une expansion vers un ensemble du type{catog,cadog}Pour la correspondance, il suffit ensuite de faire du matching de sous-chaînes de cet ensemble de chaînes dans un texte plus grand. Le problème, c’est que la plupart des moteurs d’expressions régulières réels ne fonctionnent pas ainsi et ont toutes sortes de comportements étranges pour coller aux attentes ou pour des raisons d’efficacité
Si l’on teste plusieurs outils d’expressions régulières, on obtient des variantes comme
(cat)|(dog)ou(cat)|(dog)|(ca[td]og). Donc, d’un point de vue plus formel, il me semble correct quecat:dogproduiseca(t:d)ogplutôt que(cat):(dog). Mais après des décennies à détourner les expressions régulières en outils de matching conformes aux attentes des utilisateurs, tout le monde met désormais entre parenthèses l’expression qu’il veut remplacerCette proposition est intéressante et bien conçue, mais elle donne finalement l’impression de ramener les expressions régulières à leur modèle de générateur d’origine. Le problème relève davantage des outils que de la syntaxe
J’ai travaillé autrefois sur des sujets proches, et si vous n’avez jamais pensé aux expressions régulières comme à des générateurs d’ensembles de chaînes, vous pouvez vous amuser avec ceci : https://onlinestringtools.com/generate-string-from-regex
Cela dit, le comportement de ces outils de génération est lui aussi très spécifique. Les outils que j’utilisais proposaient plusieurs façons de contraindre le générateur, par exemple en imposant des limites aux fermetures
La repousser après la concaténation peut créer d’autres problèmes. Par exemple, avec un
:non associatif,cat:dog:mousedevrait peut-être être illégal, mais je ne suis pas sûr de la façon de le traiterDans la version actuelle, on insère epsilon, c’est-à-dire la chaîne vide. Par exemple, pour supprimer un caractère sur deux, on peut techniquement exécuter
..:, qui est.(.:eps)Le résultat de
echo 'abcde' | ./trre '..:'est'ace'En fait, l’association de
:pourrait aussi avoir le sens d’une composition de relations régulières, mais j’ai considéré que c’était trop complexe pour l’instant[a:A-z:Z],[a-z:A-Z]serait préférable, et j’aimerais proposer une forme comme[a-y:b-z;z:a]au lieu de[a:b-y:zz:a]Si les transducteurs à états finis et les outils associés vous intéressent, XFST (Xerox Finite-State Transducer) vaut le coup d’œil. Il est utilisé depuis plus de 20 ans dans des applications de linguistique computationnelle
Un chercheur finlandais de PARC était venu dans un cours à l’UT pour montrer comment traiter la morphologie du finnois avec des FST, et même vu de l’extérieur, c’était assez impressionnant
Il décrit les travaux réalisés chez PARC
OpenFst est vraiment une excellente bibliothèque pour les transducteurs. Les tutoriels d’utilisation de Pynini réalisés sous forme de devoirs par Johns Hopkins et d’autres sont également corrects
[1] https://www.openfst.org/twiki/bin/view/GRM/Pynini
[2] https://www.openfst.org/
Si vous cherchez une alternative aux expressions régulières standard, en particulier si la logique de groupes vous semble difficile ou si vous voulez des expressions maintenables, Rosie Pattern Language pourrait convenir
https://gitlab.com/rosie-pattern-language/rosie/-/blob/maste...
https://rosie-lang.org/about/
Génial. Vers 1997, j’ai écrit mon mémoire de Diplom en informatique sur les transducteurs à états finis, et c’était beaucoup moins trivial que je ne l’imaginais
Le sujet consistait à implémenter la composition et les DFA lorsque c’était possible, y compris des transducteurs composés. C’était une “algèbre des transducteurs à états finis”, avec la morphologie comme cas d’usage. Le sujet était largement sous-estimé, si bien que j’ai dû m’arrêter à mi-chemin. Donc respect
Concernant la syntaxe, je me demande si vous voulez vraiment que
:se lie plus fortement que la concaténationabC’est agréable de voir que le projet continue 20 ans plus tard : https://www.openfst.org/twiki/bin/view/FST/WebHome
Je ne suis pas encore sûr que
:doive se lier plus fortement que la concaténation. J’ai regardé une centaine d’exemples et trouvé que l’approche actuelle, c’est-à-dire:avec une priorité inférieure à., était plus naturelle ; mais dans le code, il suffit littéralement de changer un seul nombre pour le modifier. C’est pour cela que je l’ai posté ici, j’ai vraiment besoin de retours concretsDès qu’on veut faire une forme de substitution structurelle, cette approche ne semble plus suffisante. Par exemple, on peut vouloir faire quelque chose comme
s/"([^"]*)"/'$1'/En plus, si l’on pouvait remplacer, parmi les
[^"], ceux qui correspondent à[']par\', ce serait encore plus utilePlus généralement, comme une expression régulière définit en pratique un arbre de parsing sur le résultat de la correspondance, il serait utile de pouvoir effectuer des transformations plus générales sur cet arbre
":'(':(\\')|[^"'])*":'"..."et remplacer les guillemets par des apostrophes'C’est possible avec cette expression :
echo '"hello world" "hello again!"' | ./trre "\":'.+?:-\":'"Le résultat est
'-' '-'Autrement dit, l’expression
".+?:-"remplace le texte à l’intérieur de""par le symbole-, tout en changeant aussi les guillemets autour. Le point d’interrogation indique le mode non gloutonTout le projet semble reposer sur l’affirmation selon laquelle « les expressions régulières sont un excellent outil pour trouver des motifs dans du texte, mais elles m’ont toujours paru peu naturelles pour l’édition de texte », mais il n’y a aucun exemple
Je ne comprends pas pourquoi les expressions régulières seraient peu naturelles pour l’édition. Je ne sais pas non plus ce que signifie « édition » ici, ni pourquoi les gens auraient des difficultés avec les groupes
Il y a beaucoup d’exemples de syntaxe dans ce projet, mais je ne vois pas en quoi c’est mieux que les expressions régulières classiques. Quelques exemples du type « voici la version avec une expression régulière de base, voici ma version, et voilà pourquoi c’est plus simple » aideraient à comprendre le projet
Par exemple, pour remplacer uniquement les
ysitués entrexetzparY, en Python on ferait à peu près ceci :pattern = r'(x)y(z)'replacement = r'\1Y\2'result = re.sub(pattern, replacement, text)J’aimerais remplacer cela par le motif
xy:Yz:result = re.trre('xy:Yz', text)Si
xetzsont des motifs plus complexes, voire des expressions régulières eux-mêmes, cette approche peut être plus pratiqueBeau projet
Le code C est vraiment agréable à lire. Très bon travail, je suis en train de le parcourir
Juste un petit commentaire : le lien vers
theory.pdfdans le README est cassé. Le PDF se trouve dans le répertoiredocs/, il suffit donc d’ajouterdocs/à l’URLIl est indiqué qu’il faut éviter d’utiliser
*ou+dans la partie droite, car cela peut provoquer une boucle infinie ; pourquoi ne pas simplement l’interdire ?Je comprends que cela complique la spécification de la syntaxe, mais je ne vois pas de bonne raison de le conserver
La raison initiale était que je voulais implémenter une opération intéressante de composition de transducteurs. On peut effectuer des opérations simples sur une chaîne et composer des trre comme des filtres, mais je ne l’ai pas encore terminé. Donc oui, la remarque est pertinente
Exploration intéressante, mais il manque des exemples montrant pourquoi c’est réellement mieux. Bien sûr, c’est peut-être parce que je suis habitué aux expressions régulières depuis trop longtemps
Par exemple, je ne vois pas en quoi
(cat):(dog)en trre est meilleur ques/cat/dog, ni ce que(x:)orapporte par rapport às/xor/or. Presque tous les exemples se traduisent assez facilement, dans ma tête, par des expressions régulières relativement simplesS’il y a un avantage clé, j’imagine qu’il se situe du côté de la logique des groupes, donc les exemples gagneraient à se concentrer là-dessus. Il me semblerait préférable d’expliquer d’abord pourquoi c’est un meilleur choix, avant même de présenter la syntaxe de base
L’exemple du chiffre de César donne vraiment envie d’une fonctionnalité « appliquer ceci en sens inverse ». C’est une demande fréquente dans beaucoup de substitutions de texte, et c’est particulièrement évident dans cet exemple. Le cerveau de programmeur crie immédiatement : « pourquoi devrais-je exprimer deux fois la même logique ? »
Je ne sais pas encore si c’est utile, mais explorer des alternatives au statu quo établi depuis longtemps est une excellente chose. En général, ce genre de tentative a aussi de bonnes chances de ne pas aboutir, mais l’exploration elle-même est plaisante à voir
La spécification semble assez lacunaire. Le tout premier exemple est déjà bizarre :
$ echo 'cat' | trre 'c:da:ot:g'dogJe ne comprends pas ce qui se passe ici. La grammaire est donnée comme ceci :
TRRE <- TRRE* TRRE|TRRE TRRE.TRRETRRE <- REGEX REGEX:REGEXQuel est l’arbre d’analyse ici ? Pourquoi
cne devient-il pasda? Ou bien pourquoicn’est-il pas supprimé etdatransformé enot?L’idée d’avoir une sémantique de recherche/remplacement plus intuitive qu’un opérateur de groupement est bonne. À l’époque de MS-DOS, on pouvait faire des choses comme
ren .log .txtet ça marchait ; avec une mentalité bash moderne, ça n’a aucun sens, mais l’intention était très claire au premier coup d’œil.Si on appelle explicitement cet opérateur
~, l’exemple ressemble à ceci :$ echo 'cat' | trre 'c:d~a:o~t:g'dogAvec des parenthèses superflues, cela donne :
$ echo 'cat' | trre '(c:d)~(a:o)~(t:g)'dogLa raison pour laquelle
cne devient pasdatient entièrement aux priorités. En voyant cette discussion, j’ai l’impression d’avoir choisi de mauvaises priorités, et que cela crée de la confusion.Le tableau des priorités actuel est le suivant :
| 1 | caractère d’échappement | \ || 2 | expression entre crochets | [] || 3 | groupement | () || 4 | répétition ERE d’un seul caractère | * + ? {m,n} || 5 | transformation | : || 6 | concaténation | . (implicite) || 8 | alternative | | |Donc
:se lie plus fortement que., c’est-à-dire que la concaténation implicite.Si, à la place, on exige que l’expression régulière ne soit pas vide, l’exemple de suppression casse, mais l’ambiguïté se reporte sur la concaténation. Autrement dit, il devient ambigu de savoir si c’est
(((c:d)(a:o))(t:g))ou((c:d)((a:o)(d:g))). Si l’on suppose une associativité, cette différence ne devrait pas avoir d’importance.c:d,a:c’est-à-dire rien, puisot:g.Mais en relisant, c’est clairement confus, et sur le plan théorique la remarque est juste. Après avoir lu le dépôt, j’en suis moi aussi venu à penser que
cdevrait être transformé enda, mais je n’en suis pas certain.