ใช้ PCRE ในภาษาของคุณ


13

หมายเหตุ:หลังจากลองตัวเองแล้วฉันก็รู้ทันทีว่านี่เป็นความผิดพลาดอะไร ก่อนหน้านี้ฉันกำลังแก้ไขกฎเล็กน้อย

ฟังก์ชั่นที่ต้องการขั้นต่ำ:

  • ชั้นเรียนตัวอักษร ( ., \w, \Wฯลฯ )
  • คูณ ( +, *และ?)
  • กลุ่มการจับภาพง่าย ๆ

ความท้าทายของคุณคือการนำPCREไปใช้ในภาษาที่คุณเลือกภายใต้เงื่อนไขดังต่อไปนี้:

  • คุณไม่สามารถใช้สิ่งอำนวยความสะดวก RegEx ในภาษาของคุณในทางใดทางหนึ่ง คุณไม่สามารถใช้ห้องสมุด RegEx ของบุคคลที่สามได้
  • ข้อมูลของคุณควรนำไปใช้กับข้อกำหนด PCRE มากที่สุด เป็นไปได้.
  • โปรแกรมของคุณควรยอมรับเป็นอินพุต 2 บรรทัด:

    • การแสดงออกปกติ
    • อินพุตสตริงที่จะจับคู่
  • โปรแกรมของคุณควรระบุในผลลัพธ์:

    • ไม่ว่า RegEx จะจับคู่ที่ใดก็ได้ในสตริงอินพุต
    • ผลลัพธ์ของกลุ่มการดักจับใด ๆ
  • ผู้ชนะจะต้องเป็นรายการที่ใช้สเป็คมากที่สุด เป็นไปได้. ในกรณีที่เสมอกันผู้ชนะจะเป็นผลงานที่สร้างสรรค์ที่สุดตามที่ฉันตัดสิน


แก้ไข:เพื่อชี้แจงบางสิ่งต่อไปนี้เป็นตัวอย่างของอินพุตและเอาต์พุตที่คาดหวัง:


  • การป้อนข้อมูล:
^ \ s * (w \ +) $
         สวัสดี
  • เอาท์พุท:
การแข่งขัน: ใช่
กลุ่ม 1: 'สวัสดี'

  • การป้อนข้อมูล:
(w \ +) @ (\ W +) (?. \ คอม | \ .net)
sam@test.net
  • เอาท์พุท:
การแข่งขัน: ใช่
กลุ่ม 1: 'sam'
กลุ่ม 2: 'ทดสอบ'


นับเป็นความท้าทายที่ท้าทายอย่างยิ่งเนื่องจากจำนวนฟีเจอร์ใน PCRE Recursion, backtracking, lookahead / assertions, unicode, subpatterns แบบมีเงื่อนไข, ...
Arnaud Le Blanc

1
ดูเอกสาร PCRE ; PERL RE ; PHP PCRE docs ก็ยอดเยี่ยมเช่นกัน
Arnaud Le Blanc

@ user300: เป้าหมายคือการนำไปใช้ให้ได้มากที่สุด เห็นได้ชัดว่าทุกอย่างจะยากไปหน่อย
Nathan Osman

2
@ George: แล้วคุณจะแสดงรายการคุณสมบัติที่คุณต้องการและให้บางกรณีทดสอบเพื่อให้เราทุกคนมีความเท่าเทียมกัน
Marko Dumic

1
@ George: ฉันคิดว่า @Marko เป็นคุณสมบัติเฉพาะหรือค่อนข้างเป็นเซตย่อยขั้นต่ำที่คุณต้องการให้ผู้คนนำไปใช้ก่อน โดยรวมแล้ว PCRE นั้นยากเกินไปสำหรับการแข่งขันการเขียนโค้ดแบบง่ายๆ ฉันขอแนะนำให้เปลี่ยนเป็นชุดย่อย RE ที่เล็กและเฉพาะเจาะจงมากและใช้ความท้าทายในการปรับใช้
MtnViewMark

คำตอบ:


10

หลาม

เนื่องจากการติดตั้ง PCRE แบบสมบูรณ์นั้นมากเกินไปฉันจึงติดตั้งชุดย่อยที่จำเป็นเท่านั้น

|.\.\w\W\s+*()รองรับ อินพุต regexp ต้องถูกต้อง

ตัวอย่าง:

$ 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

มันทำงานอย่างไร:

สำหรับทฤษฎีที่มีรายละเอียดให้อ่านบทนำของทฤษฎีออโตมาตาภาษาและการคำนวณนี้

แนวคิดคือการแปลงนิพจน์ปกติดั้งเดิมเป็น nondeterminist finite automata (NFA) ที่จริงแล้วนิพจน์ทั่วไปของ PCRE นั้นเป็นไวยากรณ์อย่างน้อยบริบทที่เราต้องการออโตมาตาแบบกดลง แต่เราจะ จำกัด ตัวเองไว้ที่เซตย่อยของ PCRE

โดยอัตโนมัติ จำกัด เป็นกราฟกำกับซึ่งโหนดเป็นสถานะขอบเป็นช่วงการเปลี่ยนภาพและแต่ละช่วงการเปลี่ยนภาพมีอินพุตที่ตรงกัน เริ่มแรกคุณเริ่มจากโหนดเริ่มต้นที่กำหนดไว้ล่วงหน้า เมื่อใดก็ตามที่คุณได้รับอินพุตที่ตรงกับช่วงการเปลี่ยนภาพคุณจะเข้าสู่สถานะใหม่ หากคุณไปถึงโหนดเทอร์มินัลจะถูกเรียกว่าอินพุตที่ยอมรับโดยอัตโนมัติ ในกรณีที่เราใส่ฟังก์ชั่นการจับคู่ที่ส่งกลับจริง

พวกเขาถูกเรียกว่า nondeterminist automata เพราะบางครั้งมีการเปลี่ยนการจับคู่มากกว่าที่คุณสามารถทำได้จากสถานะเดียวกัน ในการนำไปใช้ของฉันการเปลี่ยนสถานะทั้งหมดจะต้องตรงกับสิ่งเดียวกันดังนั้นฉันจึงเก็บฟังก์ชันการจับคู่ไว้พร้อมกับสถานะปลายทาง ( states[dest][0])

เราเปลี่ยน regexp ของเราให้เป็นออโตมาต้าที่ จำกัด โดยใช้แบบเอกสารสำเร็จรูป Building Block มีโหนดเริ่มต้น ( first) และโหนดสุดท้าย ( last) และจับคู่บางอย่างจากข้อความ (สตริงว่างที่เป็นไปได้)

ตัวอย่างที่ง่ายที่สุด ได้แก่

  • ไม่มีอะไรที่ตรงกัน: True( first == last)
  • จับคู่ตัวละคร: c == txt[pos]( first == last)
  • การจับคู่จุดสิ้นสุดของสตริง: pos == len (txt) (first == last`)

คุณจะต้องตำแหน่งใหม่ในข้อความที่ตรงกับโทเค็นถัดไป

ตัวอย่างที่ซับซ้อนมากขึ้นคือ (ตัวพิมพ์ใหญ่ยืนสำหรับบล็อก)

  • การจับคู่ B +:

    • สร้างโหนด: u, v (ไม่มีอะไรที่ตรงกัน)
    • สร้างการเปลี่ยน: u -> B. ก่อนอื่น 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 ทั้งหมดสามารถเปลี่ยนเป็นเช่นนี้ ลองใช้*ดู

ส่วนสุดท้ายคือการแยกวิเคราะห์ 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 และล้มเหลวอย่างสิ้นเชิง
นาธานออสมัน

TIL การแข่งขัน(a+b)+ abaabaaabaaaab
Alexandru
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.