Регулярные выражения (или вкратце регулярные выражения) - это тема, которую ненавидят и недооценивают в современном C ++. Но в то же время правильное использование регулярного выражения может избавить вас от написания многих строк кода. Если вы провели достаточно времени в индустрии. А не зная регулярное выражение, вы упускаете 20–30% производительности. В этом случае я настоятельно рекомендую вам изучить регулярное выражение, поскольку это единовременное вложение (что-то похожее на философию учить один раз, писать где угодно).

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

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

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

Мотивация

  • Я знаю его жалкий и несколько запутанный набор инструментов. Рассмотрим приведенный ниже шаблон регулярного выражения в качестве примера, который извлекает время в 24-часовом формате, то есть ЧЧ: ММ.
\b([01]?[0-9]|2[0-3]):([0-5]\d)\b
  • Я имею в виду! Кто хочет работать с этим загадочным текстом?
  • И все, что приходит в голову, на 100% разумно. Фактически, я дважды откладывал изучение регулярного выражения по той же причине. Но, поверьте, все уродливые вещи не так уж и плохи.
  • Способ (), который я здесь описываю, займет не более 2–3 часов, чтобы выучить регулярное выражение на интуитивном уровне. И после того, как вы его изучите, вы увидите эффект сложения с возвратом инвестиций с течением времени.

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

  • Не гуглите много и попытайтесь проанализировать, какой учебник лучше. На самом деле, не тратьте время на такой анализ. Потому что в этом нет смысла. На данный момент (ну, если вы не знаете регулярное выражение) действительно имеет значение «Начало работы», а не «Что лучше!».
  • Просто перейдите на https://regexone.com, не задумываясь. И пройти все уроки. Поверьте, я изучил много статей, курсов (‹= этот бесплатный, кстати) и книг. Но это лучший вариант для начала работы без потери мотивации.
  • И после этого, если у вас все еще есть желание решать другие задачи и упражнения. Обратите внимание на следующие ссылки:
  1. Упражнения на regextutorials.com
  2. Практическая задача на регулярном выражении по хакерранку

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

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 медленная (так как ей требуется интерпретация регулярных выражений и создание структуры данных во время выполнения), раздутые и неизбежно требуют выделения кучи (без учета распределителя). Итак, будьте осторожны, если вы используете std::regex в цикле (см. C ++ Weekly - Ep 74 - std :: regex optimize by Jason Turner). Кроме того, есть только одна функция-член, которая, как мне кажется, может быть полезной, - это std :: regex :: mark_count (), которая возвращает несколько групп захвата.
  • Более того, если вы используете несколько строк для создания шаблона регулярного выражения во время выполнения. Тогда вам может понадобиться обработка исключений, т.е. std::regex_error, чтобы проверить его правильность.

Пример 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::regex_match идеально подходит.
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("[email protected]") == true);
    assert(is_valid_email_id("@abc.com") == false);
    return EXIT_SUCCESS;
}
  • Я знаю, что это не полный шаблон регулярного выражения валидатора электронной почты. Но мое намерение также не в этом.
  • Скорее вы должны задаться вопросом, почему я использовал std::regex_match! не std::regex_search! Обоснование простое: std::regex_match соответствует всей входной последовательности.
  • Также следует отметить статический объект регулярного выражения, чтобы избежать создания («компиляции / интерпретации») нового объекта регулярного выражения каждый раз при вводе функции.
  • Ирония приведенного выше крошечного фрагмента кода заключается в том, что он создает около 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::regex_match возвращает true только тогда, когда была сопоставлена ​​вся входная последовательность, тогда как std::regex_search будет успешным, даже если только подпоследовательность соответствует регулярному выражению.

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::regex_token_iterator - это утилита, которую вы собираетесь использовать 80% времени. Он имеет небольшие отличия по сравнению с std::regex_iterator. разница между std::regex_iterator и std::regex_token_iterator составляет
  • std::regex_iterator баллов за совпадение результатов.
  • std::regex_token_iterator указывает на дополнительные совпадения.
  • В 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))");
    // 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;
}
  • Вы видите, что во втором вызове transform_pair мы передали флаг std::regex_constants::format_no_copy, который предлагает не копировать то, что не совпадает. В разделе std :: regex_constant есть много таких полезных флагов.
  • Кроме того, мы создали новую строку, содержащую результаты. Но что, если нам не нужна новая строка. Скорее хочет добавить результаты прямо куда-нибудь (возможно, в контейнер, поток или уже существующую строку). Угадай, что! стандартная библиотека покрыла это также с помощью перегруженного std::regex_replace следующим образом:
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;
}

Случаи применения

Разделение строки разделителем

  • Хотя 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, так это функция генерации кода и времени (будет полезно при оптимизации регулярных выражений).
  • Кроме того, попробуйте добавить сгенерированное из инструмента проверки объяснение в виде комментария точно над шаблоном регулярного выражения в вашем коде.
  • Представление:
  • Если вы используете чередование, попробуйте расположить параметры в порядке высокой вероятности, например com|net|org.
  • Если возможно, попробуйте использовать ленивые квантификаторы.
  • По возможности используйте группы без захвата.
  • Отключить возврат.
  • Использование инвертированного символьного класса более эффективно, чем использование ленивой точки.

Напутственные слова

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

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

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