自分の言語でPCREを実装します。


13

注:これを自分で試した後、すぐにこれがどのような間違いであるかを認識しました。そのため、ルールを少し変更しています。

最低限必要な機能:

  • 文字クラス(.\w\W、など)
  • 乗算器(+*、及び?
  • 単純なキャプチャグループ

あなたの課題は、次の条件に従って、選択した言語でPCREを実装することです。

  • あなたはないかもしれないであなたの言語のネイティブ正規表現機能を使用どのような方法。サードパーティのRegExライブラリも使用できません。
  • エントリには、PCRE仕様をできるだけ実装する必要があります。できるだけ。
  • プログラムは、入力として2行を受け入れる必要があります。

    • 正規表現
    • 照合する文字列入力
  • あなたのプログラムは、その出力で示す必要があります:

    • RegExが入力文字列のどこかに一致したかどうか
    • キャプチャグループの結果
  • 勝者は、仕様の多くを実装するエントリでなければなりません。できるだけ。同点の場合、勝者は私が判断した最も創造的なエントリーとなります。


編集:いくつかのことを明確にするために、入力と予想される出力の例をいくつか示します。


  • 入力:
^ \ s *(\ w +)$
         こんにちは
  • 出力:
一致:はい
グループ1:「こんにちは」

  • 入力:
(\ w +)@(\ w +)(?:\。com | \ .net)
sam@test.net
  • 出力:
一致:はい
グループ1:「サム」
グループ2:「テスト」


PCREの機能の量を考えると、これは非常に難しい課題です。再帰、バックトラック、先読み/アサーション、ユニコード、条件付きサブパターン、...
アルノールブラン

1
PCREのドキュメントを参照してください。PERL RE ; PHP PCREのドキュメントも素晴らしいです。
アルノールブラン

@ user300:目標は、可能な限り実装することです。明らかに、すべてが少し難しすぎるでしょう。
ネイサンオスマン

2
@George:必要な機能をリストアップして、いくつかのテストケースを提供してください。
マルコデュミック

1
@George:@Markoは特定の機能、またはむしろ、人々が最初に実装したい最小限のサブセットを求めていたと思います。しかし、全体として、PCREはカジュアルなコーディングの競争にとって本当に難しすぎる挑戦です。これを非常に小さく、特定のREサブセットに変更することをお勧めします。そして、実装に挑戦します。
MtnViewMark

回答:


10

Python

完全な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のサブセットに限定します。

有限オートマトンは、ノードが状態であり、エッジが遷移であり、各遷移に一致する入力がある有向グラフです。最初に、事前定義された開始ノードから開始します。遷移の1つに一致する入力を受け取るたびに、その遷移を新しい状態に移行します。ターミナルノードに到達すると、オートマトンは入力を受け入れたと呼ばれます。この場合、入力はtrueを返すマッチング関数です。

同じ状態から取得できるより多くの一致する遷移がある場合があるため、これらは非決定性オートマトンと呼ばれます。私の実装では、同じ状態へのすべての遷移は同じものと一致する必要があるため、一致する関数を宛先状態(states[dest][0])とともに保存しました。

ビルディングブロックを使用して、正規表現を有限オートマトンに変換します。ビルディングブロックには開始ノード(first)と終了ノード(last)があり、テキスト(空の可能性のある文字列)からの何かと一致します。

最も簡単な例は次のとおりです。

  • マッチングは何もありません:Truefirst == last
  • :文字にマッチしますc == txt[pos]first == last
  • 一致する文字列の末尾:pos == len(txt)(first == last`)

また、次のトークンと一致するテキスト内の新しい位置が必要になります。

より複雑な例は次のとおりです(大文字はブロックを表します)。

  • 一致するB +:

    • ノードの作成:u、v(何も一致しない)
    • トランジションの作成:u-> B.first、B.last-> v、v-> u
    • ノードvに到達すると、すでにBに一致しています。次に、2つのオプションがあります。さらに進むか、Bの一致を再試行します。
  • 一致するA | B | C:

    • ノードの作成:u、v(何も一致しない)
    • トランジションの作成:u-> A.first、u-> C.first、u-> C.first、
    • トランジションの作成:A-> last-> v、B-> last-> v、C-> last-> v、
    • あなたから任意のブロックに行くことができます

すべての正規表現演算子は、このように変換できます。を試してみてください*

最後の部分は、非常に単純な文法を必要とする正規表現を解析することです。

 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で自分でやろうとしましたが、まったく失敗しました。
ネイサンオスマン

TILが(a+b)+一致しabaabaaabaaaabます。
アレクサンドル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.