Pitone
Poiché l'implementazione di PCRE completo è troppo, ne ho implementato solo un sottoinsieme essenziale.
Supporti |.\.\w\W\s+*()
. Input regexp deve essere corretto.
Esempi:
$ 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
Come funziona:
Per una teoria dettagliata leggi questo introduzione alla teoria, alle lingue e al calcolo degli automi .
L'idea è di convertire l'espressione regolare originale in automi finiti non deterministici (NFA). In realtà, le espressioni regolari di PCRE sono almeno grammatiche libere dal contesto per le quali abbiamo bisogno di automi push-down, ma ci limiteremo a un sottoinsieme di PCRE.
Gli automatismi finiti sono grafici diretti in cui i nodi sono stati, i bordi sono transizioni e ogni transizione ha un input corrispondente. Inizialmente si parte da un nodo iniziale, predefinito. Ogni volta che ricevi un input che corrisponde a una delle transizioni, prendi quella transizione verso un nuovo stato. Se hai raggiunto un nodo terminale, viene chiamato input accettato dagli automi. Nel nostro caso l'input è una funzione corrispondente che restituisce true.
Sono chiamati automi non deterministici perché a volte ci sono più transizioni corrispondenti che puoi prendere dallo stesso stato. Nella mia implementazione tutte le transizioni allo stesso stato devono corrispondere alla stessa cosa, quindi ho memorizzato la funzione di corrispondenza insieme allo stato di destinazione (states[dest][0]
).
Trasformiamo la nostra regexp in automi finiti usando blocchi predefiniti. Un blocco predefinito ha un nodo iniziale (first
) e un nodo finale ( last
) e corrisponde a qualcosa del testo (possibile stringa vuota).
Gli esempi più semplici includono
- niente corrispondente:
True
( first == last
)
- abbinando un personaggio:
c == txt[pos]
( first == last
)
- fine stringa corrispondente: pos == len (txt)
(
first == last`)
Sarà inoltre necessaria la nuova posizione nel testo in cui corrispondere al token successivo.
Esempi più complicati sono (le lettere maiuscole indicano i blocchi).
Tutti gli operatori regexp possono essere trasformati in questo modo. Provaci e basta *
.
L'ultima parte è analizzare la regexp che richiede una grammatica molto semplice:
or: seq ('|' seq)*
seq: empty
seq: atom seq
seq: paran seq
paran: '(' or ')'
Spero che implementare una grammatica semplice (penso che sia LL (1), ma correggermi se sbaglio) è molto più facile che costruire un NFA.
Una volta che hai l'NFA, devi tornare indietro fino a raggiungere il nodo terminale.
Codice sorgente (o qui ):
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)