To jest stara wersja strony!
Bartłomiej Wojcieszek G1SST
Parser, zwany także analizatorem składniowym, pozwala na analize danych wejściowych i określenie czy są zgodne z daną gramatyką. Zazwyczaj parsery są wykorzystywane przy przetwarzaniu danych zrozumialych dla czlowieka w dane najbardziej wygodne dla komputera.
Biblioteka Spirit pozwala tworzyć własne złożone parsery lub wykorzytywać te najprostsze wbudowane w bibliotekę. Szablony wyrażeń zastosowanych w bibliotece pozwalają na zapis w notcacji bardzo podobnej do EBNF (Extended Backus-Normal Form):
Gramatyka EBNF:
group ::= '(' expression ')' factor ::= integer | group term ::= factor (('*' factor) | ('/' factor))* expression ::= term (('+' term) | ('-' term))*
I ten sam parser zapisany w C++ z pomocą biblioteki Spirit:
group = '(' >> expression >> ')'; factor = integer | group; term = factor >> *(('*' >> factor) | ('/' >> factor)); expression = term >> *(('+' >> term) | ('-' >> term));
Aby wykorzystywać w swoich programach bibliotekę spirit, należy dołączyć odpowiednie nagłówki do naszego pliku:
#include <boost/spirit/core.hpp> #include <boost/spirit/actor/push_back_actor.hpp>
Załóżmy,że chcemy aby nasz program akceptował tylko takie dane wejsciowe, że pierwszym elementem jest liczba rzeczywista, a następnym znak alfabetyczny. Tworzymy parser:
real_p>>alpha_p
Aby go wykorzystać, należy skorzystać z funkcji parse:
bool parseInput(String str){ return parse(str,real_p>>alpha_p,space_p); }
Funkcja parse zwraca true jeżeli parsowanie przebiegło pomyślnie. Przyjmuje trzy argumenty:
Wyrażenia parsujące tworzy się za pomocą wbudowanych w bibliotekę prymitywów oraz operatorów. Kilka podstawowych prymitywów (więcej można znaleźć w dokumentacji):
ch_p('X') //parsuje char podany jako argument range_p('a','z') //parsuje pojedynczy char z podanego zakresu str_p("Hello") //parsuje podany string anychar_p //dowolny znak alnum_p //znak alfanumeryczny alpha_p //znak alfabetu blank_p //spacje i tabulacje lower_p //male litery print_p //znaki drukowane punct_p //znaki punktuacyjne space_p //spacja tab,nowa linia upper_p //duze litery digit_p //cyfra int_p //int real_p //liczba rzeczywista
I operatory:
a | b //a lub b a & b //a i b a - b //a ale nie b a ^ b //a lub b ale nie oba (XOR) a>>b //a i b w sekwencji a&&b //jw a||b //a lub b w sekwencji *a //a zero lub wiecej razy +a //a raz lub wiecej razy !a //a zero lub jeden raz a%b //lista a oddzielonych przez b
Dodatkowo parsery numeryczna możemy bardziej sprecyzować. Oto szablon takiego parsera:
template < typename T = unsigned, //bazowy typ parsera, default jest unsigned int Radix = 10, //podstawa systemu, czyli dziesiętny szesnastkowy itp. unsigned MinDigits = 1, //minimalna liczba cyfr int MaxDigits = -1> //maksymalna liczba cyfr (-1 oznacza niograniczona) struct uint_parser { /*...*/ }; //zatem jeżeli chcemy sparsować liczbę szesnastkową, nasz parser będzie wyglądał następująco: uint_parser<unsigned, 16, 1, -1> const //lub to samo osiągniemy korzystając z wbudowanego parsera hex_p //chcielibyśmy sparsować liczbe odzdzieloną przecinkami, taką jak: 1,234,567,891 uint_parser<unsigned, 10, 1, 3> uint3_p; // parsuje od 1 do 3 cyfr,czyli pierwsza część uint_parser<unsigned, 10, 3, 3> uint3_3_p; // dokładnie 3 cyfry ts_num_p = (uint3_p >> *(',' >> uint3_3_p)); // dowolna liczba oddzielana co 3 cyfry przecinkami
Załóżmy, że chcemy wczytać ze standardowego wyjscia do wektora ciąg liczb oddzielonych przecinkami (oczywiście przecinki nas nie interesują, tylko liczby). Czyli n a wejsciu ma się pojawić liczba, nastepnie dowolna ilość razy przecinek i kolejna liczba. Zatem w kodzie c++ wygląda to następująco:
bool parse_numbers(char const* str, vector<double>& v) { return parse(str, ( real_p[push_back_a(v)] >> *(',' >> real_p[push_back_a(v)]) ) , space_p).full; }
Wywołana metoda full
zwraca true jeżeli wszystkie dane zostały poprawnie sparsowane.
A co to za dziwne nawiasy kwadratowe za wyrażeniem? Jest to akcja semantyczna (ang. Semantic Action). Dzięki niej sparsowane liczby są przekazywane do wektora. Sam parser sprawdza tylko czy dane wyrażenie pasuje do wzorca, ale nic więcej z nim nie robi. Z pomocą przychodzą akcje semantyczne. Mogą być one dodane w dowolnym miejscu. Funkcja jest wywoływana w momencie, gdy parsowanie przebiegło pomyślnie. Jak to się robi?:
//jeżeli P to nasz parser a F to funkcja w c++ to aby "podłączyć" funkcję do parsera, należy to zrobić w następujący sposób: P[&F] //jeżeli natomiast F jest funktorem: P[F] //dodatkowo argument fukncji musi się zgadzać z elementem parsowanym: void Foo(double d); real_p[&Foo]
Reguła czyli rule pozwala przechowywać skonstruowany przez nas parser. Kilka przykładów:
rule<> a_rule = *(ch_p('a') | ch_p('b')) & +(ch_p('c') | ch_p('d') | range_p('k','z')); //można też odwołać się do innej reguły: rule<> another_rule = int_p >> ch_p('a') >> str_p('bla') >> second_rule; //należy tylko pamiętać,że jest to odwołanie przez referencję, do programisty więc należy pilnowanie //aby dana reguła była w zasięgu i ciągle istniała
Dzięki regułom możemy tworzyć dynamiczne parsery w naszym programie, zalęznie od jego przebiegu:
rule<> my_rule=digit_p; if(some condition) my_rule=str_p("blaBla");