Grundlagen

Formale Sprachen und Automaten

Hinter jedem regulären Ausdruck steckt eine winzige Maschine ohne Gedächtnis außer ihrem aktuellen Zustand. Was sie kann, was sie grundsätzlich nicht kann – und warum ein harmlos aussehendes Regex eine CPU minutenlang beschäftigen kann.

Lesestand

Eine formale Sprache ist einfach eine Menge von Zeichenketten – etwa „alle gültigen Ganzzahlen“ oder „alle Binärzahlen, die durch 3 teilbar sind“. Die Frage lautet dann: Gibt es eine Maschine, die für jede Eingabe entscheidet, ob sie dazugehört?

Die einfachste solche Maschine ist der endliche Automat (DFA): eine feste Zahl von Zuständen, ein Zeichen nach dem anderen, kein weiterer Speicher. Er kann genau die Sprachen erkennen, die sich mit regulären Ausdrücken beschreiben lassen. Was er nicht kann: unbegrenzt zählen – deshalb lassen sich verschachtelte Klammern, HTML oder JSON nicht mit einem Regex prüfen. Für mehr braucht man mehr Speicher; am Ende dieser Leiter steht die Turingmaschine.

Alphabet, Wort, Sprache

Die Theorie beginnt mit drei Begriffen. Ein Alphabet ist eine endliche Menge von Zeichen, etwa {0, 1} oder {a, b}. Ein Wort ist eine endliche Folge davon – auch das leere Wort ε zählt. Eine Sprache ist eine beliebige Menge von Wörtern, meist unendlich groß.

Das klingt abstrakt, deckt aber fast alles ab: „Alle syntaktisch korrekten Java-Programme“ ist eine Sprache über dem Unicode-Alphabet. „Alle E-Mail-Adressen“ auch. Und jede Ja/Nein-Frage an einen Computer lässt sich so formulieren: Gehört diese Eingabe zur Sprache der Eingaben, bei denen die Antwort „ja“ ist?

Der endliche Automat

Ein deterministischer endlicher Automat (DFA) besteht aus Zuständen, einem Startzustand, einigen akzeptierenden Zuständen und einer Übergangstabelle: Für jeden Zustand und jedes Zeichen steht fest, wohin es geht. Er liest die Eingabe einmal von links nach rechts – und am Ende entscheidet allein der Zustand, in dem er steht. Probier es aus:

Drei Automaten zum Durchklicken

Das Bemerkenswerte am dritten Beispiel: Der Automat prüft die Teilbarkeit beliebig langer Binärzahlen mit nur drei Zuständen – er merkt sich nur den Rest. Jedes neue Bit verdoppelt die Zahl und addiert 0 oder 1, also wird aus Rest r der Rest (2r + Bit) mod 3. Mehr muss er nicht wissen.

Regex und Automat sind dasselbe

Ein zentraler Satz der Theorie (nach Stephen Kleene, 1956): Zu jedem regulären Ausdruck gibt es einen endlichen Automaten, der genau dieselbe Sprache erkennt – und umgekehrt. Ein regulärer Ausdruck ist die Beschreibung, der Automat die Maschine. Regex-Bibliotheken übersetzen den Ausdruck in einen Automaten, oft zuerst in einen nichtdeterministischen (NFA), der an einer Stelle mehrere Wege offenhalten darf. Ein NFA kann nicht mehr als ein DFA, ist aber oft viel kleiner; man kann ihn in einen DFA umrechnen, der dann allerdings im schlimmsten Fall exponentiell viele Zustände hat.

Streng genommen sind die Ausdrücke in Java, Perl oder JavaScript mehr als „regulär“: Rückverweise wie (a+)\1 gehen über das hinaus, was ein endlicher Automat kann. Der Preis dafür kommt weiter unten.

Was ein Automat nicht kann

Ein Automat hat keinen Speicher außer seinem Zustand – und davon nur endlich viele. Deshalb kann er nicht beliebig weit zählen. Das klassische Beispiel sind korrekt verschachtelte Klammern: Um zu prüfen, ob auf n öffnende auch n schließende folgen, müsste er sich n merken. Stell ein, wie viele Zustände der Automat hat und wie tief verschachtelt wird:

Klammern zählen mit endlich vielen Zuständen
Zustände des Automaten
Verschachtelungstiefe

Daraus folgt eine Regel für die Praxis: Verschachtelte Strukturen – HTML, JSON, XML, Programmcode, arithmetische Ausdrücke – lassen sich nicht zuverlässig mit einem Regex prüfen. Man braucht einen Parser. Ein Regex taugt dafür, die einzelnen Bausteine (Zahlen, Bezeichner, Zeichenketten) zu erkennen – genau so arbeitet ein Compiler: Der Lexer ist ein Automat, der Parser darüber kann verschachteln.

Die Chomsky-Hierarchie

Gibt man dem Automaten mehr Speicher, kann er mehr Sprachen erkennen. Noam Chomsky ordnete die Sprachklassen 1956 in eine Hierarchie, die bis heute den Bau von Compilern prägt:

SprachklasseMaschineSpeicherBeispiel
regulärendlicher Automatnur der ZustandZahlen, Bezeichner, einfache Muster
kontextfreiKellerautomatein StapelKlammern, Syntax von Programmiersprachen
kontextsensitivlinear beschränkte MaschineBand, so lang wie die Eingabeanbncn
rekursiv aufzählbarTuringmaschineunbegrenztes Bandalles, was überhaupt berechenbar ist

Ein Stapel reicht für Klammern: bei „(“ drauflegen, bei „)“ herunternehmen, am Ende muss er leer sein. Genau das macht jeder Parser – und jeder Entwickler, der die Klammerprüfung in einer Bewerbung programmieren soll. Die letzte Zeile ist das Thema der nächsten Seite.

Wenn ein Regex hängt

Ein echter DFA liest jedes Zeichen genau einmal – seine Laufzeit ist linear, egal wie der Ausdruck aussieht. Die Regex-Engines von Java, JavaScript, Python und Perl arbeiten aber anders: Sie probieren Möglichkeiten nacheinander aus und gehen bei einem Fehlschlag zurück (Backtracking). Das macht Rückverweise möglich, kann aber bei ungünstigen Mustern explodieren. Das Muster (a+)+b auf eine Folge von n mal „a“ ohne „b“ am Ende:

Linear gegen exponentiell
Länge der Eingabe

Modellhaft: Schritte einer einfachen Backtracking-Engine, gezählt als Versuche; Zeit bei 100 Millionen Schritten pro Sekunde. Reale Engines haben einige Optimierungen, das Grundproblem bleibt.

Das Problem entsteht durch verschachtelte Wiederholungen, die dieselben Zeichen auf viele Arten aufteilen können: n Zeichen lassen sich auf 2n−1 Arten in Gruppen zerlegen, und die Engine probiert jede, bevor sie aufgibt. Weil das auch mit Absicht ausgelöst werden kann, gilt es als Sicherheitsthema (ReDoS). Abhilfe: Muster ohne verschachtelte Quantoren schreiben, possessive Quantoren oder atomare Gruppen nutzen, Eingabelängen begrenzen – oder eine Engine mit garantierter Linearzeit verwenden, wie Googles RE2, das auf Rückverweise verzichtet.

Für Entwickler übersetzt

// Ein DFA in Java: Zustand als enum, Übergänge als switch
enum Z { START, MINUS, ZIFFERN, FEHLER }

static boolean istGanzzahl(String s) {
    Z z = Z.START;
    for (char c : s.toCharArray()) {
        boolean ziffer = c >= '0' && c <= '9';
        z = switch (z) {
            case START   -> c == '-' ? Z.MINUS : ziffer ? Z.ZIFFERN : Z.FEHLER;
            case MINUS, ZIFFERN -> ziffer ? Z.ZIFFERN : Z.FEHLER;
            case FEHLER  -> Z.FEHLER;
        };
    }
    return z == Z.ZIFFERN;     // einziger akzeptierender Zustand
}

// Dasselbe als Regex – Pattern einmal kompilieren, nicht bei jedem Aufruf
private static final Pattern GANZZAHL = Pattern.compile("-?[0-9]+");
boolean ok = GANZZAHL.matcher(s).matches();

// Backtracking vermeiden: possessiver Quantor gibt einmal Erfasstes nicht wieder her
Pattern.compile("(a++)+b");   // scheitert sofort statt exponentiell
SituationWerkzeug
Muster in flachen Zeichenkettenjava.util.regex.Pattern – vorkompiliert als Konstante
Zustandsabhängige Logik (Protokolle, Workflows)Automat als enum mit switch – oder Spring Statemachine
Verschachtelte Formateeinen Parser: Jackson für JSON, ein XML-Parser, jsoup für HTML
Eigene kleine SpracheANTLR erzeugt Lexer und Parser aus einer Grammatik
Regex auf Eingaben von außeneinfache Muster, Längenbegrenzung, oder eine Engine mit Linearzeit wie RE2/J

Häufige Irrtümer

„Mit einem guten Regex kann man HTML parsen.“ – HTML ist verschachtelt, reguläre Sprachen können das nicht. Für einzelne, bekannte Stellen mag es gutgehen; zuverlässig ist nur ein Parser.
„Regex ist immer schnell, weil es ein Automat ist.“ – Echte Automaten ja. Die Engines in Java und den meisten Skriptsprachen arbeiten mit Backtracking und können bei ungünstigen Mustern exponentiell langsam werden.
„Ein NFA kann mehr als ein DFA.“ – Nein, beide erkennen genau die regulären Sprachen. Der NFA ist nur oft kompakter.
„Formale Sprachen sind reine Theorie.“ – Jeder Compiler, jeder Syntax-Highlighter, jede Eingabevalidierung und jede Protokoll-Implementierung baut darauf auf.

Verwandte Begriffe