Когда дело доходит до написания парсера в первую очередь всплывает в памяти Bizon i YACC. Spirit – это еще один замечательный инструмент. Отличием которого является возможность описания грамматики прямо в С++ коде.
double_ -- вот так выглядит парсер одного числа.
Теперь попробуем написать парсер который будет разбирать числа, написанные через запитую, причем мы знаем, что первое число имеет тип int, второе и третье double.
+(int_>>’,’>> double_’,’>> double_>>-’,’) – на первый взгляд уже сложнее, но поверьте через несколько секунд, впечатление изменится.
- int_ как и double_ парсеры соответствующих типов.
- Оператор >> это разделитель отдельных парсеров, а также он говорит, что за ним еще что-то следует.
- ’,’ – парсер запятой. В одинарные кавычки может быть вписан любой символ, только следует иметь ввиду что писать один за другим несколько таких парсеров разделенных оператором >> нельзя.
- Оператор + говорит о том, что конструкция, которая следует за ним, может повторяться сколько угодно раз, но обязательно не меньше чем один раз.
- Оператор - означает, что в данном случае символ ’,’ может быть, а может и не быть.
И так теперь мы можем без проблем распарсить строку вида «12, 13.45, 45.8, 4, 45.9, 78.0».
std::string num = “12, 13.45, 45.8, 4, 45.9, 78.0”; bool r = phrase_parse( num.begin(), num.end(), +(int_>>’,’>> double_’,’>> double_>>-’,’), space );
Продолжаем
Теперь в качестве примера рассмотрим парсер С++ кода, который выделяет из кода классы унаследованные от Widget, и заполняет соответствующую структуру. Она имеет вид: свойства, и виртуальные функции. Функции в свою очередь структуры, в которые сохраняются возвращаемый тип данных, имя функции, список типов, получаемых функцией.
Но сначала разберемся с тем, что такое rules.
Rules – это не большие правила разбора текста, которые легче воспринимаются человеком. Которые потом объединяются. Основной задачей является повышение удобства использования.
qi::rule<iterator, std::string()=""> name; name = lexeme[(qi::alpha | '_') >> *(qi::alnum | '_')];
И так правило name означает лексему которая начинается с буквы, либо символа нижнего подчеркивания а за тем может быть сколько угодно букв, цифр или ’_’.
- Новый оператор | означает логический оператор или.
- Директива lexeme[] нужна для того чтобы по символу разобрать строку, невзирая на то правило вызывается в функции phrase_parse, которая в свою очередь уже разбивает строку на слова. Для этого в примере и передаем четвертым аргументом разделитель ascii::space_type.
qi::rule<Iterator, std::string(), ascii::space_type> type; type = *(qi::string("unsigned") | qi::string("signed") | qi::string("const") | qi::string("long")) >> name >> *(qi::string("::") >> name) >> *(char_('*') | char_('&'));
Правило type уже сложнее в своей структуре. Описывает такие типы как const float&, или long long int.
- Оператор * говорит о том, что конструкция, которая следует за ним, может повторяться сколько угодно раз, а может и вообще не встречаться.
Имея правила для описания типов и имен уже легко можно описать нужные нам функции, но имеется одно но. Нам нужно заполнять структуру соответствующими данными. Для этого используем Boost.Fusion.
struct method_struct
struct method_struct { std::string ret_type; std::string name; std::vector<std::string> in_type; }; BOOST_FUSION_ADAPT_STRUCT( method_struct, (std::string, ret_type) (std::string, name) (std::vector<std::string>, in_type) ) qi::rule<iterator, ascii::space_type, method_struct()> method; method = "virtual">> type[at_c<0>(_val) = _1] >> name[at_c<1>(_val) = _1] >> '(' >> *(type[push_back(at_c<2>(_val), _1)] >>omit[-name>>-lit(',')])>>')'>>omit[-body];
Разберем эту часть type[at_c<0>(_val) = _1]. at_c<0>(_val) = _1 означает: сохранить _1 в элемент структуры с номером 1. Таким образом выражение описанное в квадратных скобках заполняет нулевой элемент структуры данными полученными правилом type, так как _1 хранит это значение.
Почти тоже самое и здесь. [push_back(_val), _1)]. push_back служит для заполнения векторов. То что правило возвращает структуру method_struct показано в объявлении третьим параметром.
Сохранение данных
Есть несколько способов сохранять значения которые возвращают правила. Вот один из них. method [std::bind(&ClassWrapper::setFunction, &wrap, std::placeholders::_1) ]. Метод ClassWrapper::setFunction(const &method_struct) сохраняет значение нужным образом. Использовать bind здесь очень удобно.
Заключение
Сохранение данных
Есть несколько способов сохранять значения которые возвращают правила. Вот один из них. method [std::bind(&ClassWrapper::setFunction, &wrap, std::placeholders::_1) ]. Метод ClassWrapper::setFunction(const &method_struct) сохраняет значение нужным образом. Использовать bind здесь очень удобно.
Заключение
На мой взгляд самые сложные места разобраны. Привожу полную грамматику и функцию, которая осуществляет разбор. Конечно же здесь рассмотрено далеко не все, но help нам поможет.
namespace client { namespace qi = boost::spirit::qi; namespace ascii = boost::spirit::ascii; template <typename Iterator> bool parse_class(Iterator first, Iterator last, ClassWrapper& wrap) { using qi::char_; using qi::_val; using qi::phrase_parse; using qi::lexeme; using qi::_1; namespace fusion = boost::fusion; namespace phoenix = boost::phoenix; using qi::lit; using ascii::space; using boost::spirit::qi::omit; using boost::spirit::qi::alpha; using phoenix::at_c; using phoenix::push_back; qi::rule<Iterator, std::string(), ascii::space_type> class_name; qi::rule<Iterator, std::string()> class_properties; qi::rule<Iterator, method_struct(), ascii::space_type> method; qi::rule<Iterator, std::string(), ascii::space_type> type; qi::rule<Iterator, std::string()> name; qi::rule<Iterator, std::string(), ascii::space_type> data_type; qi::rule<Iterator, std::string(), ascii::space_type> body; qi::rule<Iterator, std::string(), ascii::space_type> comment; class_name = omit[*(char_ - "class")] >> "class">> name >> ":" >> lit("public") >> lit("Widget"); class_properties = lit("Q_PROPERTY")>>'('>>*(char_ - ')')>>lit(')'); type = *(qi::string("unsigned") | qi::string("signed") | qi::string("const") | qi::string("long")) >>-qi::string("long") >> name >> *(qi::string("::") >> name) >> *(char_('*') | char_('&')); name = lexeme[(qi::alpha | '_') >> *(qi::alnum | '_')]; method = "virtual">> type[at_c<0>(_val) = _1] >> name[at_c<1>(_val) = _1] >> '(' >> *(type[push_back(at_c<2>(_val), _1)] >>omit[-name>>-lit(',')])>>')'>>omit[-body]; data_type = (qi::string("public") | qi::string("protected") | qi::string("private"))[_val += _1]>>-qi::string("slots")[_val += _1]>>lit(':'); body = lit('{')>> *(char_ - '}')>>'}'; comment = qi::no_skip[(qi::string("/*") >> *(char_ - "*/") >> "*/") | "//">>(*(char_ - qi::eol) >> qi::eol) ] ; bool r = qi::phrase_parse(first, last, ( *(( class_name[std::bind(&ClassWrapper::setName, &wrap, std::placeholders::_1)] >> '{' >>+(qi::space | data_type [std::bind(&ClassWrapper::setFuncType, &wrap, std::placeholders::_1)] | body | comment | class_properties[std::bind(&ClassWrapper::setProperty, &wrap, std::placeholders::_1)] | method [std::bind(&ClassWrapper::setFunction, &wrap, std::placeholders::_1)] | (char_ - '}')) >> '}' >>*char_) | char_ ) ),space ) ; if(first!=last) return false; return r; } }
Надеюсь Spirit понравится вам также как и мне. И вы будете получать удовольствие от его использования.
В принципе идея понятна. Несмотря, что с boost я знаком мало, да и на С++ не писал уже давно.
ОтветитьУдалитьКстати ты слышал о таких способах парсинга как Regular Expressions, PCRE? Они пришли из Перла и активно внедрились в Java, C#, Python, etc. С++, и в частности Qt похоже пошли своим путём. На первый вгляд С++ boost fusion требует больше тексту сравнительно с тем же питоном или даже джавой. Дает ли это реальные плюсы?
Просто интересно сравнить их было в плане быстродействия например. Как бы намекаю на тему новой статьи..
Обсуждение этой темы являются холиваром для многих веток форумов и блогов. На мой взгляд они созданы немного для разных целей. Ведь классические регулярные выражения являются конечным автоматом. А вставка рекурсии в свою очередь отходит от базовых принципов.
ОтветитьУдалитьЕсли говорить о количестве текста для описания грамматики не стоит забывать и о преимуществах таких языков как Lua.
Преимуществом Spirit'a является возможность встраивать парсеры в С++ код. Также он очень хорошо спроектирован, примером является определение символов римской системы исчисления.
struct ones_ : qi::symbols
{
ones_()
{
add
("I" , 1)
("II" , 2)
("III" , 3)
("IV" , 4)
("V" , 5)
("VI" , 6)
("VII" , 7)
("VIII" , 8)
("IX" , 9) ;
}
} ones;(http://www.boost.org/doc/libs/1_46_0/libs/spirit/doc/html/spirit/qi/tutorials/roman_numerals.html)
Такой парсер сразу вернет нужное число. Не представляю себе что-нибудь похожее на том же Python. Хотя он в определенных случаях может оказаться удобнее.