17.3.3. Использование подвыражений

Схема в регулярном выражении зачастую содержит одно или несколько подвыражений (subexpression). Подвыражение — это часть схемы, которая сама имеет значение. Для обозначения подвыражения в регулярном выражении, как правило, используют круглые скобки.

Например, в схеме для поиска соответствий расширений файлов языка С++ (см. раздел 16.1.2) круглые скобки используются для группировки возможных расширений. Каждый раз, когда альтернативы группируются с использованием круглых скобок, одновременно объявляется, что эти альтернативы формируют подвыражение. Это выражение можно переписать так, чтобы оно предоставило доступ к имени файла, являющемуся той частью схемы, которая предшествует точке:

// r содержит два подвыражения:

// первое - часть имени файла перед точкой,

// второе - расширение файла

regex r("([[:alnum:]]+).(cpp|схх|cc)$", regex::icase);

Теперь в схеме два заключенных в скобки подвыражения:

• ([[:alnum:]]+) — представляет последовательность из одного или нескольких символов;

• (cpp|схх|cc) — представляет расширения файлов.

Теперь программу из раздела 16.1.2 можно переписать так (изменив оператора вывода), чтобы выводить только имя файла:

if (regex_search(filename, results, r))

 cout << results.str(1) << endl; // вывести первое подвыражение

В первоначальной программе для поиска схемы r в строке filename использовался вызов функции regex_search(), а также объект results класса smatch для содержания результата поиска соответствия. Если вызов успешен, выводится результат. Но в этой программе выводится str(1), т.е. соответствие для первого подвыражения.

Кроме информации об общем соответствии, объекты соответствия предоставляют доступ к каждому соответствию подвыражению в схеме. К соответствиям подвыражению обращаются по позиции. Первое соответствие подвыражению, расположенное в позиции 0, представляет соответствие для всей схемы. После него располагается каждое подвыражение. Следовательно, имя файла, являющееся первым подвыражением в схеме, находится в позиции 1, а расширение файла — в позиции 2.

Например, если именем файла будет foo.cpp, то results.str(0) содержит строку "foo.cpp"; results.str(1) — "foo", a results.str(2) — "cpp".

В этой программе требуется часть имени перед точкой, что является первым подвыражением, поэтому следует вывести results.str(1).

Подвыражения для проверки правильности данных

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

Прежде чем написать схему номеров телефона, необходимо рассмотреть еще несколько аспектов языка регулярных выражений на языке ECMAScript.

• {d} представляет одиночную цифру, а {d}{n} — последовательность из n цифр. (Например, {d}{3} соответствует последовательности из трех цифр.)

• Набор символов в квадратных скобках позволяет задать соответствие любому из трех символов. (Например, [-.] соответствует тире, точке или пробелу. Обратите внимание: у точки в квадратных скобках нет никакого специального смысла.)

• Компонент, следующий за символом '?', не обязательный. (Например, {d}{3}[-. ]?{d}{4} соответствует трем цифрам, сопровождаемым опциональными тире, точкой или пробелом и еще четырьмя цифрами. Этой схеме соответствовало бы 555-0132, или 555.0132, или 555 0132, или 5550132).

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

Поскольку наклонная черта влево является специальным символом в языке С++, когда он встречается в схеме, следует добавить вторую наклонную черту, чтобы указать языку С++, что имеется в виду символ . Следовательно, чтобы представить регулярное выражение {d}{3}, нужно написать {d}{3}.

Для проверки номеров телефонов следует обратиться к компонентам схемы. Например, необходимо проверить, что если номер использует открывающую круглую скобку для кода города, то он использует также закрывающую скобку после него. В результате такой номер, как (908.555.1800, следует отклонить.

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

// все выражение состоит из семи подвыражений: (ddd) разделитель ddd

// разделитель dddd

// подвыражения 1, 3, 4 и 6 опциональны; а 2, 5 и 7 содержат цифры

"(()?(d{3})())?([-. ])?(d{3})([-. ]?)(d{4})";

Поскольку схема использует круглые скобки, а также из-за использования наклонных черт, эту схему трудно прочитать (и написать!). Проще всего прочитать ее по каждому отдельному (заключенному в скобки) подвыражению.

1. (()? необязательная открывающая скобка для кода города.

2. (d{3}) код города.

3. ())? необязательная закрывающая скобка для кода города.

4. ([-. ])? необязательный разделитель после кода города.

5. (d{3}) следующие три цифры номера.

6. ([-. ])? другой необязательный разделитель.

7. (d{4}) последние четыре цифры номера.

Следующий код использует эту схему для чтения файла и находит данные, соответствующие общей схеме телефонных номеров. Для проверки допустимости формата номеров используется функция valid():

string phone =

 "(()?(d{3})())?([-. ])?(d{3})([-. ]?)(d{4})";

regex r(phone); // объект regex для поиска схемы

smatch m;

string s;

// прочитать все записи из входного файла

while (getline(cin, s)) {

 // для каждого подходящего номера телефона

 for (sregex_iterator it(s.begin(), s.end(), r), end_it;

                      it != end_it; ++it)

  // проверить допустимость формата номера

  if (valid(*it))

   cout << "valid: " << it->str() << endl;

  else

   cout << "not valid: " << it->str() << endl;

}

Операции с типом соответствия

Напишем функцию valid(), используя операции типа соответствия, приведенные в табл. 17.11. Не следует забывать, что схема pattern состоит из семи подвыражений. В результате каждый объект класса smatch будет содержать восемь элементов ssub_match. Элемент [0] представляет общее соответствие, а элементы [1] - [7] представляют каждое из соответствующих подвыражений.

Таблица 17.11. Операции с типом соответствия

Эти операции применимы к типам ssub_match, csub_match, wssub_match и wcsub_match matched Открытая логическая переменная-член, означающая соответствие объекта класса ssub_match first second Открытые переменные-члены, являющиеся итераторами на начало последовательности соответствия и ее следующий элемент после последнего. Если соответствия нет, то first и second равны length() Размер текущего объекта соответствия. Возвращает 0, если переменная-член matched содержит значение false str() Возвращает строку, содержащую соответствующую часть ввода. Возвращает пустую строку, если переменная-член matched содержит значение false s = ssub Преобразует объект ssub класса ssub_match в строку s. Эквивалент вызова s = ssub.str(). Оператор преобразования не является явным (см. раздел 14.9.1)

Когда происходит вызов функции valid(), известно, что общее соответствие имеется, но неизвестно, какие из необязательных подвыражений являются частью этого соответствия. Переменная-член matched класса ssub_match, соответствующая определенному подвыражению, содержит значение true, если это подвыражение является частью общего соответствия.

В правильном номере телефона код города либо полностью заключается в скобки, либо не заключается в них вообще. Поэтому действие функции valid() зависит от того, начинается ли номер с круглой скобки или нет:

bool valid(const smatch& m) {

 // если перед кодом города есть открывающая скобка

 if (m[1].matched)

  // за кодом города должна быть закрывающая скобка

  // и остальная часть номера непосредственно или через пробел

  return m[3].matched

         && (m[4].matched == 0 || m[4].str() == " ");

 else

  // здесь после кода города не может быть закрывающей скобки

  // но разделители между другими двумя компонентами должны быть

  // корректны

  return !m[3].matched

         && m[4].str() == m[6].str();

}

Начнем с проверки соответствия первому подвыражению (т.е. открывающей скобки). Это подвыражение находится в элементе m[1]. Если это соответствие есть, то номер начинается с открывающей скобки. В таком случае номер будет допустимым, только если подвыражение после кода города также будет соответствующим (т.е. будет закрывающая скобка после кода города). Кроме того, если скобки в начале номера корректны, то следующим символом должен быть пробел или первая цифра следующей части номера.

Если элемент m[1] не соответствует (т.е. открывающей скобки нет), то подвыражение после кода города также должно быть пустым. Если это так и если остальные разделители совпадают, то номер допустим, но не в противном случае.

Упражнения раздела 17.3.3

Упражнение 17.19. Почему можно вызывать функцию m[4].str() без предварительной проверки соответствия элемента m[4]?

Упражнение 17.20. Напишите собственную версию программы для проверки номеров телефонов.

Упражнение 17.21. Перепишите программу номеров телефонов из раздела 8.3.2 так, чтобы использовать функцию valid(), определенную в этом разделе.

Упражнение 17.22. Перепишите программу номеров телефонов так, чтобы она позволила разделять три части номера телефона любыми символами.

Упражнение 17.23. Напишите регулярное выражение для поиска почтовых индексов. У них может быть пять или девять цифр. Первые пять цифр могут быть отделены от остальных четырех тире.