Boost.Spirit 2 на примере анализа IRC сообщений

18 апреля 2011 г.

На просторах сети есть много статей по данному вопросу, но почему-то 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 имеет следующий каркас:

template 
struct 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 на следующий:

template  struct Grammar : qi::grammar

Создадим правило верхнего уровня, которое собственно и будет записывать результат в структуру:

Объявление:
qi::rule 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 Nick_;
Nick_ = -(-(qi::ascii::char_ ('!')
		>> User_)
		>> qi::ascii::char_ ('@')
		>> Host_);

Далее все правила реализуются по тому же алгоритму.

Остановлюсь на таком моменте, как создание перечня символов:
для BNF special; “[“, “]”, “\”, “`”, “_”, “^”, “{“, “|”, “}”
Реализуется следующим правилом:

qi::rule Special_;
Special_ = qi::ascii::char_ ("[]\\`_^{|}");

Так же любопытен момент обратный — исключение из перечня символов:
BNF: user; any octet except NUL, CR, LF, ” ” and “@”
Реализуется следующим правилом:

qi::rule 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. По созданю парсера, как мне кажется, все понятно.

Теперь каким образом вызывать эту грамматику:
Создаем объект 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.

Теги: рубрика Программирование
  • Похожие статьи
  • Предыдущие из рубрики