Con trăn
Vì việc thực hiện PCRE đầy đủ là quá nhiều nên tôi chỉ triển khai một tập hợp con thiết yếu của nó.
Hỗ trợ |.\.\w\W\s+*()
. Regrec đầu vào phải chính xác.
Ví dụ:
$ 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
Làm thế nào nó hoạt động:
Để biết lý thuyết chi tiết, hãy đọc phần Giới thiệu về Lý thuyết, Ngôn ngữ và Tính toán của Automata .
Ý tưởng là để chuyển đổi biểu thức chính quy ban đầu thành một automata hữu hạn không hữu hạn (NFA). Trên thực tế, các biểu thức chính quy của PCRE ít nhất là các ngữ pháp không có ngữ cảnh mà chúng ta cần tự động đẩy xuống, nhưng chúng ta sẽ giới hạn bản thân trong một tập hợp con của PCRE.
Máy tự động hữu hạn là đồ thị có hướng trong đó các nút là trạng thái, các cạnh là các chuyển tiếp và mỗi chuyển đổi có một đầu vào phù hợp. Ban đầu bạn bắt đầu từ một nút bắt đầu, được xác định trước. Bất cứ khi nào bạn nhận được một đầu vào khớp với một trong những chuyển đổi, bạn sẽ chuyển quá trình đó sang trạng thái mới. Nếu bạn đạt đến một nút thiết bị đầu cuối, nó được gọi là đầu vào được chấp nhận automata. Trong trường hợp của chúng tôi đầu vào là một chức năng phù hợp trả về đúng.
Chúng được gọi là automata nondeterminist bởi vì đôi khi có nhiều chuyển tiếp phù hợp hơn mà bạn có thể thực hiện từ cùng một trạng thái. Trong quá trình thực hiện của tôi, tất cả các chuyển đổi sang cùng một trạng thái phải khớp với cùng một thứ, vì vậy tôi đã lưu trữ hàm so khớp cùng với trạng thái đích ( states[dest][0]
).
Chúng tôi chuyển đổi biểu thức chính quy thành một automata hữu hạn bằng cách sử dụng các khối xây dựng. Một khối xây dựng có nút bắt đầu ( first
) và nút kết thúc ( last
) và khớp với nội dung nào đó từ văn bản (chuỗi trống có thể).
Các ví dụ đơn giản nhất bao gồm
- không có gì phù hợp:
True
( first == last
)
- khớp với một ký tự:
c == txt[pos]
( first == last
)
- kết thúc chuỗi kết hợp: pos == len (txt)
(
trước == last`)
Bạn cũng sẽ cần vị trí mới trong văn bản để khớp mã thông báo tiếp theo.
Ví dụ phức tạp hơn là (chữ in hoa là viết tắt của các khối).
khớp B +:
- tạo các nút: u, v (không khớp với gì)
- tạo hiệu ứng chuyển tiếp: u -> B.first, B.last -> v, v -> u
- khi bạn đến nút v bạn đã khớp B. Sau đó, bạn có hai tùy chọn: đi xa hơn hoặc thử khớp lại B.
phù hợp với A | B | C:
- tạo các nút: u, v (không khớp với gì)
- tạo hiệu ứng chuyển tiếp: u -> A.first, u -> C.first, u -> C.first,
- tạo hiệu ứng chuyển tiếp: A-> lần cuối -> v, B-> lần cuối -> v, C-> lần cuối -> v,
- từ bạn có thể đi đến bất kỳ khối nào
Tất cả các toán tử regrec có thể được chuyển đổi như thế này. Chỉ cần thử một lần cho *
.
Phần cuối cùng là phân tích cú pháp regrec đòi hỏi một ngữ pháp rất đơn giản:
or: seq ('|' seq)*
seq: empty
seq: atom seq
seq: paran seq
paran: '(' or ')'
Hy vọng việc thực hiện một ngữ pháp đơn giản (tôi nghĩ là LL (1), nhưng sửa tôi nếu tôi sai) dễ hơn nhiều so với việc xây dựng một NFA.
Khi bạn có NFA, bạn cần quay lại cho đến khi bạn đạt đến nút thiết bị đầu cuối.
Mã nguồn (hoặc tại đây ):
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)