Синтаксис регулярных выражений

Обычные символы, не имеющие специального значения, могут присутствовать в строке шаблона, и они будут трактоваться как есть. Пример указания в шаблоне последовательности обычных символов:

std::regex rgx("строка");
std::cout << std::regex_match("строка", rgx) << std::endl; // 1

Экранирование специальных символов

Внутри регулярного выражения символы ., ^, $, *, +, ?, {, [, ], \, |, ( и ) имеют специальное значение. Если эти символы должны трактоваться как есть, то их следует экранировать с помощью слэша. Некоторые специальные символы теряют свое специальное значение, если их разместить внутри квадратных скобок. В этом случае экранировать их не нужно. Например, метасимвол «точка» соответствует любому символу, кроме символа перевода строки. Если необходимо найти именно точку, то перед точкой необходимо указать символ \ или поместить точку внутри квадратных скобок ([.]). Продемонстрируем это на примере проверки правильности введенной даты (листинг 10.1).

Листинг 10.1. Проверка правильности ввода даты

#include <iostream>
#include <locale>
#include <string>
#include <regex>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
   std::string d("29,07.2021"); // Вместо точки указана запятая
   // Символ "\" не указан перед точкой
   std::regex rgx("[0-3][0-9].[01][0-9].[12][09][0-9][0-9]");
   if (std::regex_match(d, rgx))
      std::cout << "Дата введена правильно" << std::endl;
   else
      std::cout << "Дата введена неправильно" << std::endl;
   // Т. к. точка означает любой символ, выведет:
   // Дата введена правильно

   // Символ "\" указан перед точкой
   rgx = "[0-3][0-9]\\.[01][0-9]\\.[12][09][0-9][0-9]";
   if (std::regex_match(d, rgx))
      std::cout << "Дата введена правильно" << std::endl;
   else
      std::cout << "Дата введена неправильно" << std::endl;
   // Т. к. перед точкой указан символ "\", выведет:
   // Дата введена неправильно

   // Точка внутри квадратных скобок
   rgx = "[0-3][0-9][.][01][0-9][.][12][09][0-9][0-9]";
   if (std::regex_match(d, rgx))
      std::cout << "Дата введена правильно" << std::endl;
   else
      std::cout << "Дата введена неправильно" << std::endl;
   // Выведет: Дата введена неправильно
   return 0;
}

Следует учитывать, что символ обратного слеша является специальным не только в шаблоне регулярного выражения, но и в строке. Поэтому его нужно экранировать. Например, проверим равна ли строка значению "\s". Внутри шаблона регулярного выражения данная комбинация символов является специальной и обозначает любые пробельные символы. Поэтому внутри шаблона мы должны добавить еще три слеша, чтобы экранировать специальную комбинацию:

std::regex rgx("^\\\\s$");
std::cout << std::regex_search("\\s", rgx) << std::endl; // 1

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

std::cout << "^\\\\s$" << std::endl;  // ^\\s$
std::cout << "\\s" << std::endl;      // \s

Можно также использовать raw-строки (см. разд. 7.5 и 8.6):

std::cout << R"(^\\s$)" << std::endl;         // ^\\s$
std::cout << R"...(^\\s$)..." << std::endl;   // ^\\s$
std::wcout << LR"(^\\s$)" << std::endl;       // ^\\s$

Метасимволы

Перечислим метасимволы, применяемые в регулярных выражениях:

  • ^ — привязка к началу строки;
  • $ — привязка к концу строки;
  • [] — позволяет указать символы, которые могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире;
  • [^] — позволяет указать символы, которые не могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире;
  • n|m — соответствует одному из фрагментов n или m:
// красная или красное, но не красный
std::regex rgx("красн((ая)|(ое))");
  • . (точка) — любой символ, кроме символа перевода строки (\n). Внутри квадратных скобок точка не имеет специального значения. Если нужно найти соответствие любому символу, включая символ перевода строки, то можно воспользоваться следующим кодом: [\s\S]. Класс \s означает любой пробельный символ, а класс \S — любой не пробельный символ. Пример:
std::regex rgx("^.+$");
std::cout << std::regex_search("10\n20", rgx) << std::endl; // 0
rgx = R"...(^[\s\S]+$)...";
std::cout << std::regex_search("10\n20", rgx) << std::endl; // 1

Привязку к началу и концу строки следует использовать, если строка должна полностью соответствовать регулярному выражению. Например, проверим, содержит ли строка только число:

std::regex rgx("^[0-9]+$");
std::cout << std::regex_match("2", rgx) << std::endl;  // 1
std::cout << std::regex_match("s2", rgx) << std::endl; // 0

Так как функция regex_match() проверяет полное соответствие строки шаблону, символы привязки к началу и концу строки (^ и $) в шаблоне можно не указывать, но так выглядит нагляднее. Если мы используем другие функции, например, regex_search(), то символы становятся важными. В этом примере, не указав привязку к началу и концу, мы получим соответствие шаблону любого числа внутри строки:

std::regex rgx("^[0-9]+$");
std::cout << std::regex_search("2", rgx) << std::endl;  // 1
std::cout << std::regex_search("s2", rgx) << std::endl; // 0
rgx = "[0-9]+";
std::cout << std::regex_search("s2", rgx) << std::endl; // 1

Если убрать привязку, то любая строка, содержащая хотя бы одну цифру, будет соответствовать шаблону [09]+:

std::regex rgx("[0-9]+");
std::cmatch m;
std::cout << std::regex_search("s245", m, rgx) << std::endl; // 1
if (!m.empty()) {
   std::cout << m[0] << std::endl; // 245
}

В этом примере мы не только проверили соответствие шаблону, но и получили найденное число, указав во втором параметре функции regex_search() объект класса cmatch. Если поиск закончился удачно, то метод empty() вернет значение false. Чтобы получить найденный фрагмент указываем индекс внутри квадратных скобок. Индекс 0 соответствует полному совпадению с шаблоном, а последующие индексы — фрагментам, заключенным внутри шаблона в круглые скобки (при наличии групп в шаблоне).

Можно указать привязку только к началу или только к концу строки:

std::regex rgx("^[0-9]+");
std::cmatch m;
std::regex_search("123Строка245", m, rgx);
if (!m.empty()) std::cout << m[0] << std::endl; // 123

В этом примере мы получили только первое число, которое расположено в начале строки, так как указана привязка к началу строки. Если указать привязку только к концу строки, то получим только последнее число:

std::regex rgx("[0-9]+$");
std::cmatch m;
std::regex_search("123Строка245", m, rgx);
if (!m.empty()) std::cout << m[0] << std::endl; // 245

В квадратных скобках [] можно указать символы, которые могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире:

  • [09] — соответствует цифре 0 или 9;
  • [0-9] — соответствует любой цифре от 0 до 9;
  • [абв] — соответствует буквам а, б и в;
  • [а-г] — соответствует буквам а, б, в и г;
  • [а-яё] — соответствует любой букве от а до я;
  • [АБВ] — соответствует буквам А, Б и В;
  • [А-ЯЁ] — соответствует любой букве от А до Я;
  • [а-яА-ЯёЁ] — соответствует любой русской букве в любом регистре;
  • [0-9а-яА-ЯёЁa-zA-Z] — любая цифра и любая русская или английская буква независимо от регистра.
Обратите внимание!

Значение можно инвертировать, если после первой скобки указать символ ^. Таким образом можно указать символы, которых не должно быть на этом месте в строке:

  • [^09] — не цифра 0 или 9;
  • [^0-9] — не цифра от 0 до 9;
  • [^а-яА-ЯёЁa-zA-Z] — не русская или английская буква в любом регистре.

Как вы уже знаете, точка теряет свое специальное значение, если ее заключить в квадратные скобки. Кроме того, внутри квадратных скобок могут встретиться символы, которые имеют специальное значение (например, ^ и -). Символ ^ теряет свое специальное значение, если он не расположен сразу после открывающей квадратной скобки:

std::regex rgx("[09^]");     // 0, 9 или ^

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

std::regex rgx("[09-]");     // 0, 9 или -

Все специальные символы можно сделать обычными, если перед ними указать символ \:

std::regex rgx(R"([0\-9])"); // 0, - или 9

Обратите внимание: мы добавили один символ \ только потому, что использовали raw-строку, в обычной строке символов должно быть два:

std::regex rgx("[0\\-9]");   // 0, - или 9

Стандартные классы

В регулярных выражениях допустимы следующие основные стандартные классы (полный список смотрите в документации):

  • \d — соответствует любой цифре;
  • \w — соответствует любой букве, цифре или символу подчеркивания. По умолчанию эквивалентно: [a-zA-Z0-9_];
  • \s — любой пробельный символ (пробел, табуляция, перевод страницы, новая строка или перевод каретки);
  • \D — не цифра. Эквивалентно: [^\d];
  • \W — эквивалентно: [^\w];
  • \S — не пробельный символ. Эквивалентно: [^\s].

Следует учитывать, что стандартные классы могут зависеть от различных флагов и трактоваться гораздо шире, чем мы указали. В итоге можно получить результат, который совсем не ожидался. Советую на практике использовать только классы \s и \S, а остальные заменять явным указанием диапазона символов внутри квадратных скобок (при этом не забывайте про букву ё):

std::regex rgx("[0-9]+");      // Вместо \d
rgx = "[a-zA-Zа-яА-ЯёЁ0-9_]+"; // Вместо \w

Получим все цифры из строки:

std::regex rgx("[0-9]");
std::smatch m;
std::string str("текст123 456");
while (std::regex_search(str, m, rgx)) {
   std::cout << m.str() << ' ';
   str = m.suffix().str();
} // 1 2 3 4 5 6
std::cout << std::endl;

В этом примере мы воспользовались функцией regex_search() в сочетании с классом smatch. Метод str() класса smatch возвращает фрагмент, полностью совпадающий с шаблоном регулярного выражения, а метод suffix() — возвращает оставшийся фрагмент, в котором поиск еще не выполнялся. Чтобы найти все фрагменты, соответствующие шаблону, мы указываем функцию regex_search() в качестве условия цикла while, а внутри цикла присваиваем переменной str оставшуюся часть строки. Как только функция regex_search() вернет значение false, цикл завершится.

Если нужно получить не цифры по отдельности, а числа, то после закрывающей квадратной скобки нужно указать символ +, означающий одно или большее число вхождений символа в строку:

std::regex rgx("[0-9]+");
std::string str("текст123 456");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << ' ';
   ++istart;
} // 123 456 
std::cout << std::endl;

В этом примере вместо функции regex_search() мы воспользовались двумя итераторами, реализуемыми с помощью класса sregex_iterator. Хотя, если просто изменить первую строку в предыдущем примере и добавить символ +, то результат будет точно таким же.

Внутри квадратных скобок можно также использовать стандартные классы регулярных выражений формата POSIX:

  • [:alnum:] — алфавитно-цифровые символы;
  • [:w:] — аналог класса \w;
  • [:alpha:] — буквенные символы;
  • [:lower:] — строчные буквы;
  • [:upper:] — прописные буквы;
  • [:digit:] или [:d:] — десятичные цифры;
  • [:xdigit:] — шестнадцатеричные цифры;
  • [:punct:] — знаки пунктуации;
  • [:blank:] — символы табуляции и пробелов;
  • [:space:] или [:s:] — пробельные символы;
  • [:cntrl:] — управляющие символы;
  • [:print:] — печатные символы;
  • [:graph:] — печатные символы, за исключением пробела.

Получим все числа из строки:

std::regex rgx("[[:digit:]]+");
std::string str("s123 s456 789s");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << ' ';
   ++istart;
} // 123 456 789
std::cout << std::endl;

Квантификаторы

Количество вхождений символа (или выражения) в строку задается с помощью квантификаторов:

  • {n}n вхождений символа в строку (шаблон [0-9]{2} соответствует двум вхождениям любой цифры);
  • {n,}n или более вхождений символа в строку (шаблон [0-9]{2,} соответствует двум и более вхождениям любой цифры);
  • {n,m} — не менее n и не более m вхождений символа в строку. Числа указываются через запятую без пробела. Например, шаблон [0-9]{2,4} соответствует от двух до четырех вхождениям любой цифры;
  • * — ноль или большее число вхождений символа в строку. Эквивалентно комбинации {0,};
  • + — одно или большее число вхождений символа в строку. Эквивалентно комбинации {1,};
  • ? — ни одного или одно вхождение символа в строку. Эквивалентно комбинации {0,1}.

«Жадность» квантификаторов

Все квантификаторы являются «жадными». При поиске соответствия ищется самая длинная подстрока, соответствующая шаблону, и не учитываются более короткие соответствия. Рассмотрим это на примере. Получим содержимое всех тегов <b>, вместе с тегами:

std::regex rgx("<b>.*</b>");
std::string str("<b>Text1</b>Text2<b>Text3</b>");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << std::endl;
   ++istart;
} // <b>Text1</b>Text2<b>Text3</b>

Вместо желаемого результата мы получили полностью строку. Чтобы ограничить «жадность» квантификатора необходимо после него указать символ ?:

std::regex rgx("<b>.*?</b>");
std::string str("<b>Text1</b>Text2<b>Text3</b>");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << std::endl;
   ++istart;
}

Этот код выведет то, что мы искали:

<b>Text1</b>
<b>Text3</b>

Группы

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

std::regex rgx("<b>(.*?)</b>");
std::string str("<b>Text1</b>Text2<b>Text3</b>");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str(0) << std::endl;
   std::cout << istart->str(1) << std::endl;
   ++istart;
}

Результат:

<b>Text1</b>
Text1
<b>Text3</b>
Text3

Обратите внимание на то, что мы указали индексы в методе str(). Если индекс не указан или равняется 0, то получим строку, полностью соответствующую шаблону. Индекс больше 0 указывает на порядковый номер круглых скобок внутри шаблона. Указав индекс 1, мы получили фрагмент, соответствующий (.*?), т. е. текст внутри тегов <b>. Можно также указать индекс внутри квадратных скобок:

std::smatch m = *istart;
std::cout << m[0] << std::endl;
std::cout << m[1] << std::endl;

Круглые скобки часто используются для группировки фрагментов внутри шаблона. В этих случаях не требуется, чтобы фрагмент запоминался и был доступен в результатах поиска. Пример:

std::regex rgx("([a-z]+((st)|(xt)))");
std::string str("test text");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
std::smatch m;
while (istart != iend) {
   m = *istart;
   for (size_t i = 1, size = m.size(); i < size; ++i) {
      std::cout << m.str(i) << '_';
   }
   std::cout << std::endl;
   ++istart;
}

Результат:

test_st_st__
text_xt__xt_

В этом примере мы получили список из четырех элементов для каждого совпадения. Все эти элементы соответствуют фрагментам, заключенным в шаблоне в круглые скобки. Первый элемент содержит фрагмент, расположенный в первых круглых скобках, второй — во вторых круглых скобках и т. д. Три последних элемента являются лишними. Чтобы избежать захвата фрагмента, после открывающей круглой скобки следует разместить символы ?:. Пример:

std::regex rgx("([a-z]+(?:(?:st)|(?:xt)))");
std::string str("test text");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
std::smatch m;
while (istart != iend) {
   m = *istart;
   for (size_t i = 1, size = m.size(); i < size; ++i) {
      std::cout << m.str(i) << ' ';
   }
   std::cout << std::endl;
   ++istart;
}

В результате список состоит только из фрагментов, полностью соответствующих регулярному выражению:

test 
text 

Обратите внимание на регулярное выражение в предыдущем примере:

"([a-z]+((st)|(xt)))"

Здесь мы использовали метасимвол |, который позволяет сделать выбор между альтернативными значениями. Выражение n|m соответствует одному из символов n или m. Пример:

красн((ая)|(ое)) — красная или красное, но не красный

Обратные ссылки

К найденному фрагменту в круглых скобках внутри шаблона можно обратиться с помощью механизма обратных ссылок. Для этого порядковый номер круглых скобок в шаблоне указывается после слэша — например, \1. Нумерация скобок внутри шаблона начинается с 1. Для примера получим текст между одинаковыми парными тегами:

std::regex rgx("<([a-zA-Z]+)[^>]*?>([\\s\\S]*?)</\\1>");
std::string str("<b>Text1</b>Text2<I>Text3</I>");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
std::smatch m;
while (istart != iend) {
   m = *istart;
   for (size_t i = 1, size = m.size(); i < size; ++i) {
      std::cout << m.str(i) << ' ';
   }
   std::cout << std::endl;
   ++istart;
}

Результат:

b Text1 
I Text3 

Просмотр вперед

Внутри круглых скобок могут быть расположены следующие конструкции:

  • (?=...) — положительный просмотр вперед. Выведем все слова, после которых расположена запятая:
std::regex rgx("\\w+(?=[,])");
std::string str("text1, text2, text3 text4");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << ' ';
   ++istart;
} // text1 text2
  • (?!...) — отрицательный просмотр вперед. Выведем все слова, после которых нет запятой:
std::regex rgx("[a-z]+[0-9](?![,])");
std::string str("text1, text2, text3 text4");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str() << ' ';
   ++istart;
} // text3 text4

Рассмотрим небольшой пример. Предположим необходимо получить все слова, расположенные после тире, причем перед тире и после слов должны следовать пробельные символы:

std::regex rgx("\\s\\-([a-z0-9]+)\\s");
std::string str("-word1 -word2 -word3 -word4 -word5");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str(1) << ' ';
   ++istart;
} // word2 word4 

Как видно из примера, мы получили только два слова вместо пяти. Первое и последнее слово не попали в результат, так как расположены в начале и в конце строки. Чтобы эти слова попали в результат, необходимо добавить альтернативные выборы: (^|\\s) — для начала строки и (\\s|$) — для конца строки. Чтобы найденные выражения внутри круглых скобок не попали в результат, следует добавить символы ?: после открывающей скобки:

std::regex rgx("(?:^|\\s)\\-([a-z0-9]+)(?:\\s|$)");
std::string str("-word1 -word2 -word3 -word4 -word5");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str(1) << ' ';
   ++istart;
} // word1 word3 word5 

Первое и последнее слово успешно попали в результат. Почему же слова word2 и word4 не попали в список совпадений? Ведь и перед тире есть пробел, и после слова есть пробел. Чтобы понять причину рассмотрим поиск по шагам. Первое слово успешно попадает в результат, так как перед тире расположено начало строки, и после слова есть пробел. После поиска указатель перемещается, и строка для дальнейшего поиска примет следующий вид:

"-word1 <Указатель>-word2 -word3 -word4 -word5"

Обратите внимание на то, что перед фрагментом –word2 больше нет пробела, и тире не расположено в начале строки. Поэтому следующим совпадением станет слово word3, и указатель снова будет перемещен:

"-word1 -word2 -word3 <Указатель>-word4 -word5"

Опять перед фрагментом –word4 нет пробела, и тире не расположено в начале строки. Поэтому следующим совпадением станет слово word5, и поиск будет завершен. Таким образом, слова word2 и word4 не попадают в результат, поскольку пробел до фрагмента уже был использован в предыдущем поиске. Чтобы этого избежать, следует воспользоваться положительным просмотром вперед (?=...):

std::regex rgx("(?:^|\\s)\\-([a-z0-9]+)(?=\\s|$)");
std::string str("-word1 -word2 -word3 -word4 -word5");
std::sregex_iterator istart(str.begin(), str.end(), rgx);
std::sregex_iterator iend;
while (istart != iend) {
   std::cout << istart->str(1) << ' ';
   ++istart;
} // word1 word2 word3 word4 word5

В этом примере мы заменили фрагмент (?:\\s|$) на (?=\\s|$). Поэтому все слова успешно попали в список совпадений.

Учебник C++ (Qt Creator и MinGW)
Учебник C++ (Qt Creator и MinGW) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов