Boost.Spirit 2 на примере анализа IRC сообщений
На просторах сети есть много статей по данному вопросу, но почему-то 99% из них — это перевод документации на русский язык. Я решил поделиться своим опытом «вызова духов» на, так сказать, реальном примере.
Я являюсь автором Acetamide — это плагин для Leechcraft, который обеспечивает возможность общения по протоколу IRC (rfc 2812).
Boost::Spirit я использовал для разбора входящих сообщений для выделения из них необходимых мне частей.
Формат IRC сообщения, согласно rfc 2812, в форме Бэкуса-Наура выглядит так:
message = [ “:” prefix SPACE ] command [ params ] crlf
Как мы видим, сообщение IRC состоит из 4 частей, из которых обязательная только одна:
- prefix (необязательная);
- command (обязательная);
- params (необязательная)
Начнем:
prefix = servername / ( nickname [ [ “!” user ] “@” host ] ). — Из этой части сообщения нас интересует параметр nickname, который нам необходим для определения отправителя сообщения.
command = 1*letter / 3digit. — этот параметр нас интересует полность. Этот параметр позволяет определить вид действия, которе передает сообщения.
params = *14( SPACE middle ) [ SPACE “:” trailing ] / 14( SPACE middle ) [ SPACE [ “:” ] trailing ]. Этот параметр включает в себя всю остальную часть сообщения. Тут стоит отметить, что в случае наличия последней части, отделяемой “:”, то эта часть является сообщением.
Теперь перейдем к тому, из чего состоят вышеперечисленные части:
nospcrlfcl; any octet except NUL, CR, LF, ” ” and “:”
middle = nospcrlfcl *( “:” / nospcrlfcl )
trailing = *( “:” / ” ” / nospcrlfcl )
servername = hostname
hostname = shortname *( “.” shortname )
shortname = ( letter / digit ) *( letter / digit / “-” ) *( letter / digit )
nickname = ( letter / special ) *8( letter / digit / special / “-” ) — тут стоит отметить, что протокол IRC cильно эволюционировал и во многих местах различается с документацией. Например, в настоящее время отсутствует ограничение на 9 символов ника.
На основании этих данных я составил структутру:
struct IrcMessageStruct { std::string Nickname_; //ник отправителя. std::string Command_; // комманда QList<std::string> Parameters_; // список параметров std::string Message_; // cообщение }
В эту структуру я и собираюсь сохранять результат разбора входящего сообщения.
Перейдем непосредственно к программированию самого парсере.
В Boost.Spirit 2 рекомендуется использовть для сложных парсеров Grammar — это структура, которая объединяет все правила для определенного действия.
Grammar имеет следующий каркас:
templatestruct Grammar : qi::grammar { Grammar () : Grammar::base_type (#правило верхнего уровня) { # тут описывается семантика правил } # тут объявляются сами правила };
Согласно документации boost’а о сохранении результата разбора в структуру () изменим свой парсер.
Для начала нам необходимо, используя макрос BOOST_FUSION_ADAPT_STRUCT, создать шаблон для нашей структуры:
BOOST_FUSION_ADAPT_STRUCT ( IrcMessageStruct, (std::string, Nickname_) (std::string, Command_) (std::list<std::string>, Parameters_) (std::string, Message_) )
Затем изменим наш Grammar для возвращения результата как структуры. Для этого измени вид объявления Grammar на следующий:
templatestruct Grammar : qi::grammar
Создадим правило верхнего уровня, которое собственно и будет записывать результат в структуру:
Объявление:
qi::rule
второй параметр говорит о том, что результатом работы правила является структура.
Определение:
MainRule_ = -qi::omit [':'] >> -Nickname_ >> -qi::omit [Nick_] >> -qi::omit [qi::ascii::space] >> Command_ >> -qi::omit [qi::ascii::space] >> -Params_ >> -qi::omit [qi::ascii::space] >> -qi::omit [':'] >> -Msg_;
Cимвол “-“ перед правилом или элементом обозначает опциональность. Тоесть элемент может встречаться 0 или 1 раз.
qi::omit [] — данная директива обозначает, что парсеру необходимо игнорировать аттрибут этого типа. Тоесть, при азборе эти параметры не будут рассматриваться как потенциальные значения полей структуры.
qi::ascii::space — этот элемент обозначает символ пробела.
a >> b — этот оператор обозначает, что за a следует b.
Теперь перейдем к реализации следующего правила.
qi::rule<Iterator, std::string ()> Nickname_;
Возвращает в качестве результата разбора строку типа std::string
Nickname_ = qi::raw [ShortName_ % '.'];
% — оператор списка, который эквивалентен следующей записи:
ShortName_ >> *('.' >> ShortName_ )
qi::ruleNick_; Nick_ = -(-(qi::ascii::char_ ('!') >> User_) >> qi::ascii::char_ ('@') >> Host_);
Далее все правила реализуются по тому же алгоритму.
Остановлюсь на таком моменте, как создание перечня символов:
для BNF special; “[“, “]”, “\”, “`”, “_”, “^”, “{“, “|”, “}”
Реализуется следующим правилом:
qi::ruleSpecial_; Special_ = qi::ascii::char_ ("[]\\`_^{|}");
Так же любопытен момент обратный — исключение из перечня символов:
BNF: user; any octet except NUL, CR, LF, ” ” and “@”
Реализуется следующим правилом:
qi::ruleUser_; User_ = +(qi::ascii::char_ - '\r' - '\n' - ' ' - '@' - '\0');
C этим ничего сложного нету. Наибольшие трудности у меня вызвал способ парсинга в поле структуры, которое является списком. Способы использования SemanticActions мне не подходили, потому что тогда переставали записываться значания в остальные поля структуры. В итоге решение выглядит следующим образом.
Для начала создаем правило, которое в качестве результата возвращает нам список:
qi::rule<Iterator, std::list<std::string> ()> Params_;
Определяем это правило как:
Params_ = FirstParameters_ % -qi::ascii::space;
И так, как элемент нашего списка std::string, то и правило FirstParameters_ должно возвращать тип std::string:
qi::rule<Iterator, std::string ()> FirstParameters_;
FirstParameters_ = qi::raw [Nospcrlfcl_ >> *(qi::ascii::char_ (‘:’) | Nospcrlfcl_)];
Таким образом результат записывается в QList
Теперь каким образом вызывать эту грамматику:
Создаем объект Grammar:
Grammar<std::string::const_iterator> g;
Создаем объект структуры:
IrcMessageStruct ims;
вызываем функцию парсинга:
std::string::const_iterator first = str.begin(); std::string::const_iterator last = str.end(); qi::parse (first, last, g, ims);
Необходимо так же проверить на то, что сообщение было разобрано полностью, иначе парсер вернет положительный результат для разобранной части. Для этого проверим значения итераторов:
if (first == last)
В итоге вызов парсера выглядит следующим образом:
bool r = qi::parse (first, last, g, ims) && (first == last);
Так же хотел бы обратит внимние на такую очень полезную вещь как debug ():
Эта функция позволяет отслеживать результат выполнения парсинга в формате xml.
Для удобства отображения задаем для каждого правила имя:
Nick_.name ("nick");
и вызываем:
debug (Nick_);
Ссылка на полный код парсера. На вход подается строка, а не файл.:
Ссылка на файл с несколькими видами входящих сообщений:
P.S. если у кого-то будут предложения по улучшению моего парсера — то я с радостью их выслушаю, потому что это мое первое знакомство с Boost.Spirit 2.