Search
Write a publication
Pull to refresh

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 имеет следующий каркас:
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.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.