Реалізуйте PCRE своєю мовою.


13

Примітка. Після того, як я сам спробував це, я незабаром зрозумів, у чому це помилка. Тому я трохи змінюю правила.

Мінімально необхідна функціональність:

  • Класи символів ( ., \w, \Wі т.д.)
  • Множники ( +, *і ?)
  • Прості групи захоплення

Вашим завданням є впровадження PCRE на обраній вами мові за умови наступних умов:

  • Ви не маєте права використовувати рідні засоби RegEx жодним чином . Ви також не можете використовувати сторонні бібліотеки RegEx.
  • У вашому записі має бути реалізована значна частина специфікацій PCRE. якомога.
  • Ваша програма повинна приймати як вхід, 2 рядки:

    • регулярний вираз
    • рядок введення, щоб відповідати
  • Ваша програма повинна вказати у своєму висновку:

    • Чи збігався RegEx де-небудь у рядку введення
    • Результати будь-яких груп захоплення
  • Переможцем стає запис, який реалізує більшу частину специфікації. якомога. У разі вирівнювання, переможець буде найкреативнішим, як я судив.


Редагувати: щоб уточнити кілька речей, ось кілька прикладів введення та очікуваного виводу:


  • Вхід:
^ \ s * (\ w +) $
         Здравствуйте
  • Вихід:
Матчі: так
Група 1: "привіт"

  • Вхід:
(\ w +) @ (\ w +) (?: \. com | \ .net)
sam@test.net
  • Вихід:
Матчі: так
1 група: "сам"
Група 2: "тест"


Це справді складна задача, враховуючи кількість функцій у PCRE. Рекурсія, зворотний трек, підказки / твердження, унікод, умовні піддрубрики, ...
Arnaud Le Blanc

1
Див. Документи PCRE ; PERL RE ; Документи PHP PCRE теж чудові.
Арно Ле Блан

@ user300: мета - максимально реалізувати. Очевидно, все було б трохи важко.
Натан Осман

2
@George: А як ви перелічите потрібні функції та наведіть кілька тестових випадків, так що ми всі на рівній основі.
Марко Думич

1
@George: Я думаю, що @Marko отримав конкретні функції, а точніше - мінімальний підмножина, яку ви хочете, щоб люди вперше реалізували. В цілому, PCRE - це дуже важкий виклик для випадкової конкуренції з кодування. Я пропоную змінити це на дуже невеликий, специфічний підмножина RE, і вирішувати завдання, яке потрібно реалізувати.
MtnViewMark

Відповіді:


10

Пітон

Оскільки впровадження повного PCRE занадто багато, я реалізував лише істотний його підмножина.

Підтримує |.\.\w\W\s+*(). Регекс введення повинен бути правильним.

Приклади:

$ 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

Як це працює:

Для детальної теорії читайте це Вступ до теорії автоматів, мов та обчислень .

Ідея полягає в перетворенні оригінального регулярного виразу в недетерміновані кінцеві автомати (NFA). Насправді регулярні вирази PCRE - це щонайменше контекстні граматики, для яких нам потрібні автоматичні автомати, але ми обмежимось підмножиною PCRE.

Кінцеві автомати - це спрямовані графіки, у яких вузли є станами, ребра - переходи та кожен перехід має відповідні входи. Спочатку ви починаєте із заздалегідь заданого вузла запуску. Щоразу, коли ви отримуєте вхід, який відповідає одному з переходів, ви переходите до нового стану. Якщо ви дійшли до термінального вузла, він називається, що автомати прийняли введення. У нашому випадку введення - це відповідна функція, яка повертає істину.

Їх називають недетермінованими автоматами, оскільки іноді є більше відповідних переходів, які можна взяти з того самого стану. У моїй реалізації весь перехід до одного стану повинен відповідати одній і тій же речі, тому я зберігав функцію узгодження разом із станом призначення ( states[dest][0]).

Ми перетворюємо своє регулярне вираження в кінцеві автомати, використовуючи будівельні блоки. У будівельному блоці є початковий вузол ( first) і кінцевий вузол ( last) і відповідає чомусь із тексту (можливий порожній рядок).

До найпростіших прикладів можна віднести

  • нічого не відповідає: True( first == last)
  • відповідність символу: c == txt[pos]( first == last)
  • відповідні кінці рядка: pos == len (txt) (first == last`)

Вам також знадобиться нова позиція в тексті, де відповідати наступному маркеру.

Складнішими прикладами є (великі літери означають блоки).

  • відповідність B +:

    • створити вузли: u, v (нічого не відповідає)
    • створити переходи: u -> B.first, B.last -> v, v -> u
    • коли ви дістанетесь до вузла v, ви вже відповідали B. Тоді у вас є два варіанти: піти далі або спробувати відповідати B ще раз.
  • відповідність A | B | C:

    • створити вузли: u, v (нічого не відповідає)
    • створити переходи: u -> A.first, u -> C.first, u -> C.first,
    • створити переходи: A-> останнє -> v, B-> останнє -> v, C-> останнє -> v,
    • ви можете перейти до будь-якого блоку

Усі оператори regexp можуть бути перетворені так. Просто спробуйте *.

Остання частина полягає в розборі регулярного виразка, який вимагає дуже простої граматики:

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

Сподіваюся, реалізувати просту граматику (я вважаю, що це LL (1), але виправити мене, якщо я помиляюся) набагато простіше, ніж створити NFA.

Після того, як у вас буде NFA, вам потрібно буде відслідковувати, поки ви не досягнете термінального вузла.

Вихідний код (або тут ):

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 Вау ... Я спробував це зробити сам із PHP і зовсім провалився.
Натан Осман

(a+b)+Матчі TIL abaabaaabaaaab.
Олександру
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.