- TRRE ist eine Spracherweiterung, die regulären Ausdrücken den Operator
:hinzufügt, um Texttransformationen direkt auszudrücken. Bereitgestellt wird sie als experimentelles,grep -E-ähnliches CLI-Tooltrre. - Die Grundform ist ein transduktives Paar, das wie
a:bein Eingabemuster in ein Ausgabemuster umwandelt. Löschen wird als Transformation mit leerem String wiex:dargestellt, Einfügen entsprechend als:x. - Wie bei normalen regulären Ausdrücken lassen sich Alternativen, Wiederholungen und Zeichenbereichs-Transformationen verwenden; Beispiele wie
cat:dog,[a:A-z:Z]und eine Caesar-Chiffre sind enthalten. - Intern baut die Implementierung statt eines FSA für normale reguläre Ausdrücke einen Finite State Transducer (FST) auf, der Eingabe-Ausgabe-Paare verarbeitet; eine experimentelle On-the-fly-Determinierung wird ebenfalls unterstützt.
- Derzeit gibt es keine vorab gebauten Binaries; man muss selbst bauen. Stabilisierung von DFT, vollständige Unicode-Unterstützung, Vervollständigung der ERE-Funktionen und effiziente Bereichsverarbeitung stehen noch auf der TODO-Liste.
Das Problem, das TRRE lösen will
- Gewöhnliche reguläre Ausdrücke sind nützlich, um Muster in Texten zu finden, bei der Textbearbeitung kann die Logik zur Gruppenverarbeitung jedoch wie ein Nachbearbeitungsschritt wirken und komplex werden.
- TRRE erweitert die Sprache regulärer Ausdrücke, um Pattern Matching und Textänderungen in denselben Ausdruck zu bringen.
- Die Kernsyntax hat die Form
pattern-to-match:pattern-to-generate; das einfachste Beispiel ista:b, dasainbumwandelt. - Das CLI-Tool
trreist eine Implementierung, die dieses Konzept demonstriert und sich ähnlich wiegrep -Eanfühlt.
Grundsyntax für Transformationen
- String-Ersetzungen werden wie
cat:doggeschrieben.echo 'cat' | ./trre 'cat:dog'gibtdogaus.- Dasselbe Ergebnis lässt sich auch als zeichenweise Transformation mit
(c:d)(a:o)(t:g)erzielen.
- Wie bei
sedkann es genutzt werden, um alle Treffer innerhalb eines Strings zu ersetzen.- Wendet man
lamb:cataufMary had a little lamb.an, wird darausMary had a little cat..
- Wendet man
- Löschen wird ausgedrückt, indem die rechte Seite leer bleibt:
string_to_delete:.(x:)orentfernt inxordasxund erzeugtor.a:ersetzt im standardmäßigen Scan Mode alleadurch ein leeres Symbol und entfernt sie damit.- Mit einer Klammerausdrucksform wie
[aie]:lassen sich mehrere Zeichen entfernen.
- Einfügen wird ausgedrückt, indem die linke Seite leer bleibt:
:string_to_insert.(:x)orfügt vororeinxein und erzeugtxor.had a (:little )lambfügtlittleinnerhalb des Kontexts ein.
Transformationen auf regulären Ausdrücken
- TRRE unterstützt wie normale reguläre Ausdrücke Alternativen mit
|.(c:b)at|(d:h)ogwandeltcat doginbat hogum.
- Wiederholungsoperatoren können ebenfalls auf Transformationen angewendet werden.
(cat:dog)*wandeltcatcatcatindogdogdogum.- Im standardmäßigen Scan Mode wird bereits
cat:dogwiederholt angewendet und erzeugt dasselbe Ergebnis.
- Nutzt man Wiederholung im linken Muster, können mehrere Eingaben konsumiert und in eine einzelne Ausgabe umgewandelt werden.
(cat)*:dogwandeltcatcatcatindogum.
- Verwendet man
*oder+im rechten Muster, kann eine Endlosschleife entstehen.- Ausdrücke wie
:a*sollte man vermeiden. - Wenn eine endliche Wiederholung nötig ist, gibt man die Anzahl an, etwa
:(repeat-10-times){10}.
- Ausdrücke wie
Bereichstransformationen und Generatoren
- Zeichenbereichs-Transformationen werden wie
[a:A-z:Z]geschrieben.- Damit lässt sich
regular expressionsinREGULAR EXPRESSIONSumwandeln.
- Damit lässt sich
- Ein Beispiel für eine Caesar-Chiffre ist enthalten.
[a:b-y:zz:a]wandeltcaesar cipherindbftbs djqifsum.[a:zb:a-z:y]wandelt dies wieder zurück incaesar cipher.
- Wie ein Generator kann aus einer einzelnen Eingabe auch mehrere Ausgaben erzeugt werden.
- Standardmäßig wird der erste mögliche Treffer verwendet.
- Mit der Option
-awerden alle möglichen Ausgaben erzeugt.
- Wendet man etwa
:(0|1){3}auf eine leere Eingabe an, lassen sich 3-Bit-Binärsequenzen von000bis111erzeugen. - Kombiniert man
:(0|1){,3}?mit-ma, entstehen Ausgaben in Form von Teilmengen mit Länge höchstens 3.
Sprachspezifikation und Operatorpräzedenz
- Informell ist TRRE als Paar
pattern-to-match:pattern-to-generatedefiniert. - Das linke
pattern-to-matchkann ein String oder ein regulärer Ausdruck sein. - Das rechte
pattern-to-generateist üblicherweise ein String, kann aber ebenfalls ein regulärer Ausdruck sein. - Der Operator
:wird derzeit als nicht assoziativ behandelt; eine FormTRRE:TRREist syntaktisch nicht erlaubt.- Diese Form hätte zwar die natürliche Bedeutung einer Komposition der von TRRE definierten Relationen, könnte aber die Komplexität erhöhen und ist daher noch ausgeschlossen.
- Die Operatorpräzedenz ist von hoch nach niedrig wie folgt:
- Escape-Zeichen
\ - Klammerausdrücke
[] - Gruppierung
() - Wiederholung
* + ? {m,n} - Konkatenation
- Transduktion
: - Alternative
|
- Escape-Zeichen
Modi und Gierigkeit
trreunterstützt zwei Modi.- Scan Mode: der Standardmodus, in dem Transformationen sequenziell angewendet werden.
- Match Mode: wird mit dem Flag
-mverwendet und prüft, ob der gesamte String zum Ausdruck passt.
- Die Option
-aerzeugt alle möglichen Ausgaben. - Der Modifikator
?macht die Operatoren*,+und{,}non-greedy.<(.:)*>gibt bei<cat><dog><>aus.<(.:)*?>gibt bei derselben Eingabe<><>aus.
- Beispiele zum Ändern von Inhalten innerhalb von Tags oder Klammern sind ebenfalls enthalten.
<(.*?:cat)>wandelt<dog> <mouse>in<cat> <cat>um.
FST-basierte Implementierung und Determinierung
- TRRE baut intern einen Finite State Transducer (FST) auf.
- Ein FST ähnelt einem Finite State Automaton (FSA), wie es bei normalen regulären Ausdrücken verwendet wird, verarbeitet aber statt einfacher Strings Eingabe-Ausgabe-Paare.
- Die wichtigsten Unterschiede von TRRE sind:
- Es definiert eine binäre Relation zwischen zwei regulären Sprachen.
- Für die Inferenz wird statt eines FSA ein FST verwendet.
- Für die Performance wird experimentelle On-the-fly-Determinierung unterstützt.
- In regulären Regex-Engines ermöglicht Determinierung, nichtdeterministische Automaten in deterministische Automaten umzuwandeln und so Inferenz in linearer Zeit bezogen auf die Länge des Eingabestrings zu erreichen.
- Auch bei TRRE ist ein ähnlicher Ansatz möglich, aber nicht jeder nichtdeterministische Transducer NFT lässt sich in einen deterministischen Transducer DFT umwandeln.
- Wenn es zwei „bad“ Cycles mit demselben Eingabelabel gibt, kann die Zustandserzeugung in eine Endlosschleife geraten.
- Es gibt Methoden, solche Schleifen zu erkennen, sie sind aber teuer.
Performance und Installationsstatus
- Für einfache Ersetzungen wird ein Beispiel gezeigt, in dem die standardmäßige nichtdeterministische Version etwas langsamer als
sedist../trre '(vodka):(VODKA)': real0m0.046ssed 's/vodka/VODKA/': real0m0.024s
- Für komplexere Aufgaben gibt es ein Beispiel, in dem die deterministische Version
trre_dftschneller alssedist.sed -e 's/\(.*\)/\U\1/': real0m0.508s./trre_dft '[a:A-z:Z]': real0m0.131s
- Vorab gebaute Binaries werden noch nicht bereitgestellt.
- Zur Installation klont man das Repository und baut sowie testet es mit
make && sh test.sh. - Auf der TODO-Liste stehen noch folgende Punkte:
- stabile DFT-Version
- vollständige Unicode-Unterstützung
- Vervollständigung der ERE-Funktionen
- Negation
^innerhalb von[] - Zeichenklassen
- Ankersymbole
$^
- Negation
- effiziente Bereichsverarbeitung
Referenzierte Ansätze
- Der Ansatz zum Matching regulärer Ausdrücke ist stark von Russ Cox’ Regular Expression Matching Can Be Simple And Fast inspiriert.
- Die Idee zur Transducer-Determinierung stammt aus Finitely Subsequential Transducers von Cyril Allauzen und Mehryar Mohri.
- Der Parsing-Ansatz verwendet Erik Eidts Double-E algorithm und ist dem klassischen Shunting Yard algorithm nahe.
1 Kommentare
Hacker-News-Kommentare
Ich bin gespannt, wohin dieses Projekt führt. Allerdings wirkt die Operatorpräzedenz unnatürlich, und andere in diesem Thread scheinen ähnlich zu empfinden.
Bei
cat:dogerwartet man intuitiv, dass es wie(cat):(dog)ist, nicht wieca(t:d)og.Dass
cat:dognicht als(cat):(dog), sondern alsca(t:d)oginterpretiert wird, hat mich auch verwirrt. Wenn man sich aber vor Augen führt, dass wir alle reguläre Ausdrücke ein wenig falsch verwenden, ergibt es Sinn. Reguläre Ausdrücke sollte man „eigentlich“ nicht als Matcher, sondern als String-Generatoren betrachten; formal kann mancat|dogdaher als Erweiterung zu einer Menge wie{catog,cadog}auffassen.Beim Matching nimmt man dann diese String-Menge und führt ein Substring-Matching gegen einen größeren Text durch. Das Problem ist, dass die meisten realen Regex-Engines nicht so funktionieren und aus Gründen der Erwartungskonformität oder Effizienz allerlei seltsame Verhaltensweisen haben.
Wenn man verschiedene Regex-Tools ausprobiert, kommen Varianten wie
(cat)|(dog)oder(cat)|(dog)|(ca[td]og)heraus. Aus einer formelleren Sicht ist es daher meiner Meinung nach korrekt, dasscat:dogca(t:d)ogerzeugt und nicht(cat):(dog). Aber weil wir reguläre Ausdrücke jahrzehntelang als Matching-Werkzeuge missbraucht haben, die auf Nutzererwartungen zugeschnitten sind, setzen inzwischen alle Klammern um den Ausdruck, den sie ersetzen wollen.Dieser Vorschlag ist interessant und gut gestaltet, fühlt sich aber letztlich so an, als wolle er reguläre Ausdrücke zu ihrem ursprünglichen Generator-Modell zurückführen. Das Problem liegt weniger in der Grammatik als in den Werkzeugen.
Ich habe früher etwas in diesem Umfeld gemacht; wenn man reguläre Ausdrücke noch nie als Generatoren von String-Mengen betrachtet hat, kann man hier damit herumspielen: https://onlinestringtools.com/generate-string-from-regex
Allerdings ist auch das Verhalten solcher Generator-Tools sehr spezifisch. Die Tools, die ich verwendet habe, hatten verschiedene Möglichkeiten, den Generator einzuschränken, etwa durch Vorgaben für Closures.
Wenn man sie hinter die Konkatenation verschiebt, können andere Probleme entstehen. Zum Beispiel müsste bei einem nicht-assoziativen
:vielleichtcat:dog:mouseillegal sein; ich bin mir nicht sicher, wie man damit umgehen sollte.In der aktuellen Version wird Epsilon, also der leere String, eingefügt. Wenn man zum Beispiel jedes zweite Zeichen überspringend entfernen will, kann man technisch
..:ausführen, also.(.:eps).Das Ergebnis von
echo 'abcde' | ./trre '..:'ist'ace'.Eigentlich könnte die
:-Verknüpfung auch die Bedeutung einer Komposition regulärer Relationen haben, aber das erschien mir im Moment zu komplex.[a:A-z:Z]wäre[a-z:A-Z]besser; und statt[a:b-y:zz:a]würde ich eine Form wie[a-y:b-z;z:a]vorschlagen.Wenn dich endliche Transduktoren und verwandte Werkzeuge interessieren, lohnt sich ein Blick auf XFST (Xerox Finite-State Transducer). Es wird seit über 20 Jahren in Anwendungen der Computerlinguistik eingesetzt.
Ein finnischer Forscher von PARC kam einmal in eine UT-Vorlesung und zeigte, wie man mit FSTs finnische Morphologie verarbeitet; schon oberflächlich betrachtet war das ziemlich beeindruckend.
Es beschreibt die Arbeit, die bei PARC gemacht wurde.
OpenFst ist eine wirklich hervorragende Bibliothek für Transduktoren. Auch die Pynini-Tutorials mit Anwendungsfällen, die unter anderem bei Johns Hopkins als Aufgaben erstellt wurden, sind recht gut.
[1] https://www.openfst.org/twiki/bin/view/GRM/Pynini
[2] https://www.openfst.org/
Wenn du nach einer Alternative zu Standard-Regex suchst, insbesondere wenn Gruppenlogik schwierig ist oder du wartbare Ausdrücke möchtest, könnte die Rosie Pattern Language passen.
https://gitlab.com/rosie-pattern-language/rosie/-/blob/maste...
https://rosie-lang.org/about/
Cool. Um 1997 herum habe ich meine Informatik-Diplomarbeit über endliche Transduktoren geschrieben, und es war deutlich weniger trivial, als ich erwartet hatte.
Die Aufgabe bestand darin, Komposition und DFA zu implementieren, wo möglich, einschließlich zusammengesetzter Transduktoren. Es ging um die „Algebra endlicher Transduktoren“, der Anwendungsfall war Morphologie. Das Thema war stark unterschätzt, sodass ich etwa in der Mitte aufhören musste. Daher: Respekt.
Zur Syntax frage ich mich, ob du wirklich möchtest, dass
:stärker bindet als die Konkatenationab.Schön zu sehen, dass das Projekt 20 Jahre später immer noch weiterläuft: https://www.openfst.org/twiki/bin/view/FST/WebHome
Ob
:stärker binden sollte als Konkatenation, weiß ich noch nicht sicher. Ich habe mir etwa 100 Beispiele angesehen und fand die aktuelle Variante, also dass:niedriger bindet als., natürlicher; im Code lässt sich das aber buchstäblich durch Ändern einer einzigen Zahl umstellen. Deshalb habe ich es hier gepostet: Ich brauche echtes Feedback.Sobald man irgendeine Art von struktureller Ersetzung vornehmen möchte, wirkt dieser Ansatz nicht mehr ausreichend. Zum Beispiel möchte man manchmal so etwas wie
s/"([^"]*)"/'$1'/machenDarüber hinaus wäre es nützlicher, wenn man innerhalb von
[^"]das, was auf[']matcht, durch\'ersetzen könnteAllgemeiner gesagt: Da ein regulärer Ausdruck für das Matchergebnis faktisch einen Parse Tree definiert, wäre es nützlich, allgemeinere Transformationen auf diesem Baum durchführen zu können
":'(':(\\')|[^"'])*":'"..."-Blocks ändern und die Anführungszeichen in einfache Anführungszeichen'umwandelnDas geht mit diesem Ausdruck:
echo '"hello world" "hello again!"' | ./trre "\":'.+?:-\":'"Das Ergebnis ist
'-' '-'Das heißt, mit dem Ausdruck
".+?:-"wird der Text innerhalb von""durch das Zeichen-ersetzt und zugleich werden die umgebenden Anführungszeichen geändert. Das Fragezeichen bedeutet nicht-gieriger ModusDie Behauptung „Reguläre Ausdrücke sind ein hervorragendes Werkzeug, um Muster in Text zu finden, fühlten sich für das Bearbeiten von Text aber immer unnatürlich an“ scheint das ganze Projekt zu tragen, aber es gibt kein einziges Beispiel
Ich verstehe nicht, warum reguläre Ausdrücke fürs Bearbeiten unnatürlich sein sollen. Ich weiß auch nicht, was hier mit Bearbeiten gemeint ist, und auch nicht, warum Leute mit Gruppen Schwierigkeiten haben
Dieses Projekt hat viele Syntaxbeispiele, aber ich sehe nicht, warum es besser sein soll als normale reguläre Ausdrücke. Mit ein paar Beispielen nach dem Muster „die Standard-Regex-Version sieht so aus, meine Version so, und deshalb wird es einfacher“ könnte ich das Projekt vermutlich verstehen
Wenn man zum Beispiel nur das
yzwischenxundzdurchYersetzen möchte, würde man das in Python ungefähr so machen:pattern = r'(x)y(z)'replacement = r'\1Y\2'result = re.sub(pattern, replacement, text)Ich würde das gern durch das Muster
xy:Yzersetzen:result = re.trre('xy:Yz', text)Wenn
xundzkomplexere Muster oder selbst reguläre Ausdrücke sind, kann dieser Ansatz bequemer seinGutes Projekt
Der C-Code macht wirklich Spaß zu lesen. Sehr schön, ich lese ihn gerade
Nur ein kurzer Kommentar: Der Link zu
theory.pdfim README ist kaputt. Das PDF liegt im Verzeichnisdocs/, daher muss in die URL nurdocs/aufgenommen werdenEs heißt, man solle
*oder+auf der rechten Seite vermeiden, weil das zu Endlosschleifen führen kann. Könnte man das nicht einfach verbieten?Ich verstehe, dass die Syntax-Spezifikation dadurch schwieriger wird, aber ich sehe keinen guten Grund, es beizubehalten
Der ursprüngliche Grund war, dass ich eine interessante Operation namens Komposition von Transducern implementieren wollte. Man kann einfache Operationen auf Strings ausführen und trre wie Filter zusammensetzen, aber ich habe das noch nicht fertiggestellt. Daher ist der Einwand absolut berechtigt
Spannende Erkundung, aber es fehlen Beispiele dafür, warum es in der Praxis besser ist. Natürlich kann es auch sein, dass ich mich einfach zu lange an reguläre Ausdrücke gewöhnt habe
Zum Beispiel sehe ich nicht, warum
(cat):(dog)in trre besser ist alss/cat/dog, oder was an(x:)orbesser ist alss/xor/or. Fast alle Beispiele lassen sich in meinem Kopf relativ leicht auf reguläre Ausdrücke abbildenWenn es einen zentralen Vorteil gibt, dann vermutlich bei der Gruppenlogik, daher wäre es gut, wenn sich die Beispiele darauf konzentrieren würden. Es scheint besser, zuerst zu erklären, warum das die bessere Wahl ist, noch bevor man die Grundsyntax erklärt
Beim Caesar-Chiffre-Beispiel wirkt die Funktion „das rückwärts anwenden“ sehr nötig. Das ist eine häufige Anforderung bei vielen Textersetzungen und in diesem Beispiel besonders klar. Der Programmiererkopf ruft sofort: „Warum muss ich dieselbe Logik zweimal ausdrücken?“
Ich weiß noch nicht, ob es nützlich ist, aber Alternativen zu einem lange etablierten Status quo zu erkunden, ist großartig. Solche Versuche haben zwar oft eine hohe Wahrscheinlichkeit, nicht erfolgreich zu sein, aber die Erkundung an sich ist schön anzusehen
Die Spezifikation wirkt ziemlich unvollständig. Schon das erste Beispiel ist seltsam:
$ echo 'cat' | trre 'c:da:ot:g'dogIch verstehe nicht, was hier passiert. Die Grammatik sieht so aus:
TRRE <- TRRE* TRRE|TRRE TRRE.TRRETRRE <- REGEX REGEX:REGEXWas ist hier der Parse Tree? Warum wird
cnicht zuda? Oder warum wirdcnicht entfernt unddazuotgeändert?Die Idee, eine intuitivere Such-/Ersetzungssemantik als Gruppenoperatoren zu haben, ist gut. Zu MS-DOS-Zeiten konnte man Dinge wie
ren .log .txtmachen, und es funktionierte; aus heutiger Bash-Denkweise ergibt das keinen Sinn, aber auf den ersten Blick war die Absicht sehr klar.Wenn man diesen Operator explizit
~nennt, sieht das Beispiel so aus:$ echo 'cat' | trre 'c:d~a:o~t:g'dogMit überflüssigen Klammern sieht es so aus:
$ echo 'cat' | trre '(c:d)~(a:o)~(t:g)'dogDass
cnicht zudawird, liegt ausschließlich an der Präzedenz. Wenn ich mir diese Diskussion ansehe, habe ich wohl die falsche Präzedenz gewählt, und das stiftet Verwirrung.Die aktuelle Präzedenz-Tabelle sieht so aus:
| 1 | Escape-Zeichen | \ || 2 | Klammerausdruck | [] || 3 | Gruppierung | () || 4 | Wiederholung einzelner Zeichen in ERE | * + ? {m,n} || 5 | Transformation | : || 6 | Verkettung | . (implizit) || 8 | Alternative | | |Daher bindet
:stärker als., also die implizite Verkettung.Wenn man stattdessen verlangt, dass Regexe nicht leer sein dürfen, geht das Löschbeispiel kaputt, aber die Mehrdeutigkeit verschiebt sich zur Verkettung: Es ist dann unklar, ob es
(((c:d)(a:o))(t:g))oder((c:d)((a:o)(d:g)))ist. Wenn man Assoziativität annimmt, dürfte dieser Unterschied keine Rolle spielen.c:d,a:– also nichts – undot:g.Aber beim erneuten Lesen ist es definitiv verwirrend, und theoretisch ist der Einwand berechtigt. Nachdem ich das Repository gelesen hatte, war ich selbst der Meinung, dass
czudawerden müsste, aber sicher bin ich mir nicht.