Reguläre Ausdrücke

Richard Feynman, einer der bekanntesten Physiker des 20. Jahrhunderts, war überzeugt, dass niemand die Quantentheorie versteht. Über reguläre Ausdrücke, auch reguläre Expressionen genannt, könnte ähnliches behauptet werden. Vielleicht liegt es daran, dass diese Ausdrücke auf den ersten Blick einschüchternd aussehen können:

#?([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})

Allerdings sind reguläre Ausdrücke nicht ganz so kontraintuitiv wie besagte Theorie und sind in der Entwicklung sowie im IT-Alltag ein Werkzeug, welches in vielen Fällen hilfreich zur Seite stehen kann, aber auch Grenzen hat.

Definition

Aus Sicht der theoretischen Informatik ist ein regulärer Ausdruck eine Methode, um Mengen von Zeichenketten mit gemeinsamen Mustern zu beschreiben. Reguläre Ausdrücke werden durch die Verwendung von verschiedenen Operatoren und Konventionen definiert, die es ermöglichen, komplexe Suchmuster zu erstellen.

Die Theorie der regulären Ausdrücke basiert auf den Konzepten der endlichen Automaten und der regulären Sprachen. Vereinfacht gesehen, kann die Funktionalität eines regulären Ausdrucks als Musterabgleich gesehen werden. Oft werden diese Ausdrücke als Regex oder Rexexp abgekürzt.

Ein einfacher regulärer Ausdruck könnte wie folgt aussehen:

[abc]

Dieser Ausdruck würde in einem Text die Zeichen a, b, und c finden. Über einen solchen regulären Ausdruck können Zeichenfolgen in Texten identifiziert, extrahiert und verarbeitet werden.

Geschichte

Die Geschichte regulärer Ausdrücke geht zurück auf das Jahr 1951, in welchem der Mathematiker Stephen Cole Kleene den Begriff prägte.

Praktische Anwendung fanden solche Ausdrücke in den 1960er-Jahren, als Ken Thompson, diese im Editor QED implementierte, als er diesen für das Compatible Time-Sharing System (CTSS) neuschrieb.

Über die Jahre wurde die Funktionalität regulärer Ausdrücke in weitere Werkzeuge wie grep und sed integriert. Im Rahmen der Unix-Philosophie, in welcher ein Werkzeug eine Aufgabe gut beherrschen soll, boten sich hiermit noch mächtigere Werkzeuge für die Textverarbeitung. Die Ausdrücke fanden nicht nur in der Textverarbeitung, sondern auch in der lexikalischen Analyse im Compiler-Design Anwendung.

Neben der Integration in solche Werkzeuge wurde vor allem beginnend mit den 1980er-Jahren die Integration regulärer Ausdrücke in Programmiersprachen wie Perl vorangetrieben. Einhergehend mit dieser Entwicklung wurde die Ausdrücke mächtiger und mehr Anwendungsfälle konnten mit diesen bearbeitet werden. Larry Wall, der Schöpfer von Perl, erweiterte die Fähigkeiten regulärer Ausdrücke erheblich und machte sie zu einem zentralen Bestandteil seiner Sprache.

In den 1990er-Jahren kam es zu einer Standardisierung von Syntax und Verhalten regulärer Ausdrücke, was die Entwicklung der Bibliothek der Perl-kompatiblen regulären Ausdrücke (PCRE) vorantrieb. Diese Bibliothek wurde in vielen Anwendungen verwendet und ist bis heute eine der am weitesten verbreiteten Implementierungen von regulären Ausdrücken.

Grundlagen

Doch wie genau werden reguläre Ausdrücke erstellt? Solche Ausdrücke können aus vielen unterschiedlichen Elementen bestehen, wie Literalen, Zeichenklassen, Quantifizierer und Gruppen.

So wäre ein regulärer Ausdruck bestehend aus einem einzigen Literal gültig:

a

Dieser Ausdruck würde hierbei auf die Vorkommen von a in einem Text matchen.
In regulären Ausdrücken können Oder-Verknüpfungen gebildet werden. So würde der Ausdruck:

a|b

entweder auf das Zeichen a oder auf das Zeichen b in einem Text matchen.

In den meisten Fällen wird allerdings nicht nach einem einzelnen Literal gematcht, sondern mit Zeichenklassen, Quantifizierern und Gruppen gearbeitet. Im Grundsatz würde allerdings nichts dagegen sprechen, einen regulären Ausdruck komplett, als Literal zu definieren:

Supercalifragilisticexpialigetisch

Diese Definition würde dazu führen, dass jedes Auftreten des Wortes Supercalifragilisticexpialigetisch gematcht werden würde. Allerdings wäre der reguläre Ausdruck in diesem Fall nicht mehr als eine einfache Suche.

Da es in regulären Ausdrücken eine Reihe von Zeichen mit spezieller Bedeutung gibt, müssen diese in bestimmten Fällen maskiert werden. Dies geschieht mit einem Backslash und dem sich anschließenden Zeichen:

\.

In diesem Beispiel würde der Punkt als normaler Punkt behandelt werden und nicht als Zeichen mit spezieller Bedeutung betrachtet werden.

Zeichenklassen

Zeichenklassen in regulären Ausdrücken erlauben es, eine Menge von Zeichen zu definieren, von denen jedes ein potenzielles Match für ein Zeichen aus dem Eingabetext darstellen kann. So würde der reguläre Ausdruck:

[abcdefghijklmnopqrstuvwxyz]

auf alle Zeichen zwischen a und z matchen. Die Zeichen werden hierbei in eckige Klammern eingefasst. Obiger Ausdruck kann allerdings sinnvoller gestaltet werden:

[a-z]

Der Bindestrich führt dazu, dass der Ausdruck als a bis z gelesen werden kann und damit einen Bereich definiert. Auch können mehrere Zeichenklassen in einem Block definiert werden:

[a-zA-Z]

Diese Zeichenklassen würde bei Buchstaben in Klein- und Großschreibung anschlagen. Bei dieser Notation ist darauf zu achten, dass die Definitionen ohne Leerzeichen aneinander gehangen werden.

Um auch deutsche Umlaute und das Eszett zu berücksichtigen, müssen diese Zeichen explizit zur Zeichenklasse hinzugefügt werden:

[a-zA-ZäöüÄÖÜß]

Standard-Zeichenklassen

Neben den selbstdefinierten Zeichenklassen existieren eine Reihe von vordefinierten Zeichenklassen, welche ebenfalls genutzt werden können.

So existiert die Zeichenklasse \d, welche allen Dezimalziffern entspricht, also 0 bis 9; dies entspricht der selbstdefinierten Zeichenklasse:

[0-9]

Die Zeichenklasse \D definiert das Gegenteil der vorherigen Klasse und matcht auf alle Nichtziffern und entspricht damit folgender Zeichenklasse:

[^0-9]

Während der Zirkumflex innerhalb einer Zeichenklasse normal genutzt werden kann, negiert er, am Anfang der Zeichenklasse stehend, diese. Eine Erweiterung dieser Standardklassen sind die Klassen \w und \W, welche für alle Wortzeichen stehen und folgenden selbstdefinierten Zeichenklassen entsprechen würden:

[a-zA-Z0-9_]
[^a-zA-Z0-9_]

Daneben existieren, abhängig von der Implementierung weitere vordefinierte Zeichenklassen, wie \s für Whitespace-Zeichen oder die Klasse \S für alle Nicht-Whitespace-Zeichen.

Metazeichen

Im Rahmen regulärer Ausdrücke sind einige Metazeichen definiert, welche unterschiedlichste Bedeutungen haben und nicht als die Zeichen selbst interpretiert werden. Sie werden verwendet, um Muster für die Textsuche und -manipulation zu definieren. Einige dieser Zeichen sollen nachfolgend vorgestellt werden.

Der Punkt ist als Metazeichen so definiert, dass er für jedes beliebige Zeichen, bis auf einen Zeilenumbruch steht. Damit würde der Ausdruck:

a.c

unter anderem auf folgende Zeichenketten matchen:

aac
abc
acc

Solange ein a gefolgt von einem beliebigen Zeichen, wiederum gefolgt von einem c im Text vorkommt, wird dieses gematcht.

Der Zirkumflex markiert den Anfang einer Zeile bzw. eines Textes. Damit würde der reguläre Ausdruck:

^Lorem Ipsum

in einem gegebenen Beispieltext:

Lorem Ipsum
	
abcdefghijklmnopqrstuvwxyz ABCDEFGHIKLMNOPQRSTUWXYZ
	
Lorem Ipsum

das Auftreten von Lorem Ipsum am Anfang des Textes matchen. Die gegenteilige Operation kann mit dem Metazeichen $ erreicht werden:

Lorem Ipsum$

In diesem Fall würde das Lorem Ipsum am Ende des Beispieltextes gefunden werden.

Es ist wichtig zu beachten, dass innerhalb einer benutzerdefinierten Zeichenklasse die meisten Metazeichen, wie der Punkt oder der Stern, ihre spezielle Bedeutung verlieren und als normale Zeichen behandelt werden. Ausnahmen sind der Zirkumflex, wenn er als erstes Zeichen in der Klasse verwendet wird, um die Klasse zu negieren, der Bindestrich, um einen Bereich anzugeben, und der Backslash, um Escape-Sequenzen zu ermöglichen.

Quantifizierer

Quantifizierer (engl. Quantifiers) in regulären Ausdrücken sind spezielle Metazeichen, die angeben, wie oft das vorangehende Element in einem Textmuster vorkommen muss, um eine Übereinstimmung zu erzielen. Sie sind entscheidend, um die Flexibilität der Mustererkennung zu erhöhen.

Gegeben sei folgende Telefonnummer:

0176/04069015

Für diese Nummer könnte ein regulärer Ausdruck erstellt werden:

[0-9][0-9][0-9][0-9]/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]

In dem Beispiel wurde auf Standardzeichenklassen verzichtet. Stattdessen wurden eigene Zeichenklassen definiert. Durch diesen Ausdruck werden die ersten vier Ziffern, dann ein Slash und anschließend die folgenden Ziffern gematcht.

Allerdings ist dieser Ausdruck weder sonderlich elegant, noch deckt er das Problem vollständig ab. Immerhin können Telefonnummern unterschiedlich lang sein und auch der Slash könnte optional sein. Hier kommen Quantifizierer zum Einsatz, von denen es eine Vielzahl mit diversen Anwendungsoptionen gibt.

Der Stern-Quantifizierer definiert, dass das vorangehende Element null- oder mehrmals vorkommt. So würde der Ausdruck:

a*b

unter anderem auf folgende Zeichenketten matchen:

ab
aab
aaab
b

Die Anzahl der a’s vor dem Zeichen b sind hierbei unerheblich.

Das Plus-Quantifizierer definiert, dass das vorangehende Element mindestens einmal vorkommen muss. Damit würde der Ausdruck:

a+b

unter anderem auf folgende Zeichenketten matchen:

ab
aab
aaab

Ein Match auf eine Zeichenkette nur bestehend aus einem b wäre bei diesem Quantifizierer ausgeschlossen.

Zu dieser Gruppe von Quantifizierern gehört auch das Fragezeichen. Bei diesem kann das vorangehende Element null- oder einmal vorkommen. Damit würde der Ausdruck:

a?b

unter anderem auf folgende Zeichenketten matchen:

ab
b

Ein Match auf eine Zeichenkette wie aab wäre bei dieser Variante ausgeschlossen.

Daneben existieren eine Reihe von komplexeren Quantifizierer wie den geschweiften Klammern, mit denen eine bestimmte Anzahl von Wiederholungen definiert werden kann. Der Ausdruck:

a{3}b

wurde spezifizieren, dass drei a’s hintereinander folgend von einem b gesucht werden. Damit würde dieser Ausdruck auf folgende Zeichenkette matchen:

aaab

Eine Erweiterung dieser Variante ist die Nutzung von Bereichen:

a{2,4}b

Hiermit würden alle Zeichenketten matchen bei denen zwischen zwei und vier a’s enthalten sind:

aab
aaab
aaaab

Eine weitere Abwandlung dieser Notationen ist es festzulegen, wie oft ein Element mindestens vorkommen muss:

a{2,}b

In diesem Fall müsste das Zeichen a mindestens zweimal vorkommen.

Mithilfe dieser Quantifizierer könnte der Ausdruck zum Match obiger Telefonnummer nun wesentlich vereinfacht werden:

[0-9]+/?[0-9]+

Damit ist der Ausdruck so definiert, dass eine beliebige Anzahl an Ziffern vorkommen können, gefolgt von einem optionalen Slash, wiederum gefolgt von einer beliebigen Anzahl an Ziffern.

Allerdings zeigt sich hier auch eine der Grenzen regulärer Ausdrücke: die Vielfalt möglicher Telefonnummernformate weltweit. Je nach Land und Region gelten unterschiedliche Regeln für den Aufbau einer Telefonnummer. Ein umfassender regulärer Ausdruck, der all diese Varianten abdeckt, könnte schnell sehr komplex werden und schwer zu warten sein.

In solchen Fällen kann es sinnvoll sein, zusätzliche Validierungslogik zu implementieren und nicht zu versuchen, die komplette Logik über einen regulären Ausdruck zu implementieren.

Gierige und faule Quantifizierer

Bei den beschriebenen Quantifizierern existieren gierige (engl. greedy) und faule (engl. lazy) Varianten. Gierige Quantifizierer versuchen, so viel wie möglich von der Zeichenkette zu erfassen, während sie den Regeln des regulären Ausdrucks folgen. Damit greifen sie den längstmöglichen Teil der Zeichenkette, der mit dem Muster übereinstimmt.

Faulen Quantifizierern hingegen geht es darum, so wenig wie möglich zu erfassen, während sie immer noch eine Übereinstimmung finden. Damit greifen sie den kürzestmöglichen Teil der Zeichenkette, der mit dem Muster übereinstimmt.

Quantifizierer sind im Normalfall gierig. Der Ausdruck:

a.*b

würde hiermit die Zeichenfolge:

ababab

als Ganzes matchen. Um diesen Ausdruck in der Lazy-Konfiguration zu betreiben, muss ein Fragezeichen nachgestellt werden:

a.*?b

In diesem Fall würden die ab-Blöcke jeweils einzeln gematcht werden.

Neben diesen beiden Varianten existieren noch possessive Quantifizierer. Solche Quantifizierer verhalten sich wie gierige Quantifizierer, aber sie geben einmal erfasste Zeichen nicht mehr frei.

Das kann das Matching verhindern, wenn der Rest des Musters nicht mehr passt. Possessive Quantifizierer können in bestimmten Situationen die Effizienz der Auswertung verbessern, da sie das aufwendige Backtracking unterbinden, können aber auch zu nicht intuitiven Ergebnissen führen, wenn sie nicht mit Bedacht eingesetzt werden.

Gruppierungen

Eine weitere Möglichkeit bei der Entwicklung regulärer Ausdrücke sind Gruppierungen. So würde der Ausdruck:

abc

nur auf die Zeichenkette abc matchen. Sollen hier jetzt auch Zeichenketten wie abcabc gematcht werden, können Klammern zur Gruppierung im Zusammenhang mit einem Quantifizierer genutzt werden:

(abc)+

Damit würden unter anderem folgende Zeichenketten auf den Ausdruck passen:

abc
abcabc
abcabcabc

Eine weitere Art von Gruppierung ist die Bildung von sogenannten Erfassungsgruppen. Mit diesen Gruppen, welche einen Teil des Ausdrucks ausmachen, kann später weitergearbeitet werden, z. B. in Form von Rückreferenzen:

(abc).*\1

In diesem Beispiel wird die Zeichenkette abc gesucht, welcher beliebige Zeichen folgen, bis schlussendlich wieder abc folgt. Dies wird über den Rückverweis auf die erste Erfassungsgruppe (\1) gelöst. Damit würde dieser Ausdruck unter anderem auf folgende Zeichenketten matchen:

abcabc
abcloremabc
abcipsumabc

Eine weitere Möglichkeit zur Nutzung von Gruppierung sind Oder-Verknüpfungen in einer Gruppe:

(a|b)c

Mit einer solchen Verknüpfung würden Zeichenketten wie ac und bc gematcht, allerdings nicht die Zeichenkette abc.

Daneben existieren Varianten wie verschachtelte Gruppen, benannte Gruppen oder nicht erfassende Gruppen, welche hier nicht weiter im Detail behandelt werden sollen.

Lookaround-Assertions

Eine weitere Möglichkeit zur Verfeinerung von regulären Ausdrücken sind Lookahead– und Lookbehind-Assertions. Im Deutschen könnte man diese Begrifflichkeiten mit vorwärts bzw. rückwärtsgerichtete Bedingungen grob übersetzen. Mit diesen kann die Umgebung eines Matches definiert werden.

So würde der Ausdruck:

Redaktion(?=skonferenz)

in einem Text auf das Wort Redaktion matchen, wenn es in der Zeichenkette Redaktionskonferenz enthalten wäre. Würde das Wort Redaktion alleine im Text stehen, so würde hier kein Match stattfinden. Bei dieser Variante handelt es sich um einen positiven Lookahead, da überprüft wird, ob das Muster nach dem Match vorkommt.

Das Gegenteil ist ein negativer Lookahead:

Redaktion(?!skonferenz)

Bei diesem würde auf das einzelne Wort Redaktion gematcht werden, auf das Wort Redaktion in der Zeichenkette Redaktionskonferenz allerdings nicht.

Neben den Lookahead-Assertions existieren analog dazu Lookbehind-Assertions welche prüfen, ob das Muster vor dem Match vorhanden ist:

(?<=Schluss)redaktion
(?<!Schluss)redaktion

Ein komplexeres Beispiel für die Anwendung von Lookarounds könnte die Suche nach verschiedenen Schreibweisen des Wortes Hauptstraße sein. Ein regulärer Ausdruck, der Hauptstr., Hauptstraße und Hauptstrasse matcht, könnte wie folgt aussehen:

Hauptstr(aße|asse|\.)(?=\s|\b)

Hierbei sorgt die Lookahead-Assertion (?=\s|\b) dafür, dass das Match nur dann stattfindet, wenn das gesuchte Wort gefolgt von einem Whitespace (\s) oder einem Wortgrenzenzeichen (\b) steht. Diese Vorgehensweise verhindert, dass unerwünschte Matches wie Hauptstrasseneinmündung entstehen.

Flags

Neben den eigentlichen regulären Ausdrücken existieren eine Reihe von Optionen, welche das Verhalten der Engine zur Auswertung der regulären Ausdrücke anpassen.

So existiert mit der Global-Option (g) die Möglichkeit die Suche im gesamten Text durchzuführen und nicht nur bis zur ersten Übereinstimmung. Mit der Option i ignoriert die Engine die Groß- und Kleinschreibung im Rahmen der Mustererkennung.

Die Option Multiline (m) verändert das Verhalten für die Zeichen ^ und $, sodass sie nicht mehr nur den Start und das Ende des gesamten Textes markieren, sondern den Anfang und das Ende jeder einzelnen Zeile analysieren. Über die Singleline– bzw. Dotall-Direktive (s) kann der Punkt als Wildcard-Zeichen auch über die Grenzen der Zeilenumbrüche hinweg arbeiten.

Mit der Unicode-Option (u) kann bei einigen Engines die Auswertung von Unicode-Zeichen aktiviert werden.

Gesetzt werden können diese Flags als Teil des regulären Ausdrucks, so z. B. für das Global-Flag in Verbindung mit der Ignorierung der Groß- und Kleinschreibung:

(abc)/gi

Je nach verwendeter Engine werden diese Flags auch anders gesetzt, z. B. in Java beim Anlegen eines Pattern:

Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);

Daneben existieren in einigen Engines bestimmte Flags nicht, z. B. da Java bereits so konzipiert ist, dass die Engine alle Übereinstimmungen in einem gegebenen Text finden kann. Damit entspricht dieses Verhalten dem Global-Flag.

Unterschiede

Obwohl die grundlegenden Konzepte von regulären Ausdrücken über verschiedene Sprachen hinweg ähnlich sind, gibt es Unterschiede in der Syntax und Funktionalität, die Entwickler beachten müssen.

Einige Engines bieten erweiterte Funktionen wie Lookahead– und Lookbehind-Assertions, andere unterstützen benannte Gruppen. Die Unterstützung für Unicode ist ebenfalls ein Unterscheidungsmerkmal, da manche Engines in der Lage sind, mit einer Vielzahl von Zeichensätzen und Sprachen umzugehen, während andere auf ASCII beschränkt sind.

Trotz der Vielfalt unter den Engines haben sich viele Gemeinsamkeiten herausgebildet. Dazu gehören Metazeichen wie den Punkt für jedes Zeichen, den Stern für null oder mehr Wiederholungen, das Plus für eine oder mehr Wiederholungen und das Fragezeichen für null oder eine Wiederholung. Ebenso sind Zeichenklassen, Negationen, Anker für Anfang und Ende einer Zeichenkette, und einfache Quantifizierer weitgehend identisch.

Reguläre Ausdrücke in der Entwicklung

Bei der Anwendung regulärer Ausdrücke in der Entwicklung sind die jeweiligen Gegebenheiten der Programmiersprachen zu berücksichtigen. Ein Beispiel hierfür ist die Java-Methode zum Matchen in der Stringklasse:

String text = "3";
boolean matches = text.matches("[123]");

Hier wird die matches-Methode genutzt, um zu überprüfen, ob der gesamte String text dem regulären Ausdruck entspricht. Diese Herangehensweise ist für einmalige Überprüfungen einfach und direkt. Allerdings ist es ineffizient, wenn der gleiche Ausdruck in einer Schleife oder mehrfach im Code verwendet wird, da bei jedem Aufruf der Ausdruck neu kompiliert wird.

Alternativ kann diese Operation anders implementiert werden:

Pattern pattern = Pattern.compile("[123]");
boolean matches = pattern.matcher(text).matches();

Diese Variante ist effizienter, wenn der gleiche Ausdruck mehrfach genutzt werden soll. Hier wird das Pattern einmal kompiliert und kann anschließend mehrfach verwendet werden, ohne dass es jedes Mal neu kompiliert werden muss.

Reguläre Ausdrücke in Anwendungen

Neben der direkten Nutzung regulärer Ausdrücke in der Entwicklung, existieren unzählige Tools, wie Texteditoren oder Kommandozeilenwerkzeuge wie grep, welche ebenfalls Unterstützung für reguläre Ausdrücke liefern.

Für grep könnte dies wie folgt aussehen:

grep -E "G{1}N" gpl3.txt

Auch Texteditioren und IDEs enthalten Suchmethodiken, um über reguläre Ausdrücke zu finden.

Die Suche mittels regulärer Ausdrücke in IntelliJ IDEA

Damit ist es möglich, Textstellen zu finden, die komplexeren Mustern entsprechen und mit einer einfachen Suche nicht ohne weiteres gefunden werden können.

Nutzung

Reguläre Ausdrücke sind ein mächtiges Werkzeug, wenn es darum geht, Benutzereingaben zu validieren. Sie werden häufig verwendet, um sicherzustellen, dass E-Mail-Adressen, Telefonnummern und andere Formen von Daten bestimmten Mustern entsprechen.

So könnte mit einem solchen Ausdruck z. B. die Validität einer E-Mail-Adresse überprüft werden:

[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?

Daneben können sie beim Parsen von Zeichenketten genutzt werden, z. B. um spezifische Informationen aus einem Text wie einer Logdatei herauszufiltern.

In der Compiler-Konstruktion werden reguläre Ausdrücke verwendet, um Tokens zu identifizieren, indem sie Muster definieren, die Schlüsselwörter, Operatoren oder andere syntaktische Elemente erkennen. Auch bei der Textbearbeitung, wie dem massenhaften Ersetzen, können reguläre Ausdrücke ihre Stärke ausspielen.

Trotzdem sollte nicht vergessen werden, dass sich reguläre Ausdrücke nicht für alle Zwecke eignen. So sollte z. B. davon abgesehen werden, nicht formale Sprachen mit regulären Ausdrücken zu parsen. Für das Parsen von HTML mittels regulärer Ausdrücke existiert dazu ein geradezu legendärer Post bei Stack Overflow, der sich dieses Problems annimmt.

Stack Overflow beantwortet die Frage nach dem Sinn von regulären Ausdrücken in Verbindung mit HTML

Best Practices

Werden reguläre Ausdrücke genutzt, so sollten einige Best Practices berücksichtigt werden. Bei unsachgemäßer Verwendung kann ein regulärer Ausdruck schnell unübersichtlich und schwer wartbar werden kann.

So sollten genutzte reguläre Ausdrücke kommentiert werden und ihre Fachlichkeit darlegen, um das Verständnis zu erhöhen.

Anstelle von nummerierten Rückreferenzen lassen sich, wenn unterstützt, benannte Gruppen nutzen. Einige Engines erlauben auch die Verwendung von Leerzeichen und Zeilenumbrüchen in den Ausdrücken, was hilft, sie besser zu strukturieren. Zudem kann es vorteilhaft sein, lange und komplexe Muster in kleinere, wiederverwendbare Teile zu zerlegen, was die Wartung und das Verständnis des Ausdrucks erleichtert.

Es ist ratsam, in regulären Ausdrücken so spezifisch wie möglich zu sein. Anstatt generische Zeichen wie den Punkt, der jedes Zeichen repräsentieren kann, zu verwenden, sollten präzisere Zeichen oder Zeichenklassen gewählt werden. Dies hilft, unnötiges Backtracking zu vermeiden, das die Performance beeinträchtigen kann.

Obwohl Lookaround-Assertions in bestimmten Situationen sehr nützlich sein können, sollten sie mit Bedacht eingesetzt werden, da sie bei komplexen Mustern und großen Textmengen die Performance negativ beeinflussen können. Auch bei der Verwendung von Gruppierungen gilt gleiches, denn jede zusätzliche Gruppierung bedeutet mehr Aufwand bei der Verarbeitung des Ausdrucks.

Schließlich ist es unerlässlich, dass reguläre Ausdrücke gründlich getestet werden. Idealerweise sollte dies mit einer breiten Palette von Testfällen geschehen, um sicherzustellen, dass sie in jeder erwarteten Situation korrekt funktionieren.

Unter der Haube

In der alltäglichen Anwendung werden regulären Ausdrücke von einer Engine ausgeführt. Ein kurzer Blick in eine solche Engine, kann sich lohnen, da es hilft zu verstehen, wie reguläre Ausdrücke sinnvoll gestaltet werden können. Eine Engine, die diese Ausdrücke verarbeitet, kann entweder textorientiert oder regex-orientiert sein.

Eine textorientierte Engine, implementiert als deterministischer endlicher Automat (DFA), analysiert den Eingabetext sequenziell. Diese Methode ist schnell und effizient, da sie keine alternativen Pfade verfolgt und somit kein Backtracking benötigt. DFAs liefern stets die längste Übereinstimmung und sind aufgrund ihrer deterministischen Natur in der Performance vorhersehbar.

Im Gegensatz dazu steht die regex-orientierte Engine, die auf einem nichtdeterministischen endlichen Automaten (NFA) basiert. Eine solche Engine ist in der Lage, mehrere Pfade gleichzeitig zu verfolgen und bei Bedarf mittels Backtracking alternative Wege zu untersuchen. Dies ermöglicht eine flexible Mustererkennung, kann jedoch bei komplexen Ausdrücken zu einer erhöhten Rechenlast führen. NFAs priorisieren die am weitesten links stehende Übereinstimmung und können bei mehreren möglichen Matches zu einer kürzeren Übereinstimmung führen, selbst wenn weiter rechts im Text eine längere vorhanden wäre.

Moderne Regex-Engines sind meist regex-orientiert und nutzen einen Preprozessor, um den regulären Ausdruck vorzuverarbeiten, etwa um Makros in Zeichenklassen umzuwandeln. Anschließend wird der Ausdruck kompiliert, wobei er in eine effiziente Form überführt wird, die entweder als Reihe von Instruktionen oder als Zustandsautomat von der Engine verarbeitet werden kann.

Die Wahl der Engine hängt von den spezifischen Anforderungen der Aufgabe ab. Während DFAs für einfache, vorhersehbare Suchvorgänge geeignet sind, bieten NFAs die notwendige Flexibilität für komplexere Mustererkennungen.

Risiken und Nebenwirkungen

Neben der Möglichkeit reguläre Ausdrücke für Aufgaben zu nutzen, für die sie nicht geeignet sind, existieren auch andere Probleme, die mit diesen Ausdrücken zusammenhängen.

So gibt es den Regular expression Denial of Service
-Angriff (ReDoS), welcher ausnutzt, dass viele Engines für reguläre Ausdrücke bei bestimmten Ausdrücken extrem langsam werden und viele Systemressourcen beanspruchen können.

Ein schönes Beispiel für eine solche Anfälligkeit, war der Ausfall von Stack Overflow im Jahre 2016. Dieser wurde durch einen Post mit zu vielen Leerzeichen verursacht, welcher dazu führte, dass die auf Backtracking basierte Engine über 199 Millionen Überprüfungen durchführen musste.

Allerdings gibt es Alternativen zu Backtracking nutzenden Engines, wie RE2 von Google. Diese Engine garantiert, basierend auf einem endlichen Automaten, eine lineare Ausführungszeit, bezogen auf die Eingabedaten und ist trotzdem mit den Features moderner Engines ausgestattet.

Werkzeuge

Für die Nutzung und Erstellung von regulären Ausdrücken existieren hilfreiche Werkzeugen. Zu diesen Werkzeugen gehören eine Reihe von Online-Testern. Dies sind interaktive Werkzeuge, die es dem Nutzer ermöglicht, regulären Ausdrücke in Echtzeit zu testen und zu debuggen. Diese Werkzeuge bieten oft farblich hervorgehobene Übereinstimmungen und Erklärungen für jedes Element des Ausdrucks.

Einer dieser Tester ist RegExr, welcher unter regexr.com zu finden ist.

Mit RegExr können reguläre Ausdrücke schnell ausprobiert werden

Neben der interaktiven Oberfläche bietet RegExr eine Referenz und eine große Anzahl von Community Patterns, die viele Probleme bereits abdecken und so die Entwicklung eines eigenen Ausdrucks beschleunigen können.

Ein weiterer Tester für reguläre Ausdrücke ist regular expressions 101, welcher unter regex101.com zu finden ist.

regular expressions 101 verfügt über einen Debugger

Eine Besonderheit dieses Dienstes ist der integrierte Debugger, mit welchem regulären Ausdrücke analysiert werden können. Einen umgekehrten Weg geht der Regex Generator von Olaf Neumann.

Der Regex Generator von Olaf Neumann

Mithilfe dieses Werkzeuges können reguläre Ausdrücke anhand eines Datenbeispieles erzeugt werden. Kommandozeilenwerkzeuge wie rgxg arbeiten nach ähnlichen Prinzipien und können auch offline genutzt werden.

Fazit

Insgesamt sind reguläre Ausdrücke ein mächtiges Werkzeug, um Textmuster zu durchsuchen und zu manipulieren. Sie ermöglichen eine effiziente Verarbeitung von Texten und sind daher ein wichtiges Werkzeug für Entwickler und Anwender. In der Praxis werden sie in verschiedenen Bereichen eingesetzt, um Texte zu durchsuchen, zu filtern und zu manipulieren.

Durch die Einhaltung von Best Practices und dem damit verbundenen Vermeiden häufiger Fehler können Entwickler sicherstellen, dass ihre regulären Ausdrücke sowohl leistungsfähig als auch wartbar sind.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.

Self Healing Code

In der Softwareentwicklung stellt sich manchmal das Gefühl ein, von Buzzwörtern umgeben zu sein. Auch Self Healing Code könnte ein solches sein. Doch trägt das Konzept einige interessante Eigenschaften mit sich und sollte nicht vorschnell verworfen werden.

Die grobe Idee hinter diesem Konzept ist es, dass die Anwendung Fehler erkennen und diese im Idealfall auch beheben kann. Dieser Prozess soll ohne menschliches Eingreifen stattfinden.

Neben der einzelnen Applikation kann, sich ein solches Verhalten auf komplexere Systeme und deren Zusammenspiel beziehen. Neben der Fehlerbehebung, während der Laufzeit einer solchen Software, wird der Begriff des Self Healing Code in letzter Zeit auch im Zusammenhang mit generativer KI genutzt.

Definition

Im Bereich der Softwareentwicklung ist Self Healing Code so definiert, dass ein Programm in der Lage ist, Fehler zu erkennen und zu korrigieren. Verwandt damit ist der Begriff der selbstheilenden Systeme, denen die Fähigkeit inhärent ist, aus einem defekten Zustand wieder in einen funktionalen Zustand zu wechseln.

Ein einfaches Model von Self Healing Code

Das Ziel des selbstheilenden Codes ist es, die Notwendigkeit menschlicher Eingriffe zu minimieren und die Betriebszeit und Effizienz der Software zu maximieren. Dies kann durch die Implementierung von Überwachungs- und Diagnosefunktionen erreicht werden, die auf Anomalien oder Fehler hin überprüfen. Sobald ein Problem erkannt wird, wird versucht darauf zu reagieren, indem entweder eine Korrekturmaßnahme ausgeführt oder auf einen vorherigen stabilen Zustand zurückkehrt wird.

Defensive Programmierung

Eng verwandt mit Self Healing Code ist defensive Programmierung. Beides sind Praktiken, die dazu dienen, die Robustheit und Zuverlässigkeit von Software zu verbessern, aber sie tun dies auf unterschiedliche Weisen.

Defensive Programmierung ist eine Methode, bei der der Entwickler davon ausgeht, dass Probleme auftreten werden und daher Vorkehrungen trifft, um diese zu bewältigen. Dies kann beinhalten, dass überprüft wird, ob Eingaben gültig sind, bevor sie verwendet werden, Ausnahmen ordnungsgemäß behandelt werden und der Code so geschrieben wird, dass er leicht zu verstehen und zu warten ist.

Das Ziel der defensiven Programmierung ist es, die Anzahl der Fehler zu reduzieren und sicherzustellen, dass die Anwendung auch bei unerwarteten Eingaben oder Bedingungen korrekt funktioniert.

Selbstheilender Code hingegen geht einen Schritt weiter. Anstatt nur zu versuchen, Fehler zu vermeiden, versucht er, Fehler zu erkennen und zu beheben, wenn sie auftreten.

Bei der Betrachtung von Self Healing Code sollte auch defensive Programmierung Berücksichtigung finden. Diese kann dazu beitragen, die Anzahl der Fehler zu reduzieren, die auftreten können. Self Healing Code kann anschließend dazu beitragen, die Auswirkungen der Fehler zu minimieren, die trotzdem auftraten.

Implementation

Natürlich muss die gewünschte Funktionsweise der Selbstheilung bei der Entwicklung und dem Design einer Applikation und entsprechender Systeme berücksichtigt und implementiert werden.

Der erste Schritt ist die Fehlererkennung. Das bedeutet, dass die Applikation in der Lage sein muss, eventuelle Fehler und Probleme selbstständig zu erkennen. Hier können Applikationslogiken, Logs oder auch Überwachungssysteme genutzt werden.

Wird eine Anwendung oder ein Teil eines Softwaresystems überwacht, so müssen Schwellwerte definiert werden, welche definieren, ab wann die Services sich in einem kritischen Zustand befinden, damit darauf basierend Maßnahmen ergriffen werden können.

Präventiv und reaktiv

Die Fähigkeit der Selbstheilung kann in präventives und reaktives Handeln unterschieden werden. Beim präventiven Handeln werden gewisse Schlüsselindikatoren von der Applikation ausgewertet und darauf basierend eine Handlung ausgelöst.

So könnte eine Server-Applikation keine neuen Verbindungen mehr zulassen, wenn die CPU-Auslastung auf dem eigenen System zu hoch ist und somit einer Überlastung vorbeugen.

Reaktives Handeln ist vonseiten der Applikation immer dann notwendig, nachdem es zu einem Fehler gekommen ist. In diesem Fall muss die Applikation reagieren, um wieder einen funktionsfähigen Ablauf herstellen zu können.

Fehlerbeseitigung

Wurde ein Fehler erkannt, sollte er im nächsten Schritt behoben werden. Hier sind unterschiedliche Möglichkeiten denkbar, wie der Neustart eines Services oder das Ausweichen auf andere Datenquellen.

Dieser Prozess der Erkennung und Beseitigung von Fehlern und Problemen sollte ebenfalls automatisiert sein, sodass er ohne menschliche Einwirkung auskommt. Wird der Mechanismus aktiv, sollte ein Logging vorgenommen werden, damit dies später nachvollzogen werden kann.

Test und Dokumentation

Neben der Implementierung sollten selbstheilende Funktionalitäten auch regelmäßig getestet und gut dokumentiert werden, um bei Bedarf eine schnelle und effiziente Fehleranalyse zu ermöglichen.

Auch auf Sicherheitsaspekte sollte achtgegeben werden. Es sollte sichergestellt werden, dass die Maßnahmen zur Selbstheilung nicht von außen manipuliert oder missbraucht werden können.

Ein einfaches Beispiel

Wie könnte die Anwendung dieses Konzeptes aussehen? Ein heruntergebrochenes Beispiel unter Java könnte sich wie folgt darstellen:

public Connection connectToDatabase() {

    Connection connection = null;

    while (connection == null) {
        
        try {
            connection = DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (SQLException e) {

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // Ignore
            }
        }
    }

    return connection;
}

In der Methode connectToDatabase soll eine Datenbankverbindung erstellt und diese anschließend zurückgegeben werden. Tritt beim Aufbau der Verbindung eine Ausnahme auf, so wird versucht nach einer Wartezeit nochmals eine Verbindung aufzubauen, in der Hoffnung, dass der Fehler nur temporärer Natur war.

Damit wird dem Nutzer eine Alternative zu einem völligen Abbruch des Verbindungsversuches geboten. Im Idealfall, auch bei Auftreten eines Fehlers, wird ein verzögerter Aufbau der Verbindung ermöglicht. In der Praxis sollte dieses Beispiel allerdings in der vereinfachten Form nicht genutzt werden, da die Methode connectToDatabase niemals eine Antwort liefern würde, wenn die Datenbank nicht mehr antwortet. Hier ist es nötig, nach einer gewissen Zeit oder einer bestimmten Anzahl an Versuchen abzubrechen.

Methodiken und Pattern

Für die Bereitstellungen selbstheilenden Codes können unterschiedlichste Methodiken innerhalb einer Applikation genutzt werden, um dieses Konzept zum Erfolg zu führen.

Dies führt vom Einsatz von Entwurfsmustern wie dem Circut Breaker über die Möglichkeit zum Failover, den Neustart fehlerhafter Komponenten, oder die Nutzung von Read-Only-Mechanismen, bei verschlechternder Servicequalität.

Circut Breaker

Der Circuit Breaker ist ein Entwurfsmuster, welches verwendet wird, um Systeme zu schützen. Die Benamung ist nicht ohne Grund so gewählt, da er wie eine Sicherung die Verbindung zu einem System kappt, wenn bestimmte Kriterien erfüllt sind.

Dies kann z. B. eine definierte Fehlerrate in einem bestimmten Zeitraum sein. Ein Beispiel wäre ein Webservice, welcher wiederholt auf Anfragen nicht antwortet. Der Aufrufer könnte nun versuchen immer und immer wieder Anfragen zu senden, was dazu führen kann, dass das System im schlimmsten Fall unter der Last zusammenbricht.

Hier greift der Circut Breaker ein und unterbricht die Verbindung. Indessen kann in der Anwendung auf den Fehler reagiert werden, die Anfrage z. B. zu einem späteren Zeitpunkt wiederholt werden.

Meist wird nach einer Cooldown-Phase die Verbindung zum Service wieder aufgenommen. Treten hierbei wieder Fehler auf, so wird der Circut Breaker die Verbindung erneut trennen und der Prozess beginnt von vorn.

Auch in der Applikation selbst führt dies zu positiven Effekten, da nicht mehr auf die entsprechende Verbindung gewartet werden muss und eventuell dafür genutzte Threads und weitere Ressourcen für diesen Moment abgewickelt werden können.

Failover

Eine weitere Möglichkeit für Self Healing Code ist die Implementierung von Failover-Verfahren unter der Bereitstellung von Redundanz. Grundsätzlich bedeutet dies, dass auf andere Systeme umgeschaltet wird, wenn das angefragte System ausfällt.

Dies kann bedeuten, dass im Falle eines nicht oder fehlerhaft antwortenden Webservices, eine andere Instanz des Webservices genutzt wird.

Ein Rückfall auf einen alternativen Payment-Provider sichert den Geschäftsprozess ab

Daneben sind auch andere Szenarien denkbar. So konnte die eigene Applikation einen Payment-Provider nutzen. Bei einem Ausfall könnte dies ein geschäftskritisches Problem darstellen. Als Failover-Variante kann auf einen zweiten unabhängigen Payment-Provider umgeschwenkt werden, bis der primäre Provider wieder verfügbar ist.

Ziel ist es beim Failover, die Ausfallzeiten zu minimieren und die Verfügbarkeit zu gewährleisten. Aus Sicht eines Nutzers würde ein solcher Ausfall eines Service zu keinem veränderten Ergebnis führen.

Änderung in der Applikationslogik

Eine weitere Möglichkeit innerhalb einer Applikation auf Probleme zu reagieren, ist es Änderung in der eigentlichen Logik vorzunehmen. Beispielhaft könnte von einem externen Webservice eine Route anhand gewisser Parameter geliefert werden.

Fällt dieser Service aus, könnte die Anwendung stattdessen intern eine Route mit einem vereinfachten Algorithmus berechnen. Das Ergebnis ist qualitativ nicht unbedingt mit dem des externen Service zu vergleichen, allerdings kann es aus Sicht des Nutzers trotzdem ausreichend sein, falls der Webservice nicht zur Verfügung steht.

Limiter

Eine weitere Klasse nützlicher Methodiken sind Limiter. So begrenzt ein Rate Limiter die Anzahl der Anfragen, die ein Nutzer in einem bestimmten Zeitraum senden kann. Dies ist besonders nützlich in Szenarien, in denen Systemressourcen begrenzt sind oder um versehentliche Denial-of-Service-Angriffe zu verhindern.

Daneben existieren weitere Limiter, wie der Time Limiter welcher die Zeit begrenzt, die ein bestimmter Prozess zur Ausführung nutzen kann. Wenn der Prozess die zugewiesene Zeit überschreitet, wird er abgebrochen oder eine Ausnahme wird ausgelöst. Dies kann eine Möglichkeit darstellen, Timeouts zu realisieren.

Let It Crash

Aus einer übergeordneten Sicht kann es sinnvoll sein, Services abstürzen zu lassen, wenn es zu schwerwiegenden Problemen kommt. Je nachdem, wie das System gestaltet ist, wird der Service anschließend wieder gestartet und hochgefahren.

Erlang ist ein anschauliches Beispiel für diese Let It Crash-Philosophie. Sie führt dazu, dass sobald ein Prozess auf einen Fehler stößt, dieser beendet wird, anstatt den Fehler zu beheben. Andere, überwachende Prozesse können anschließend entscheiden, wie sie auf den Absturz reagieren, oft indem sie den fehlerhaften Prozess neu starten.

Hier gibt es im Service selbst keinen selbstheilenden Code, sondern es wird sich auf die Gesamtarchitektur des Systems verlassen, welche dafür sorgt, dass der Dienst wieder neu gestartet wird oder das Problem auf andere Art und Weise behoben wird.

Hier muss darauf geachtet werden, dass die Applikationen auf diesen Fall vorbereitet sein müssen. So müssen z. B. Verbindungen wieder aufgenommen werden, nachdem der jeweilige Service wieder verfügbar ist.

Caches

Auch Caches können im Rahmen selbstheilender Anwendungen nützlich sein. Wenn eine Information nicht vom externen Service bezogen werden kann, kann unter Umständen die letzte gecachte Antwort für die Anforderung genutzt werden.

Dies ist natürlich nur in solchen Fällen möglich, in denen der Cache für die gewünschte Anfrage vorhanden ist und sichergestellt werden kann, dass die gespeicherte Antwort den Anforderungen an die benötigte Aktualität gerecht wird.

Veränderung der Servicequalität

Wer Dienste entwickelt und betreibt, kann weitere Funktionalitäten implementieren, um zumindest ein Teil eines Dienstes noch funktionsfähig zu halten. So kann ein entsprechender Service z. B. in einen Read-Only-Modus gesetzt werden, wenn Schreibzugriffe aufgrund eines Fehlers aktuell nicht funktionieren.

Da in den meisten Fällen Lesezugriffe einem Schreibzugriff überwiegen, können in einem solchen Read-Only-Modus viele der Anfragen immer noch erfolgreich beantwortet werden.

Das dahinter liegende Konzept ist es, die Funktionalität, welche noch zur Verfügung steht, dem Aufrufer zur Verfügung zu stellen, anstatt den Betrieb komplett einzustellen. So können einzelne Features im Fehlerfall abgeschaltet werden, anstatt den kompletten Service zu deaktivieren.

Allerdings müssen die aufrufenden Applikationen hierauf vorbereitet sein und damit umgehen können.

Retry

Wie bereits im Beispiel oben demonstriert, können wiederholte Versuche einen Service aufzurufen, eine Möglichkeit sein, Systeme fehlertolerant und selbstheilend zu gestalten. Dies ist vorwiegend bei Problemen temporärer Natur wie kurzzeitigen Netzwerkausfällen oder einer Überlastung nützlich.

Dieses Muster kann jedoch komplexer werden, abhängig von den Anforderungen der Anwendung. Es kann unter anderem notwendig sein, die Wartezeit zwischen den Wiederholungsversuchen zu erhöhen oder bestimmte Arten von Fehlern von den Wiederholungsversuchen auszuschließen.

Auch zu häufige Wiederholungen sollten ausgeschlossen werden, damit z. B. im Falle einer Überlastung der aufgerufene Service nicht weiter belastet wird.

Timeouts

Ein weiterer wichtiger Punkt bei selbstheilendem Code und fehlertoleranten Architekturen sollten Timeouts sein. Operationen wie Netzwerk oder IO sollten immer mit einem Timeout versehen werden, damit niemals der Fall entsteht, dass auf unbestimmte Zeit auf Ressourcen gewartet wird.

Ähnliche Verfahren lassen sich in der Theorie auch anwenden, wenn längere Berechnungen getätigt werden. In einigen Fällen ist es hier sinnvoll, einen Timeout zu definieren.

Grundsätzlich sollte es immer das Ziel sein, endlose Warteschleifen zu vermeiden und Systemressourcen wie CPU oder Speicher wieder freizugeben. Auch erhält die Anwendung durch Timeouts eine gewisse Art an Kontrolle, da durch diese klar wird, wie lange bestimmte Prozesse maximal laufen dürfen.

Aus Sicht des Nutzers sind Timeouts hilfreich, da dieser nicht unnötige Wartezeiten in Kauf nehmen muss und eine zeitnahe Rückmeldung erhält, wenn auch im schlechtesten Fall in Form einer Fehlermeldung.

Ein interessanter Nebeneffekt ist, dass Timeouts teilweise zur Fehlersuche genutzt werden können. Wenn bestimmte Anforderungen ständig zu Timeouts führen, könnte dies auf ein tiefer liegendes Problem hinweisen, das analysiert werden sollte.

Tooling

Viele der beschriebenen Mechanismen können von Grund auf vom Entwickler implementiert werden. Allerdings existieren eine Reihe von Bibliotheken und Frameworks welche einen Teil dieser Arbeit abnehmen.

In der Java-Welt liefern Frameworks z. B. Spring Boot, Möglichkeiten für eine robuste Fehlerbehandlung. Bibliotheken, wie Resilience4j, bieten Lösungen für Selbstheilungsfunktionen und Fehlertoleranz. Sie ermöglicht es Entwicklern, selbstheilende Muster zu implementieren, wie Circut Breaker oder Fallback-Mechanismen, um Ausfälle effektiv zu behandeln.

Damit wird es einfacher und bequemer, selbstheilenden Code zu implementieren.

Self Healing Code in der Zukunft

Neben den klassischen Methoden, um selbstheilenden Code zu realisieren, werden in letzter Zeit immer mehr Varianten von Self Healing Code in Verbindung mit generativer KI postuliert, wie dem Large Language Model GPT-4.

Während die bisherigen Beispiele selbstheilender Systeme auf die Laufzeit abzielten, existieren auch Verfahren und Ideen, generative KI zu Nutzung bei der Entwicklung einzusetzen, um Quellcode „ohne Mitwirkung des Entwicklers“ zu realisieren.

Mittelfristig sind Systeme im breiten Einsatz denkbar, welche die Codebasis eines Projektes analysieren und basierend darauf Änderungen generieren, welche anhand von Pull-Requests dem menschlichen Entwickler vorgeschlagen werden können.

Ein Workflow zur Erzeugung automatisierter Änderungen via LLM

Damit bei den automatisiert erstellten Änderungen möglichst sichergestellt wird, dass sie auch funktionieren, können die Änderungen durch eine Continuous Integration-Pipeline entsprechenden Tests unterworfen werden. Nur wenn die Pipeline erfolgreich durchläuft, werden die Änderungen dem Entwickler vorgeschlagen.

So nutzt Microsoft mit InferFix ein System zur semiautomatischen Fehlerbehebung, um den Arbeitsablauf für interne Projekte zu verbessern. Auch andere Firmen, wie Stackoverflow, denken ebenfalls über die Nutzung von LLMs und generativer KI im Rahmen der Softwareentwicklung nach.

Automatisiertes Debugging

Solche „selbstheilenden Fähigkeiten“ können auch für die komplett automatische Fehlerbehebung genutzt werden. So existiert mit Wolverine ein Proof of Concept für ein solches System. Wolverine nutzt GPT-4 von OpenAI, um Fehler in einem Python-Skript zu reparieren.

Dabei werden Fehler im Skript in das Sprachmodell gegeben und anschließend die Lösung auf den Quelltext angewendet. Danach wird ermittelt, ob das Skript nach der Änderung funktioniert. Treten erneut Fehler auf, werden diese wieder an das LLM übermittelt und dessen Lösung wieder in das Skript übernommen.

Solche Verfahren könnten weitergedacht und direkt beim Nutzer, im Falle eines Fehlers, ausgeführt werden.

Auch in IDEs ziehen Plugins basierend auf generativer KI ein, wie das AI Assistant-Plugin von Jetbrains. Mit diesem können Fehlermeldungen und Codeteile analysiert und erklärt und eine weitere Interaktion mit den Sprachmodellen durchgeführt werden. Dazu zählen unter anderem das Generieren von Dokumentation, sowie von Namen.

Probleme

Im Idealfall können solche Systeme eine Arbeitserleichterung sein, allerdings führen sich auch zu Problemen. So ist nicht sichergestellt, dass die von der KI gefundenen Lösungen wirklich die geforderten fachlichen Spezifikationen erfüllen. Daneben kann sich die Codequalität verschlechtern, wenn solche Änderung ungeprüft übernommen werden.

Hier könnte es im Laufe der Zeit vorkommen, dass aus Bequemlichkeit dazu übergegangen wird, solche Änderungen automatisiert auf den Quellcode anzuwenden.

Auf lange Sicht kann dies dazu führen, dass die eigene Codebasis immer schlechter verstanden wird, wenn diese generativer KI „gepflegt“ wird und diese Änderungen ohne ein sinnvollen Reviewprozess übernommen werden. Es ist denkbar, dass sich die Verantwortlichkeit von der eigentlichen Implementation des Quellcodes immer mehr in Richtung des Reviews verschiebt.

Wenn es an den Einsatz generativer KI geht, müssen neben solchen Fragen auch datenschutzrechtliche und sicherheitstechnische Aspekte bedacht werden.

Auch sollte beachtet werden, dass generative KI, wie die meistgenutzten Modelle von OpenAI Kosten verursachen, welche meist je Token abgerechnet werden. Werden lokale Modelle genutzt, muss stattdessen Rechenleistung und dahinterstehende Infrastruktur bereitgestellt werden.

Daneben existieren Größenbeschränkungen. Modelle wie solche von OpenAI sind bezüglich der maximal verarbeitbaren Token beschränkt, sodass größere Quelltext auf diese Art und Weise nur schwer am Stück analysiert werden können.

In Bezug auf selbstheilenden Code und Systeme kann ein blindes Vertrauen zu erheblichen Problemen führen.

Beispielhaft könnte ein System nur bestimmte Datentypen verarbeiten. Wenn ein solches Datenfeld vom Typ Integer ist und der Nutzer nun stattdessen Zeichenketten sendet, würde die Anwendung dies ablehnen. In einem solchen Fall könnte ein auf KI basierendes System zur Behebung dieses Fehlers den Typ der Schnittstelle, so ändern, dass auch Zeichenketten erlaubt sind und somit weiteren Problemen die Tür öffnen.

Fazit

Self Healing Code bietet eine Reihe von Vorteilen, im Betrieb der Anwendungen und sollte beim Design entsprechender Applikationen und Systeme berücksichtigt werden.

So sind diese Systeme zuverlässiger, bieten erhöhte Verfügbarkeit, und verringern eventuelle Downtimes. Auch in der Wartung können solche Systeme günstiger sein.

Allerdings sollten die Schwierigkeiten bedacht werden. Die Entwicklung von selbstheilendem Code kann komplex sein. Es kann eine Herausforderung darstellen, effektive Mechanismen zur Fehlererkennung, Diagnosealgorithmen und Strategien zur Fehlerbehebung zu entwickeln, die reibungslos miteinander interagieren.

Daneben bedeutet Self Healing Code in vielen Fällen auch ein Overhead. Unter Umständen werden zusätzliche Systemressourcen, für die Erkennung und die Beseitigung von Problemen, benötigt.

Wenn fälschlicherweise Fehler erkannt werden und die selbstheilenden Mechanismen aktiv werden, kann dies problematisch sein. Dazu gehören die Schwierigkeit, solche Systeme zu testen und zu überprüfen, und die Möglichkeit, dass das System unvorhersehbare oder unerwünschte Änderungen vornimmt.

Neben dem klassischen selbstheilenden Code hält generative KI immer mehr Einzug in unseren Alltag und dies wird auch in Verbindung mit Self Healing Code keine Ausnahme sein. Allerdings sollte hier Vorsicht geboten sein und der Mensch nicht aus dem Loop genommen werden.

Self Healing Code hat das Potenzial, die Zuverlässigkeit und Verfügbarkeit von Systemen zu verbessern und dieses Potenzial sollte nicht brach liegen gelassen werden.

jQuery Mobile und jQuery UI sind tot

Die bekannte JavaScript-Bibliothek jQuery, welche unter anderem zur Manipulation des DOMs genutzt wird, wird von zwei Bibliotheken flankiert, welche sich dem UI annehmen. Die Rede ist von jQuery Mobile und jQuery UI. Mithilfe dieser Bibliotheken ist es möglich Oberflächen für Webanwendungen zu entwickeln.

Eine mittels jQuery UI entwickelte Webapplikation

Während sich die Bibliotheken weiterhin sehr beliebt sind, was sich unter anderen in den Fragen auf Stack Overflow widerspiegelt, gibt es ein Problem mit den Projekten; sie sind praktisch tot. Der letzte Commit für jQuery UI ist vom Mai 2017 und auch die Entwicklung der letzten Jahre von jQuery Mobile ist sehr übersichtlich. Ansonsten sind viele andere Zeichen zu entdecken das die beiden Projekte tot sind.

Für neue Projekte in der Webentwicklung, sollte sich deshalb nach einem anderen Framework für die Erstellung von Weboberflächen umgesehen werden. Eine Alternative stellt unter anderem Framework 7 dar, welches von der Einfachheit der jQuery UI– und jQuery Mobile-Bibliotheken inspiriert ist.

Per Javascript Parameter per POST an ein PHP Skript schicken

Wenn man in Javascript einem PHP Skript etwas schicken möchte so kann man dies per GET Methode machen. Das bedeutet das die Parameter an die URL des PHP Skriptes angehangen werden. Für größere Datenmengen ist die Methode POST allerdings wesentlich sinnvoller. Das Problem ist das man die Daten dann mittels eines Formulars senden muss. Das macht nicht wirklich Spaß. Einfacher geht es mit der Methode postToUrl welche ich auf Stack Overflow gefunden habe:

function postToUrl(path, params, method)
{
 method = method || "post"; // Standardmethode wird auf POST gesetzt, wenn keine andere angegeben

 var form = document.createElement("form");
 form.setAttribute("method", method);
 form.setAttribute("action", path);

 for(var key in params) {
 var hiddenField = document.createElement("input");
 hiddenField.setAttribute("type", "hidden");
 hiddenField.setAttribute("name", key);
 hiddenField.setAttribute("value", params[key]);

 form.appendChild(hiddenField);
 }

 document.body.appendChild(form);
 form.submit();
}

Die Funktion funktioniert dabei auch im allseits beliebten Internet Explorer ;)

Update: Mit dem Internet Explorer 8 macht das ganze Probleme. Die umgeschriebene Funktion mit der es in allen Browsern funktionieren sollte sieht dann so aus:

function postToUrl(path, params, method) 
{
 method = method || "post"; // Set method to post by default, if not specified.

 // The rest of this code assumes you are not using a library.
 // It can be made less wordy if you use one.
 //var form = document.createElement("select");
 var form = document.createElement("form");
 form.setAttribute("method", method);
 form.setAttribute("action", path);

 for(i=0; i<params.length; i++)
 {
 var key=i;
 
 var hiddenField = document.createElement("input");
 hiddenField.setAttribute("type", "hidden");
 hiddenField.setAttribute("name", key);
 hiddenField.setAttribute("value", params[key]);

 form.appendChild(hiddenField);
 }

 document.body.appendChild(form);    // Not entirely sure if this is necessary
 form.submit();
 return false;
}

Weitere Informationen gibt es unter:
http://stackoverflow.com/questions/133925/javascript-post-request-like-a-form-submit