17 lutego 2015

Parser na bazie własnej gramatyki - pyparsing

Czasami zachodzi potrzeba, przeanalizowania danych, na które nie ma dedykowanego parsera. Niekiedy wystarcza regexp, ale jeżeli struktury danych zaczynają być zagnieżdżone, można pokusić się o stworzenie gramatyki i skorzystanie z jakiejś biblioteki. Ponieważ dawno się już tym nie zajmowałem, nie przywiązywałem szczególnej uwagi, czy biblioteka pozwala na zapis zbliżony do notacji BNF (http://pl.wikipedia.org/wiki/Notacja_BNF).

Wybrałem pierwszą z brzegu, omówioną w wykładzie Erik Rose - "Parsing Horrible Things with Python":



Dokumentacja jest moim zdaniem kiepska (pierwszy link), ale następny link (wykład Paula McGuire) bardzo ułatwiły pracę:
Instalacja pyparsing w wersji 2.0.3.
pip install pyparsing
Przykład - wyciąganie liczby ze stringu, który może być wielokrotnie zamknięty za pomocą nawiasów < i >
import pyparsing as pp

startMark = pp.Suppress(pp.Literal('<'))
endMark = pp.Suppress(pp.Literal('>'))

greet = pp.Word(pp.alphas + ',')
value = pp.Word(pp.nums)
lastContent = greet + value

expr = pp.Forward()

content = pp.Group(expr).setResultsName('Indent') | \
          pp.Group(lastContent).setResultsName('Right Content')

expr << startMark + content + endMark

grammar = expr

data = '<<Hello, 1337>>'
res = grammar.parseString(data)
print(res)
print(type(res))
print(res[0].getName())
Wynik:
[[['Hello,', '1337']]]
<class 'pyparsing.ParseResults'>
Indent
Do budowania gramatyk, najlepiej korzystać z operatorów +, ^, |. Do dopasowania tekstu służy nam wiele obiektów (tokenów) np.
  • Word - dopasowuje tekst z dowolnego złożenia znaków
  • Literal - dopasowuje zwartość do znaków dokładnie w takiej kolejności jak zostały podane
  • Suppress - dopasowuje, ale zawartość zostanie pominięta w wyniku
Problemem może być dopasowywanie do zagnieżdżonych zawartości. Token w takim przypadku, powinien odwoływać się sam do siebie. Z pomocą przychodzi Forward(), a ostateczny opis należy wykonać za pomocą operatora << (linijka 14).
Group, pozwala na grupowanie wyników/tokenów jakie pojawią się w ostatecznym wyniku.
Słowo jeszcze o metodach, które okazały się w moim przypadku niezwykle przydane:
  • delimitedList(expression, delim=',') - dopasuje wyrażenie, rozdzielone za pomocą delimitera (domyślnie jest to przecinek)
  • Group.setResultsName() - nadaje nazwę do dopasowanego tokenu, ma niby posłużyć w celu zbudowania słownika ze sparsowanymi danymi, ale ja po prostu korzystam z getName() na wynikowych danych, gdy po nich iteruje
  • ParseResults.asList() - wynik domyślnie jest obiektem ParseResult, ale możemy skorzystać z konwersji i przekształcić go na listę

Brak komentarzy:

Prześlij komentarz