Введение в регулярные выражения в современном C++

от автора

Привет, Хабр. Будущих студентов курса «C++ Developer. Professional» приглашаем записаться на открытый урок по теме «Backend на современном C++»


А пока делимся традиционным переводом полезного материала.


Регулярные выражения (Regular expressions или, вкратце, regex — регулярки) — это пока что непопулярная и недооцененная тема в современном C++. Но в то же время разумное использование регулярных выражений может избавить вас от написания множества строчек кода. Если у вас уже есть какой-никакой опыт работы в индустрии, но вы не умеете использовать регулярные выражения — вы разбазариваете 20-30% своей продуктивности. Я настоятельно рекомендую вам освоить регулярные выражение, так как это единовременная инвестиция в себя (по известному принципу “learn once, write anywhere”).

/!\: Изначально эта статья была опубликована в моем личном блоге. Если вам станет интересно и дальше читать мои самые актуальные статьи, вы можете подписаться на мою рассылку.

Изначально я хотел включить в эту статью сведения о регулярных выражениях вообще в целом. Но это не имеет особого смысла, так как уже существует множество людей и учебников, которые намного лучше меня вводят в регулярные выражения. Но все же я оставил небольшую часть, посвященную мотивации и изучению регулярок. В оставшейся части статьи я сосредоточусь на функциональности для работы с регулярным выражением, предоставляемой конкретно C++. И если вы уже имеете представление о регулярных выражениях, вы можете использовать приведенную ниже ассоциативную карту в качестве напоминания.

Примечание: стандартная библиотека C++ предлагает несколько различных разновидностей («flavours») синтаксиса регулярных выражений, но вариант по умолчанию (тот, который вы всегда должны использовать, и который я демонстрирую здесь) был полностью позаимствован из стандарта ECMAScript.

Мотивация

Я понимаю, инструментарий регулярок скуден и достаточно запутан. Рассмотрим приведенный ниже шаблон регулярного выражения, который извлекает время в 24-часовом формате (т.е. ЧЧ:ММ), в качестве примера.

\b([01]?[0-9]|2[0-3]):([0-5]\d)\b

Вот да! Кто захочет возиться с этим непонятным текстом?

И все, что приходит вам в голову, глядя на это, на 100% небезосновательно. Я сам дважды откладывал изучение регулярных выражений по той же причине. Но, поверьте, этот неприглядный инструмент не так уж и плох.

Подход (↓), который я здесь описываю, не отнимет у вас больше 2-3 часов на изучение регулярных выражений, которые, по правде говоря, интуитивно понятны. После того, как вы их освоите, вы увидите, что с течением времени ваша инвестиция дает стабильные дивиденды.

Изучение регулярных выражений

Не нужно много гуглить и пытаться анализировать, какой учебник лучше. Вообще не тратьте время на такой анализ. Потому что в этом нет смысла. На данный момент (ну, если вы еще не знаете регулярные выражения) больше смысла имеет не пытаться угадать, где же лучше начать, а собственно начать изучение.

Просто не задумываясь перейдите на https://regexone.com. И пройдите все уроки. Поверьте мне, я перелопатил множество статей, курсов (<= этот бесплатный, кстати) и книг. Но это лучший вариант чтобы начать и не потерять мотивацию.

После этого, если у вас осталось желание порешать побольше задач и упражнений, рассмотрите приведенные ниже ссылки:

  1. Упражнения на regextutorials.com

  2. Практические задачи с регулярными выражениями на hackerrank

Пример std::regex и std::regexerror

int main() {     try {         static const auto r = std::regex(R"(\)"); // Escape sequence error     } catch (const std::regex_error &e) {         assert(strcmp(e.what(), "Unexpected end of regex when escaping.") == 0);         assert(e.code() == std::regex_constants::error_escape);     }     return EXIT_SUCCESS; }

Вот видите! Я использую сырые строковые литералы. Вы также можете использовать обычную строку, но в таком случае вы должны использовать двойной бэкслеш (\) для escape-последовательности.

Текущая реализация std::regex медленная (так как требует интерпретации регулярных выражений и создания структуры данных во время выполнения), раздувается и неизбежно требует динамического выделения памяти (не allocator aware). Будьте осторожны, если вы используете std::regex в цикле (см. C++ Weekly — Ep 74 — std::regex optimize by Jason Turner). Кроме того, в ней есть только одна функция-член, которая, как я думаю, действительно может быть полезной — это std::regex::markcount(), которая возвращает несколько групп захвата.

Более того, если вы используете несколько строк для создания шаблона регулярного выражения во время выполнения, то вам может потребоваться обработка исключений (например, std::regexerror), чтобы проверить его правильность.

Пример std::regex_search

int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"((\w+):(\w+);)");     smatch m;      if (regex_search(input, m, r)) {         assert(m.size() == 3);         assert(m[0].str() == "PQR:2;");                // Entire match         assert(m[1].str() == "PQR");                   // Substring that matches 1st group         assert(m[2].str() == "2");                     // Substring that matches 2nd group         assert(m.prefix().str() == "ABC:1->   ");      // All before 1st character match         assert(m.suffix().str() == ";;   XYZ:3<<<");   // All after last character match          // for (string &&str : m) { // Alternatively. You can also do         //     cout << str << endl;         // }     }     return EXIT_SUCCESS; }

smatch — это специализация std::match_results, которая хранит информацию о найденных совпадениях (матчах).

Пример std::regex_match

Лаконичный и приятный пример, который вы всегда можете найти в каждой книге о регулярках, — это валидация электронной почты. И именно здесь идеально подходит функция std::regexmatch.

bool is_valid_email_id(string_view str) {     static const regex r(R"(\w+@\w+\.(?:com|in))");     return regex_match(str.data(), r); }  int main() {     assert(is_valid_email_id("vishalchovatiya@ymail.com") == true);     assert(is_valid_email_id("@abc.com") == false);     return EXIT_SUCCESS; }

return EXIT¨C14Cmatch, а не std::regex¨C15Cmatch сопоставляет (матчит) всю входную последовательность.

Еще одна примечательная вещь — это статический объект регулярного выражения (static const regex), чтобы избежать создания («компиляции/интерпретации») нового объекта регулярного выражения каждый раз при заходе в функцию.

Вся ирония этого крошечного фрагмента кода заключается в том, что он генерирует порядка 30 тысяч строк сборки с флагом -O3. И это просто смешно. Но не волнуйтесь, это уже было доведено до комитета ISO C++. И в скором времени мы можем получить обновление, устраняющее проблему. Между тем у нас есть и другие альтернативы (упомянутые в конце этой статьи).

Разница между std::regex_match и std::regex_search

Вам может быть интересно, почему у нас есть две функции, выполняющие почти одинаковую работу? Даже я изначально не понимал этого. Но после многократного прочтения описания, предоставляемого cppreference, я нашел ответ. И чтобы объяснить этот ответ, я создал пример (очевидно, не без помощи StackOverflow):

int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"((\w+):(\w+);)");     smatch m;      assert(regex_match(input, m, r) == false);      assert(regex_search(input, m, r) == true && m.ready() == true && m[1] == "PQR");      return EXIT_SUCCESS; }

std::regexmatch возвращает true только тогда, когда совпадает ​​вся входная последовательность, в то время как std::regexsearch вернет true, даже если только часть последовательности соответствует регулярному выражению.

Пример std::regex_iterator

std::regex_iterator полезен, когда требуется очень подробная информация о соответствиях и сабматчах.

#define C_ALL(X) cbegin(X), cend(X)  int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"((\w+):(\d))");      const vector<smatch> matches{         sregex_iterator{C_ALL(input), r},         sregex_iterator{}     };      assert(matches[0].str(0) == "ABC:1"          && matches[0].str(1) == "ABC"          && matches[0].str(2) == "1");      assert(matches[1].str(0) == "PQR:2"          && matches[1].str(1) == "PQR"          && matches[1].str(2) == "2");      assert(matches[2].str(0) == "XYZ:3"          && matches[2].str(1) == "XYZ"          && matches[2].str(2) == "3");      return EXIT_SUCCESS; }

Ранее (в C++11) существовало ограничение, заключающееся в том, что std::regex_interator нельзя было вызывать с временным объектом регулярного выражения. Что было исправлено с помощью перегрузки в C++14.

Пример std::regex_token_iterator

std::regextokeniterator — это утилита, которую вы будете использовать в 80% случаев. Она немного отличается от std::regexiterator. Разница между td::regexiterator и std::regextokeniterator заключается в том, что

  • std::regexiterator указывает на соответствующие результаты.

  • std::regextokeniterator указывает на сабматчи.

В std::regextoken_iterator каждый итератор содержит только один соответствующий результат.

#define C_ALL(X) cbegin(X), cend(X)  int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"((\w+):(\d))");      // Note: vector<string> here, unlike vector<smatch> as in std::regex_iterator     const vector<string> full_match{         sregex_token_iterator{C_ALL(input), r, 0}, // Mark `0` here i.e. whole regex match         sregex_token_iterator{}     };     assert((full_match == decltype(full_match){"ABC:1", "PQR:2", "XYZ:3"}));      const vector<string> cptr_grp_1st{         sregex_token_iterator{C_ALL(input), r, 1}, // Mark `1` here i.e. 1st capture group         sregex_token_iterator{}     };     assert((cptr_grp_1st == decltype(cptr_grp_1st){"ABC", "PQR", "XYZ"}));      const vector<string> cptr_grp_2nd{         sregex_token_iterator{C_ALL(input), r, 2}, // Mark `2` here i.e. 2nd capture group         sregex_token_iterator{}     };     assert((cptr_grp_2nd == decltype(cptr_grp_2nd){"1", "2", "3"}));      return EXIT_SUCCESS; }

Инверсия соответствия с std::regex_token_iterator

#define C_ALL(X) cbegin(X), cend(X)  int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"((\w+):(\d))");      const vector<string> inverted{         sregex_token_iterator{C_ALL(input), r, -1}, // `-1` = parts that are not matched         sregex_token_iterator{}     };     assert((inverted == decltype(inverted){                             "",                             "->   ",                             ";;;   ",                             "<<<",                         }));      return EXIT_SUCCESS; }

Пример std::regex_replace

string transform_pair(string_view text, regex_constants::match_flag_type f = {}) {     static const auto r = regex(R"((\w+):(\d))");     return regex_replace(text.data(), r, "$2", f); }  int main() {     assert(transform_pair("ABC:1, PQR:2"s) == "1, 2"s);      // Things that aren't matched are not copied     assert(transform_pair("ABC:1, PQR:2"s, regex_constants::format_no_copy) == "12"s);     return EXIT_SUCCESS; }

Вы видите, что во втором вызове transformpair мы передали флаг std::regexconstants::formatnocopy, который говорит не копировать те части, которые не соответствуют регулярке. В std::regexconstant есть много подобных полезных флагов.

Кроме того, мы создали новую строку, содержащую результаты. Но что, если нам не нужна новая строка, а нужно добавить результаты куда-нибудь, возможно, в контейнер, поток или уже существующую строку. Угадайте, что! Стандартная библиотека уже реализует такую потребность также с помощью перегруженного std::regexreplace следующим образом:

int main() {     const string input = "ABC:1->   PQR:2;;;   XYZ:3<<<"s;     const regex r(R"(-|>|<|;| )");      // Prints "ABC:1     PQR:2      XYZ:3   "     regex_replace(ostreambuf_iterator<char>(cout), C_ALL(input), r, " ");      return EXIT_SUCCESS; }

Примеры использования

Разделение строки с помощью разделителя (delimiter)

std::strtok является наиболее подходящим и оптимальным кандидатом для такой задачи, но в целях демонстрации продемонстрируем как это можно сделать с помощью регулярного выражения:

#define C_ALL(X) cbegin(X), cend(X)  vector<string> split(const string& str, string_view pattern) {     const auto r = regex(pattern.data());     return vector<string>{         sregex_token_iterator(C_ALL(str), r, -1),         sregex_token_iterator()     }; }  int main() {     assert((split("/root/home/vishal", "/")                 == vector<string>{"", "root", "home", "vishal"}));     return EXIT_SUCCESS; }

Удаление пробелов из строки

string trim(string_view text) {     static const auto r = regex(R"(\s+)");     return regex_replace(text.data(), r, ""); }  int main() {     assert(trim("12   3 4      5"s) == "12345"s);     return EXIT_SUCCESS; }

Поиск строк, содержащих или НЕ содержащих определенные слова, из файла

string join(const vector<string>& words, const string& delimiter) {     return accumulate(next(begin(words)), end(words), words[0],             [&delimiter](string& p, const string& word)             {                 return p + delimiter + word;             }); }  vector<string> lines_containing(const string& file, const vector<string>& words) {     auto prefix = "^.*?\\b("s;     auto suffix = ")\\b.*$"s;      //  ^.*?\b(one|two|three)\b.*$     const auto pattern = move(prefix) + join(words, "|") + move(suffix);      ifstream        infile(file);     vector<string>  result;      for (string line; getline(infile, line);) {         if(regex_match(line, regex(pattern))) {             result.emplace_back(move(line));         }     }      return result; }  int main() {    assert((lines_containing("test.txt", {"one","two"})                                         == vector<string>{"This is one",                                                           "This is two"}));     return EXIT_SUCCESS; } /* test.txt This is one This is two This is three This is four */

То же самое касается поиска строк, которые не содержат слов с шаблоном ^((?!(one|two|three)).)*$.

Поиск файлов в папке

namespace fs = std::filesystem;  vector<fs::directory_entry> find_files(const fs::path &path, string_view rg) {     vector<fs::directory_entry> result;     regex r(rg.data());     copy_if(         fs::recursive_directory_iterator(path),         fs::recursive_directory_iterator(),         back_inserter(result),         [&r](const fs::directory_entry &entry) {             return fs::is_regular_file(entry.path()) &&                    regex_match(entry.path().filename().string(), r);         });     return result; }  int main() {     const auto dir        = fs::temp_directory_path();     const auto pattern    = R"(\w+\.png)";     const auto result     = find_files(fs::current_path(), pattern);     for (const auto &entry : result) {         cout << entry.path().string() << endl;     }     return EXIT_SUCCESS; }

Общие советы по использованию регулярных выражений

  • Для описания шаблона регулярного выражения в C++ лучше используйте сырые строковые литералы.

  • Пользуйтесь инструментом проверки регулярных выражений, например https://regex101.com. Что мне нравится в regex101, так это функция генерации кода и вычисление затраченного времени (будет полезна при оптимизации регулярного выражения).

  • Кроме того, хорошим тоном будет добавление объяснения, сгенерированного инструментом проверки, в виде комментария над шаблоном регулярного выражения в вашем коде.

    Производительность:

    • Если вы используете чередование (alternation), попробуйте расположить параметры в порядке наивысшей вероятности, например com|net|org.

    • Старайтесь использовать ленивые квантификаторы.

    • По возможности используйте группы без захвата.

    • Отключайте бэктрекинг.

    • Использование отрицательного класса символов более эффективно, чем использование ленивой точки.

Заключение

Дело не в том, будете ли вы использовать регулярные выражения исключительно на C++ или любым другим языком. Я сам использую их в основном в IDE (в vscode для анализа файлов логов) и на терминале Linux. Имейте в виду, что чрезмерное использование регулярных выражений дает чрезмерное ощущение собственной сообразительности. И это отличный способ рассердить на вас ваших коллег (и всех, кому нужно работать с вашим кодом). Кроме того, регулярные выражения являются излишними для большинства задач синтаксического анализа, с которыми вы столкнетесь в своей повседневной работе.

Регулярные выражения действительно подходят для сложных задач, где рукописный код синтаксического анализа в любом случае будет столь же медленным; и для чрезвычайно простых задач, когда удобочитаемость и надежность регулярных выражений перевешивают их затраты на производительность.

Еще одна примечательная вещь — текущая реализация регулярных выражений (до 19 июня 2020 года) в стандартных библиотеках имеет проблемы с производительностью и раздутием кода. Так что выбирайте с умом между версиями библиотек Boost, CTRE и Std. Скорее всего, вы согласитесь с работой Ханы Дусиковой над регулярным выражением времени компиляции. Кроме того, ее выступление на CppCon в 2018 и 2019 было бы для вас полезно, особенно если вы планируете использовать регулярное выражение во встроенных системах.


Узнать подробнее о курсе «C++ Developer. Professional»

Записаться на открытый урок по теме «Backend на современном C++»

ЗАБРАТЬ СКИДКУ

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/532056/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *