Come simulare backreferenze, lookahead e lookbehind in automi a stati finiti?


26

Ho creato un semplice lexer e parser di espressioni regolari per prendere un'espressione regolare e generare il suo albero di analisi. La creazione di un automa a stati finiti non deterministico da questo albero di analisi è relativamente semplice per le espressioni regolari di base. Tuttavia, non riesco a avvolgere la mia testa su come simulare i riferimenti indietro, i lookaheads e i lookbehinds.

Da quello che ho letto nel libro del drago viola ho capito che per simulare un lookahead cui l'espressione regolare è abbinata se e solo se la corrispondenza è seguita da una corrispondenza dell'espressione regolare , si crea un finito non deterministico automa dello stato in cui è sostituito da . È possibile creare un automa a stati finiti deterministico che faccia lo stesso?r s / εr/srs/ε

Che dire di simulare lookaheads negativi e lookbehinds? Ti sarei davvero grato se mi collegassi a una risorsa che descrive come farlo in dettaglio.



Risposte:


21

Innanzitutto, i backreferences non possono essere simulati da automi finiti in quanto consentono di descrivere linguaggi non regolari. Ad esempio, ([ab]^*)\1corrisponde a , che non è nemmeno privo di contesto.{www{a,b}}

Il look-ahead e il look-behind non sono niente di speciale nel mondo degli automi finiti poiché qui abbiniamo solo input interi . Pertanto, la semantica speciale di "basta controllare ma non consumare" non ha senso; concatenate e / o intersecate il controllo e il consumo delle espressioni e utilizzate gli automi risultanti. L'idea è di controllare le espressioni look-ahead o look-behind mentre si "consuma" l'input e si archivia il risultato in uno stato.

Quando si implementano regexps, si desidera eseguire l'input attraverso un automa e tornare indietro e terminare gli indici delle partite. Questo è un compito molto diverso, quindi non esiste davvero una costruzione per automi finiti. Costruisci l'automa come se l'espressione look-ahead o look-behind stia consumando e cambi il tuo indice di memorizzazione resp. riferendo di conseguenza.

Prendi, ad esempio, i retroscena. Possiamo imitare la semantica di regexp eseguendo il controllo regexp contemporaneamente al regexp "match-all" che consuma implicitamente. solo dagli stati in cui l'automa dell'espressione look-behind è in uno stato finale è possibile inserire l'automa dell'espressione protetta. Ad esempio, regexp /(?=c)[ab]+/(supponendo che sia l'alfabeto completo) - nota che si traduce nell'espressione regolare - potrebbe corrispondere a{ a , b , c } c { a , b } + { a , b , c } {a,b,c}{a,b,c}c{a,b}+{a,b,c}

inserisci qui la descrizione dell'immagine
[ fonte ]

e dovresti

  • memorizza l'indice corrente come ogni volta che inserisci (inizialmente o da ) eq 2 q 2iq2q2
  • segnala una corrispondenza (massima) da all'indice corrente ( ) ogni volta che premi (esci) .iq 21q2

Si noti come la parte sinistra dell'automa sia l'automa parallelo degli automi canonici per [abc]*e c(iterato), rispettivamente.

Le prospettive possono essere trattate in modo simile; devi ricordare l'indice quando inserisci l'automa "principale", l'indice quando lasci l'automa principale ed entri nell'automa look-ahead e segnala una corrispondenza da a solo quando raggiungi la finale dell'automa look-ahead stato.j i jijij

Si noti che il non determinismo è inerente a questo: l'automa principale e l'autocontrollo / dietro potrebbero sovrapporsi, quindi è necessario memorizzare tutte le transizioni tra di loro al fine di riportare quelle corrispondenti in un secondo momento, o backtrack.


11

Il riferimento autorevole sulle questioni pragmatiche alla base dell'implementazione dei motori regex è una serie di tre post sul blog di Russ Cox . Come descritto qui, poiché i riferimenti secondari rendono la tua lingua non regolare, vengono implementati utilizzando il backtracking .

Lookaheads e lookbehinds, come molte caratteristiche dei motori di corrispondenza dei pattern regex, non si adattano perfettamente al paradigma di decidere se una stringa è o meno membro di un linguaggio. Piuttosto con regex di solito stiamo cercando sottostringhe all'interno di una stringa più grande. Le "corrispondenze" sono sottostringhe che sono membri della lingua e il valore restituito è il punto iniziale e finale della sottostringa all'interno della stringa più grande.

Lo scopo di lookaheads e lookbehinds non è tanto quello di introdurre la capacità di abbinare le lingue non regolari, ma piuttosto di regolare dove il motore riporta i punti di inizio e fine della sottostringa abbinata.

Mi affido alla descrizione su http://www.regular-expressions.info/lookaround.html . I motori regex che supportano questa funzione (Perl, TCL, Python, Ruby, ...) sembrano tutti basati sul backtracking (ovvero, supportano un set di lingue molto più ampio rispetto alle sole lingue normali). Sembrano implementare questa funzionalità come un'estensione relativamente "semplice" del backtracking, piuttosto che cercare di costruire automi finiti reali per eseguire l'attività.

Lookahead positivo

La sintassi per lookahead positivo è (?=regex) . Quindi, ad esempio, q(?=u)corrisponde qsolo se è seguito da u, ma non corrisponde a u. Immagino che lo implementino con una variazione sul backtracking. Crea un FSM per l'espressione prima del lookahead positivo. Quando le corrispondenze ricordano dove è finita e inizia un nuovo FSM che rappresenta l'espressione all'interno del lookahead positivo. Se corrisponde, allora hai una "corrispondenza", ma la corrispondenza "termina" appena prima della posizione in cui è iniziata la partita di lookahead positiva.

L'unica parte di ciò che sarebbe difficile senza backtracking è che è necessario ricordare il punto nell'input in cui inizia il lookahead e spostare il nastro di input in questa posizione dopo aver terminato la corrispondenza.

Lookahead negativo

La sintassi per lookahead negativo è (?!regex) . Quindi, ad esempio, q(?!u)corrisponde qsolo se non è seguito da u. Questo potrebbe essere o qseguito da un altro personaggio o qalla fine della stringa. Immagino che questo sia implementato creando un NFA per l'espressione lookahead, quindi riuscendo solo se l'NFA non riesce a corrispondere alla stringa successiva.

Se vuoi farlo senza fare affidamento sul backtracking, potresti annullare l'NFA dell'espressione lookahead, quindi trattarlo allo stesso modo in cui tratti lo sguardo positivo.

Lookbehind positivo

La sintassi per lookbehind positivo è (?<=regex) . Quindi, ad esempio, (?=q)ucorrisponde u, ma solo se è preceduto da q, ma non corrisponde a q. Apparentemente questo è implementato come un hack completo in cui il motore regex esegue il backup di caratteri e cerca di abbinare regex a quegli caratteri. Ciò significa che regex deve essere tale da corrispondere solo a stringhe di lunghezza .n nnnn

Potresti essere in grado di implementarlo senza tornare indietro prendendo l'intersezione di "stringa che termina con regex " con qualsiasi parte del regex che precede l'operatore lookbehind. Questo sarà però complicato, perché la regex lookbehind potrebbe aver bisogno di guardare più indietro rispetto all'attuale inizio dell'input.

Lookbehind negativo

La sintassi per lookbehind negativo è (?<!regex) . Quindi, ad esempio, (?<!q)ucorrisponde u, ma solo se non è preceduto da q. Quindi corrisponderebbe a uin umbrellae uin doubt, ma non a uin quick. Ancora una volta, questo sembra essere fatto calcolando la lunghezza di regex , eseguendo il backup di molti personaggi, testando la partita con regex , ma ora fallendo l'intera partita se il lookbehind corrisponde.

Potresti essere in grado di implementarlo senza tornare indietro prendendo la negazione di regex e quindi facendo lo stesso che faresti per un lookbehind positivo.


5

Almeno per i riferimenti indietro, questo non è possibile. Ad esempio, regex (.*)\1rappresenta una lingua che non è regolare. Ciò significa che è impossibile creare un automa finito (deterministico o no) che riconoscerebbe questo linguaggio. Se vuoi dimostrarlo formalmente, puoi usare il lemma del pompaggio .


4

Ho esaminato questo argomento da solo e dovresti essere in grado di implementare lookahead usando un Alternite Finite Automaton . Quando incontri lookahead, esegui in modo non deterministico sia il lookahead che il resto dell'espressione, accettando solo se entrambi i percorsi accettano. Puoi convertire un AFA in un NFA con un ingrandimento ragionevole (e quindi in un DFA), anche se non ho verificato che la costruzione ovvia si adatta bene ai gruppi di acquisizione.

Lookbehind a larghezza fissa dovrebbe essere perfettamente possibile senza backtracking. Sia n la larghezza. A partire dal punto in cui hai iniziato il lookbehind, hai diviso gli stati guardando indietro, in modo che ogni percorso nel lookbehind finisse con n caratteri di stati che andavano solo nel lookbehind. Quindi, aggiungi lookahead all'inizio di quegli stati (e se lo desideri compila immediatamente il sottografo da AFA a NFA).

Le backreferenze, come altri hanno già detto, non sono regolari, quindi non possono essere implementate da un automa finito. In effetti, sono NP-complete. Nell'implementazione su cui sto lavorando, la corrispondenza sì / no rapida è fondamentale, quindi ho scelto di non implementare affatto i riferimenti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.