Zaimplementuj PCRE w swoim języku.


13

Uwaga: po tym, jak sam to wypróbowałem, szybko zdałem sobie sprawę, jaki to był błąd. Dlatego trochę modyfikuję zasady.

Minimalna wymagana funkcjonalność:

  • Klasy znaków ( ., \w, \W, itd.)
  • Mnożniki ( +, *i ?)
  • Proste grupy przechwytywania

Twoim wyzwaniem jest wdrożenie PCRE w wybranym przez Ciebie języku z zastrzeżeniem następujących warunków:

  • Nie możesz w żaden sposób korzystać z natywnych funkcji RegEx swojego języka . Nie możesz również korzystać z zewnętrznej biblioteki RegEx.
  • Wpis powinien implementować jak najwięcej specyfikacji PCRE. jak to możliwe.
  • Twój program powinien przyjąć jako dane wejściowe 2 linie:

    • wyrażenie regularne
    • ciąg wejściowy, z którym ma się znaleźć dopasowanie
  • Twój program powinien wskazać w danych wyjściowych:

    • Określa, czy RegEx pasuje gdziekolwiek w ciągu wejściowym
    • Wyniki wszystkich grup przechwytywania
  • Zwycięzcą zostanie zgłoszenie, które implementuje tyle specyfikacji. jak to możliwe. W przypadku remisu zwycięzca będzie najbardziej kreatywnym, według mnie, zdaniem.


Edycja: aby wyjaśnić kilka rzeczy, oto kilka przykładów danych wejściowych i oczekiwanych wyników:


  • Wejście:
^ \ s * (\ w +) $
         Witaj
  • Wynik:
Mecze: tak
Grupa 1: „cześć”

  • Wejście:
(\ w +) @ (\ w +) (?: \. com | \ .net)
sam@test.net
  • Wynik:
Mecze: tak
Grupa 1: „sam”
Grupa 2: „test”


To naprawdę trudne wyzwanie, biorąc pod uwagę liczbę funkcji w PCRE. Rekurencja, cofanie się, wyprzedzanie / asercje, Unicode, warunkowe wzorce, ...
Arnaud Le Blanc

1
Zobacz dokumenty PCRE ; PERL RE ; Dokumenty PHP PCRE są również świetne.
Arnaud Le Blanc

@ user300: Celem jest wdrożenie w jak największym stopniu. Oczywiście wszystko byłoby trochę za trudne.
Nathan Osman,

2
@George: A może wymień funkcje, które chcesz, i podaj kilka przypadków testowych, abyśmy wszyscy byli na równej powierzchni.
Marko Dumic

1
@George: Myślę, że @Marko dążyło do określonych funkcji, a raczej minimalnego podzbioru, który ludzie chcieliby wdrożyć jako pierwsi. Ogólnie rzecz biorąc, PCRE jest naprawdę zbyt trudnym wyzwaniem dla zwykłego konkursu na kodowanie. Sugeruję zmianę tego na bardzo mały, konkretny podzbiór RE i podjęcie wyzwania.
MtnViewMark

Odpowiedzi:


10

Pyton

Ponieważ wdrożenie pełnego PCRE to za dużo, zaimplementowałem tylko jego niezbędny podzbiór.

Podpory |.\.\w\W\s+*(). Wyrażenie regularne danych wejściowych musi być poprawne.

Przykłady:

$ 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

Jak to działa:

Aby uzyskać szczegółową teorię, przeczytaj to wprowadzenie do teorii automatów, języków i obliczeń .

Chodzi o to, aby zamienić oryginalne wyrażenie regularne na niedeterministyczne skończone automaty (NFA). W rzeczywistości wyrażenia regularne PCRE to co najmniej gramatyka bezkontekstowa, dla której potrzebujemy automatów push-down, ale ograniczymy się do podzbioru PCRE.

Automaty skończone to skierowane wykresy, w których węzły to stany, krawędzie to przejścia, a każde przejście ma pasujące wejście. Początkowo zaczynasz od wstępnie zdefiniowanego węzła początkowego. Za każdym razem, gdy otrzymasz dane pasujące do jednego z przejść, przenosisz je do nowego stanu. Jeśli dotarłeś do terminala, nazywa się to wejściem akceptowanym przez automaty. W naszym przypadku wejście jest funkcją dopasowania, która zwraca true.

Nazywa się je niedeterministycznymi automatami, ponieważ czasami istnieje więcej pasujących przejść, które można pobrać z tego samego stanu. W mojej implementacji całe przejście do tego samego stanu musi pasować do tego samego, więc zapisałem funkcję dopasowania wraz z stanem docelowym ( states[dest][0]).

Przekształcamy nasze wyrażenie regularne w skończone automaty przy użyciu bloków konstrukcyjnych. Blok konstrukcyjny ma węzeł początkowy ( first) i końcowy ( last) i dopasowuje coś z tekstu (możliwy pusty ciąg).

Najprostsze przykłady to

  • nic nie pasuje: True( first == last)
  • dopasowanie znaku: c == txt[pos]( first == last)
  • pasujący koniec łańcucha: pos == len (txt) (pierwszy == ostatni`)

Będziesz także potrzebować nowej pozycji w tekście, gdzie dopasować następny token.

Bardziej skomplikowane przykłady to (wielkie litery oznaczają bloki).

  • pasujące B +:

    • utwórz węzły: u, v (nic nie pasuje)
    • utwórz przejścia: u -> B. pierwszy, B. ostatni -> v, v -> u
    • kiedy dojdziesz do węzła v, już dopasowałeś B. Następnie masz dwie opcje: idź dalej lub spróbuj ponownie dopasować B.
  • pasujące A | B | C:

    • utwórz węzły: u, v (nic nie pasuje)
    • utwórz przejścia: u -> A. pierwszy, u -> C. pierwszy, u -> C. pierwszy,
    • twórz przejścia: A-> ostatnie -> v, B-> ostatnie -> v, C-> ostatnie -> v,
    • od ciebie możesz przejść do dowolnego bloku

Wszystkie operatory regularne można przekształcić w ten sposób. Po prostu spróbuj *.

Ostatnią częścią jest parsowanie wyrażenia regularnego, które wymaga bardzo prostej gramatyki:

 or: seq ('|' seq)*
 seq: empty
 seq: atom seq
 seq: paran seq
 paran: '(' or ')'

Mam nadzieję, że implementacja prostej gramatyki (myślę, że to LL (1), ale popraw mnie, jeśli się mylę) jest znacznie łatwiejsza niż zbudowanie NFA.

Po uzyskaniu NFA musisz cofnąć się, aż dotrzesz do węzła końcowego.

Kod źródłowy (lub tutaj ):

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)

1
+1 Wow ... Próbowałem to zrobić sam z PHP i całkowicie nie udało mi się.
Nathan Osman

TIL (a+b)+mecze abaabaaabaaaab.
Alexandru
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.