Python
Da die Implementierung von vollständigem PCRE zu viel ist, habe ich nur eine wesentliche Teilmenge davon implementiert.
Unterstützt |.\.\w\W\s+*()
. Der reguläre Ausdruck muss korrekt sein.
Beispiele:
$ python regexp.py
^\s*(\w+)$
hello
Matches: hello
Group 1 hello
$ python regexp.py
(a*)+
infinite loop
$ python regexp.py
(\w+)@(\w+)(\.com|\.net)
sam@test.net
Matches: sam@test.net
Group 1 sam
Group 2 test
Group 3 .net
Wie es funktioniert:
Eine detaillierte Theorie finden Sie in dieser Einführung in Automatentheorie, Sprachen und Berechnung .
Die Idee ist, den ursprünglichen regulären Ausdruck in nichtdeterministische endliche Automaten (NFA) umzuwandeln. Tatsächlich sind reguläre PCRE-Ausdrücke zumindest kontextfreie Grammatiken, für die wir Pushdown-Automaten benötigen, aber wir beschränken uns auf eine Teilmenge von PCRE.
Endliche Automaten sind gerichtete Graphen, in denen Knoten Zustände sind, Kanten Übergänge sind und jeder Übergang eine passende Eingabe hat. Zunächst starten Sie von einem vordefinierten Startknoten. Wann immer Sie eine Eingabe erhalten, die einem der Übergänge entspricht, nehmen Sie diesen Übergang in einen neuen Zustand. Wenn Sie einen Endknoten erreicht haben, wird dies als Eingabe bezeichnet, die von Automaten akzeptiert wurde. In unserem Fall ist die Eingabe eine Matching-Funktion, die true zurückgibt.
Sie werden als nichtdeterministische Automaten bezeichnet, da es manchmal mehr übereinstimmende Übergänge gibt, die Sie aus demselben Zustand ableiten können. In meiner Implementierung müssen alle Übergänge in denselben Zustand mit demselben übereinstimmen, daher habe ich die Übereinstimmungsfunktion zusammen mit dem Zielzustand ( states[dest][0]
) gespeichert .
Wir wandeln unseren regulären Ausdruck mit Hilfe von Bausteinen in endliche Automaten um. Ein Building Block hat einen Startknoten ( first
) und einen Endknoten ( last
) und stimmt mit etwas aus dem Text überein (mögliche leere Zeichenfolge).
Die einfachsten Beispiele sind
- nichts passendes:
True
( first == last
)
- einem Zeichen entsprechen:
c == txt[pos]
( first == last
)
- passendes Ende der Zeichenkette: pos == len (txt)
(
first == last`)
Sie benötigen auch die neue Position im Text, an der das nächste Token gefunden werden soll.
Kompliziertere Beispiele sind (Großbuchstaben stehen für Blöcke).
passendes B +:
- Erstelle Knoten: u, v (nichts passendes)
- Übergänge erstellen: u -> B.first, B.last -> v, v -> u
- Wenn Sie zu Knoten v gelangen, haben Sie bereits eine Übereinstimmung mit B. Dann haben Sie zwei Möglichkeiten: Gehen Sie weiter oder versuchen Sie erneut, eine Übereinstimmung mit B herzustellen.
passend zu A | B | C:
- Erstelle Knoten: u, v (nichts passendes)
- Übergänge erstellen: u -> A.first, u -> C.first, u -> C.first,
- Übergänge erstellen: A-> last -> v, B-> last -> v, C-> last -> v,
- Von dir aus kannst du zu jedem Block gehen
Alle Regexp-Operatoren können auf diese Weise transformiert werden. Probieren Sie es einfach aus *
.
Der letzte Teil ist das Parsen des regulären Ausdrucks, der eine sehr einfache Grammatik erfordert:
or: seq ('|' seq)*
seq: empty
seq: atom seq
seq: paran seq
paran: '(' or ')'
Hoffentlich ist die Implementierung einer einfachen Grammatik (ich denke, sie ist LL (1), aber korrigieren Sie mich, wenn ich falsch liege) viel einfacher als die Erstellung einer NFA.
Sobald Sie die NFA haben, müssen Sie zurückverfolgen, bis Sie den Endknoten erreichen.
Quellcode (oder hier ):
from functools import *
WORDCHAR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_'
def match_nothing(txt, pos):
return True, pos
def match_character(c, txt, pos):
return pos < len(txt) and txt[pos] == c, pos + 1
def match_space(txt, pos):
return pos < len(txt) and txt[pos].isspace(), pos + 1
def match_word(txt, pos):
return pos < len(txt) and txt[pos] in WORDCHAR, pos + 1
def match_nonword(txt, pos):
return pos < len(txt) and txt[pos] not in WORDCHAR, pos + 1
def match_dot(txt, pos):
return pos < len(txt), pos + 1
def match_start(txt, pos):
return pos == 0, pos
def match_end(txt, pos):
return pos == len(txt), pos
def create_state(states, match=None, last=None, next=None, name=None):
if next is None: next = []
if match is None: match = match_nothing
state = len(states)
states[state] = (match, next, name)
if last is not None:
states[last][1].append(state)
return state
def compile_or(states, last, regexp, pos):
mfirst = create_state(states, last=last, name='or_first')
mlast = create_state(states, name='or_last')
while True:
pos, first, last = compile_seq(states, mfirst, regexp, pos)
states[last][1].append(mlast)
if pos != len(regexp) and regexp[pos] == '|':
pos += 1
else:
assert pos == len(regexp) or regexp[pos] == ')'
break
return pos, mfirst, mlast
def compile_paren(states, last, regexp, pos):
states.setdefault(-2, []) # stores indexes
states.setdefault(-1, []) # stores text
group = len(states[-1])
states[-2].append(None)
states[-1].append(None)
def match_pfirst(txt, pos):
states[-2][group] = pos
return True, pos
def match_plast(txt, pos):
old = states[-2][group]
states[-1][group] = txt[old:pos]
return True, pos
mfirst = create_state(states, match=match_pfirst, last=last, name='paren_first')
mlast = create_state(states, match=match_plast, name='paren_last')
pos, first, last = compile_or(states, mfirst, regexp, pos)
assert regexp[pos] == ')'
states[last][1].append(mlast)
return pos + 1, mfirst, mlast
def compile_seq(states, last, regexp, pos):
first = create_state(states, last=last, name='seq')
last = first
while pos < len(regexp):
p = regexp[pos]
if p == '\\':
pos += 1
p += regexp[pos]
if p in '|)':
break
elif p == '(':
pos, first, last = compile_paren(states, last, regexp, pos + 1)
elif p in '+*':
# first -> u ->...-> last -> v -> t
# v -> first (matches at least once)
# first -> t (skip on *)
# u becomes new first
# first is inserted before u
u = create_state(states)
v = create_state(states, next=[first])
t = create_state(states, last=v)
states[last][1].append(v)
states[u] = states[first]
states[first] = (match_nothing, [[u], [u, t]][p == '*'])
last = t
pos += 1
else: # simple states
if p == '^':
state = create_state(states, match=match_start, last=last, name='begin')
elif p == '$':
state = create_state(states, match=match_end, last=last, name='end')
elif p == '.':
state = create_state(states, match=match_dot, last=last, name='dot')
elif p == '\\.':
state = create_state(states, match=partial(match_character, '.'), last=last, name='dot')
elif p == '\\s':
state = create_state(states, match=match_space, last=last, name='space')
elif p == '\\w':
state = create_state(states, match=match_word, last=last, name='word')
elif p == '\\W':
state = create_state(states, match=match_nonword, last=last, name='nonword')
elif p.isalnum() or p in '_@':
state = create_state(states, match=partial(match_character, p), last=last, name='char_' + p)
else:
assert False
first, last = state, state
pos += 1
return pos, first, last
def compile(regexp):
states = {}
pos, first, last = compile_or(states, create_state(states, name='root'), regexp, 0)
assert pos == len(regexp)
return states, last
def backtrack(states, last, string, start=None):
if start is None:
for i in range(len(string)):
if backtrack(states, last, string, i):
return True
return False
stack = [[0, 0, start]] # state, pos in next, pos in text
while stack:
state = stack[-1][0]
pos = stack[-1][2]
#print 'in state', state, states[state]
if state == last:
print 'Matches: ', string[start:pos]
for i in xrange(len(states[-1])):
print 'Group', i + 1, states[-1][i]
return True
while stack[-1][1] < len(states[state][1]):
nstate = states[state][1][stack[-1][1]]
stack[-1][1] += 1
ok, npos = states[nstate][0](string, pos)
if ok:
stack.append([nstate, 0, npos])
break
else:
pass
#print 'not matched', states[nstate][2]
else:
stack.pop()
return False
# regexp = '(\\w+)@(\\w+)(\\.com|\\.net)'
# string = 'sam@test.net'
regexp = raw_input()
string = raw_input()
states, last = compile(regexp)
backtrack(states, last, string)