Регулярные выражения (или вкратце регулярные выражения) - это тема, которую ненавидят и недооценивают в современном C ++. Но в то же время правильное использование регулярного выражения может избавить вас от написания многих строк кода. Если вы провели достаточно времени в индустрии. А не зная регулярное выражение, вы упускаете 20–30% производительности. В этом случае я настоятельно рекомендую вам изучить регулярное выражение, поскольку это единовременное вложение (что-то похожее на философию учить один раз, писать где угодно).
/! \: Эта статья изначально была опубликована в моем блоге. Если вы заинтересованы в получении моих последних статей, подпишитесь на мою рассылку.
Изначально в этой статье я решил также включить регулярное выражение в целом. Но это не имеет смысла, так как уже есть люди / учебники, которые лучше меня учат регулярному выражению. Но все же я оставил небольшой раздел, посвященный Мотивации и Обучение регулярному выражению. В оставшейся части статьи я сосредоточусь на функциональности, предоставляемой C ++ для работы с регулярным выражением. И если вы уже знаете о регулярных выражениях, вы можете использовать приведенную выше карту разума в качестве напоминания.
Указатель: стандартная библиотека C ++ предлагает несколько различных разновидностей синтаксиса регулярных выражений, но вариант по умолчанию (тот, который вы всегда должны использовать, и я демонстрирую здесь) был полностью заимствован из стандарта для ECMAScript .
Мотивация
- Я знаю его жалкий и несколько запутанный набор инструментов. Рассмотрим приведенный ниже шаблон регулярного выражения в качестве примера, который извлекает время в 24-часовом формате, то есть ЧЧ: ММ.
\b([01]?[0-9]|2[0-3]):([0-5]\d)\b
- Я имею в виду! Кто хочет работать с этим загадочным текстом?
- И все, что приходит в голову, на 100% разумно. Фактически, я дважды откладывал изучение регулярного выражения по той же причине. Но, поверьте, все уродливые вещи не так уж и плохи.
- Способ (↓), который я здесь описываю, займет не более 2–3 часов, чтобы выучить регулярное выражение на интуитивном уровне. И после того, как вы его изучите, вы увидите эффект сложения с возвратом инвестиций с течением времени.
Изучение регулярного выражения
- Не гуглите много и попытайтесь проанализировать, какой учебник лучше. На самом деле, не тратьте время на такой анализ. Потому что в этом нет смысла. На данный момент (ну, если вы не знаете регулярное выражение) действительно имеет значение «Начало работы», а не «Что лучше!».
- Просто перейдите на https://regexone.com, не задумываясь. И пройти все уроки. Поверьте, я изучил много статей, курсов (‹= этот бесплатный, кстати) и книг. Но это лучший вариант для начала работы без потери мотивации.
- И после этого, если у вас все еще есть желание решать другие задачи и упражнения. Обратите внимание на следующие ссылки:
Пример 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 было бы полезно, особенно если вы планируете использовать регулярное выражение во встроенных системах.