Python
Étant donné que la mise en œuvre de PCRE complet est trop, je n'en ai implémenté qu'un sous-ensemble essentiel.
Prise en charge |.\.\w\W\s+*()
. L'expression rationnelle d'entrée doit être correcte.
Exemples:
$ 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
Comment ça fonctionne:
Pour une théorie détaillée, lisez cette introduction à la théorie des automates, aux langages et au calcul .
L'idée est de convertir l'expression régulière d'origine en automates finis non déterministes (NFA). En fait, les expressions régulières PCRE sont au moins des grammaires sans contexte pour lesquelles nous avons besoin d'automates déroulants, mais nous nous limiterons à un sous-ensemble de PCRE.
Les automates finis sont des graphes dirigés dans lesquels les nœuds sont des états, les arêtes sont des transitions et chaque transition a une entrée correspondante. Au départ, vous démarrez à partir d'un nœud de démarrage, prédéfini. Chaque fois que vous recevez une entrée correspondant à l'une des transitions, vous passez cette transition à un nouvel état. Si vous avez atteint un nœud terminal, il est appelé cette entrée acceptée par les automates. Dans notre cas, l'entrée est une fonction de correspondance qui renvoie vrai.
Ils sont appelés automates non déterministes car il existe parfois plus de transitions correspondantes que vous pouvez prendre à partir du même état. Dans mon implémentation, toute transition vers le même état doit correspondre à la même chose, j'ai donc stocké la fonction correspondante avec l'état de destination ( states[dest][0]
).
Nous transformons notre expression rationnelle en automates finis en utilisant des blocs de construction. Un bloc de construction a un nœud de départ (first
) et un nœud de fin (last
) et correspond à quelque chose du texte (chaîne vide possible).
Les exemples les plus simples incluent
- ne correspondant à rien:
True
(first == last
)
- correspondant à un caractère:
c == txt[pos]
(first == last
)
- fin de chaîne correspondante: pos == len (txt)
(
first == last`)
Vous aurez également besoin de la nouvelle position dans le texte où faire correspondre le prochain jeton.
Des exemples plus compliqués sont (les majuscules représentent les blocs).
Tous les opérateurs d'expression rationnelle peuvent être transformés comme ceci. Essayez juste *
.
La dernière partie consiste à analyser l'expression rationnelle qui nécessite une grammaire très simple:
or: seq ('|' seq)*
seq: empty
seq: atom seq
seq: paran seq
paran: '(' or ')'
Espérons que la mise en œuvre d'une grammaire simple (je pense que c'est LL (1), mais corrigez-moi si je me trompe) est beaucoup plus facile que de construire un NFA.
Une fois que vous avez le NFA, vous devez revenir en arrière jusqu'à ce que vous atteigniez le nœud terminal.
Code source (ou ici ):
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)