Boost.Spirit 2 на примере анализа IRC сообщений
На просторах сети есть много статей по данному вопросу, но почему-то 99% из них — это перевод документации на русский язык. Я решил поделиться своим опытом «вызова духов» на, так сказать, реальном примере.
Я являюсь автором Acetamide — это плагин для Leechcraft, который обеспечивает возможность общения по протоколу IRC (rfc 2812).
Boost::Spirit я использовал для разбора входящих сообщений для выделения из них необходимых мне частей.
Формат IRC сообщения, согласно rfc 2812, в форме Бэкуса-Наура выглядит так:
message = [ ":" prefix SPACE ] command [ params ] crlf
Как мы видим, сообщение IRC состоит из 4 частей, из которых обязательная только одна:
Начнем:
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 символов ника.
На основании этих данных я составил структутру:
В эту структуру я и собираюсь сохранять результат разбора входящего сообщения.
Перейдем непосредственно к программированию самого парсере.
В Boost.Spirit 2 рекомендуется использовть для сложных парсеров Grammar — это структура, которая объединяет все правила для определенного действия.
Grammar имеет следующий каркас:
Для начала нам необходимо, используя макрос BOOST_FUSION_ADAPT_STRUCT, создать шаблон для нашей структуры:
Затем изменим наш Grammar для возвращения результата как структуры. Для этого измени вид объявления Grammar на следующий:
Объявление:
второй параметр говорит о том, что результатом работы правила является структура.
Определение:
Cимвол "-" перед правилом или элементом обозначает опциональность. Тоесть элемент может встречаться 0 или 1 раз.
qi::omit [] — данная директива обозначает, что парсеру необходимо игнорировать аттрибут этого типа. Тоесть, при азборе эти параметры не будут рассматриваться как потенциальные значения полей структуры.
qi::ascii::space — этот элемент обозначает символ пробела.
a >> b — этот оператор обозначает, что за a следует b.
Теперь перейдем к реализации следующего правила.
Возвращает в качестве результата разбора строку типа std::string
% — оператор списка, который эквивалентен следующей записи:
Далее все правила реализуются по тому же алгоритму.
Остановлюсь на таком моменте, как создание перечня символов:
для BNF special; "[", "]", "\", "`", "_", "^", "{", "|", "}"
Реализуется следующим правилом:
Так же любопытен момент обратный — исключение из перечня символов:
BNF: user; any octet except NUL, CR, LF, " " and "@"
Реализуется следующим правилом:
C этим ничего сложного нету. Наибольшие трудности у меня вызвал способ парсинга в поле структуры, которое является списком. Способы использования SemanticActions мне не подходили, потому что тогда переставали записываться значания в остальные поля структуры. В итоге решение выглядит следующим образом.
Для начала создаем правило, которое в качестве результата возвращает нам список:
Определяем это правило как:
И так, как элемент нашего списка std::string, то и правило FirstParameters_ должно возвращать тип std::string:
FirstParameters_ = qi::raw [Nospcrlfcl_ >> *(qi::ascii::char_ (':') | Nospcrlfcl_)];
Таким образом результат записывается в QList<std::string>. По созданю парсера, как мне кажется, все понятно.
Теперь каким образом вызывать эту грамматику:
Создаем объект Grammar:
Создаем объект структуры:
вызываем функцию парсинга:
Необходимо так же проверить на то, что сообщение было разобрано полностью, иначе парсер вернет положительный результат для разобранной части. Для этого проверим значения итераторов:
В итоге вызов парсера выглядит следующим образом:
Так же хотел бы обратит внимние на такую очень полезную вещь как debug ():
Эта функция позволяет отслеживать результат выполнения парсинга в формате xml.
Для удобства отображения задаем для каждого правила имя:
и вызываем:
Ссылка на полный код парсера. На вход подается строка, а не файл.:
Ссылка на файл с несколькими видами входящих сообщений:
P.S. если у кого-то будут предложения по улучшению моего парсера — то я с радостью их выслушаю, потому что это мое первое знакомство с Boost.Spirit 2.
Я являюсь автором 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 имеет следующий каркас:
template <typename Iterator> struct Grammar : qi::grammar<Iterator> { 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 на следующий:
template <typename Iterator> struct Grammar : qi::grammar<Iterator, IrcMessageStruct ()>Создадим правило верхнего уровня, которое собственно и будет записывать результат в структуру:
Объявление:
qi::rule<Iterator, IrcMessageStruct ()> MainRule_;
второй параметр говорит о том, что результатом работы правила является структура.
Определение:
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::rule<Iterator, std::string ()> Nick_; Nick_ = -(-(qi::ascii::char_ ('!') >> User_) >> qi::ascii::char_ ('@') >> Host_);
Далее все правила реализуются по тому же алгоритму.
Остановлюсь на таком моменте, как создание перечня символов:
для BNF special; "[", "]", "\", "`", "_", "^", "{", "|", "}"
Реализуется следующим правилом:
qi::rule<Iterator> Special_; Special_ = qi::ascii::char_ ("[]\\`_^{|}");
Так же любопытен момент обратный — исключение из перечня символов:
BNF: user; any octet except NUL, CR, LF, " " and "@"
Реализуется следующим правилом:
qi::rule<Iterator, std::string ()> User_; 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<std::string>. По созданю парсера, как мне кажется, все понятно.
Теперь каким образом вызывать эту грамматику:
Создаем объект 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.
0 комментариев