Основы регулярных выражений в Java. Часть 3

13-09-18 Java Java, RegExp 0

Продолжаем изучение регулярных выражений.
Предыдущие уроки:

В этой статье мы рассмотрим предопределенные классы символов, а также квантификацию (поиск последовательностей).

Предопределенные классы символов

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

Конструкция Описание
. Любой символ
\d Цифра: [0-9]
\D Не цифра: [^0-9]
\s Символ пробела: [ \t\n\x0B\f\r]
\S Не пробел: [^\s]
\w Буква, цифра или подчеркивание: [a-zA-Z_0-9]
\W Отрицание предыдущего: [^\w]

В этой таблице конструкции в левой колонке — сокращенное представление выражений из правой колонки. Например, \d означает цифру (0-9), \w означает любую заглавную или прописную букву, символ подчеркивания или цифру). Используйте предопределенные классы символов, где это возможно. Это сделает ваш код проще для чтения и исправления ошибок.

Конструкции, начинающиеся с обратного слеша, называются экранированными или защищенными. В предыдущих статьях мы уже говорили об экранировании специальных символов обратным слешем или символами \Q и \E для использования их в качестве обычных символов. Если вы используете обратный слеш с обычными символами (литералами), то необходимо экранировать этот бэк слеш, чтобы выражение скомпилировалось.

private final String REGEX = "\\d"; // цифра

В этом примере \d — регулярное выражениеs; дополнительный обратный слеш необходим для того, чтобы программа скомпилировалась. Наша тестовая программа читает регулярные выражения прямо из консоли, поэтому дополнительный слеш не нужен.

Следующий пример демонстрирует использование предопределенных символьных классов:

Enter your regex: .
Enter input string to search: @
I found the text "@" starting at index 0 and ending at index 1.

Enter your regex: .
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.

Enter your regex: .
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \d
Enter input string to search: 1
I found the text "1" starting at index 0 and ending at index 1.

Enter your regex: \d
Enter input string to search: a
No match found.

Enter your regex: \D
Enter input string to search: 1
No match found.

Enter your regex: \D
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \s
Enter input string to search:
I found the text " " starting at index 0 and ending at index 1.

Enter your regex: \s
Enter input string to search: a
No match found.

Enter your regex: \S
Enter input string to search:
No match found.

Enter your regex: \S
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \w
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Enter your regex: \w
Enter input string to search: !
No match found.

Enter your regex: \W
Enter input string to search: a
No match found.

Enter your regex: \W
Enter input string to search: !
I found the text "!" starting at index 0 and ending at index 1.

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

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

Жадные Ленивые Ревнивые (сверхжадные) Значение
X? X?? X?+ X присутствует один раз или отсутствует
X* X*? X*+ X, отсутствует или есть последовательность
X+ X+? X++ X, последовательность из одного или более символов
X{n} X{n}? X{n}+ X, последовательность из n символов X
X{n,} X{n,}? X{n,}+ X, последовательность минимум из n символов
X{n,m} X{n,m}? X{n,m}+ X, последовательность из n, но не более m символов

Квантификаторы позволяют задавать количество вхождений символа в строку. Рассмотрим подробнее тонкости работы жадных, ленивых и очень жадных квантификаторов. На первый взгляд может показаться, что квантификаторы X?X?? и X?+ работают одинаково: «X есть один раз или нет совсем». Есть небольшие отличия в реализации этих квантификаторов, которые мы рассмотрим ниже.

Совпадения нулевой длины

Начнем с жадного. Напишем три разных регулярных выражения: буква «a» со специальными символами ?или +. Посмотрим, что произойдет если тестировать эти регулярки на пустой строке:

Enter your regex: a?
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a*
Enter input string to search:
I found the text "" starting at index 0 and ending at index 0.

Enter your regex: a+
Enter input string to search:
No match found.

В примере выше, поиск был удачным в первых двух случаях, потому что выражения a? и a* допускают отсутствие символа a в строке. Также обратите внимание, что начальный и последний индексы совпадения одинаковы (0). Так как входная строка не имеет длины, программа находит ничего 🙂 на первой же позиции. Этот случай называется совпадением нулевой длины. Такие совпадения встречаются в нескольких случаях: при пустой входной строке, в начале входной строки, после последнего символа строки или между символами строки. Совпадения нулевой длины легко обнаружить: они начинаются и заканчиваются на одной и той же позиции.

Рассмотрим еще несколько примеров совпадений нулевой длины.

Let’s explore zero-length matches with a few more examples. Изменим входную строку на символ «a» и наблюдаем интересный эффект:

Enter your regex: a?
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a*
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.

Enter your regex: a+
Enter input string to search: a
I found the text "a" starting at index 0 and ending at index 1.

Все три квантификатора нашли символ «a», но первые два, которые допускают отсутствие символа, нашли совпадение нулевой длины на позиции 1 — после последнего символа строки. Так происходит, потому что программа воспринимает символ «a» как строку и «бежит» по ней пока совпадения не закончатся. В зависимости от используемого квантификатора, программа будет или не будет находить «ничего» в конце строки.

Теперь изменим входную строку на последовательность из пяти букв «a»:

Enter your regex: a?
Enter input string to search: aaaaa
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 1 and ending at index 2.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "a" starting at index 3 and ending at index 4.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a*
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.
I found the text "" starting at index 5 and ending at index 5.

Enter your regex: a+
Enter input string to search: aaaaa
I found the text "aaaaa" starting at index 0 and ending at index 5.

Регулярное выражение a? находит совпадение для каждой буквы в строке отдельно. Выражение a* находит два совпадения: последовательность символов «a»‘ и совпадение нулевой длины на 5 позиции. И, наконец, регулярное выражение a+ находит только последовательность символов «a», не находя при этом «ничего» 🙂

Что же будет происходить, если на вход подать строку, содержащую разные символы? Например, «ababaaaab»:

Enter your regex: a?
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "a" starting at index 4 and ending at index 5.
I found the text "a" starting at index 5 and ending at index 6.
I found the text "a" starting at index 6 and ending at index 7.
I found the text "a" starting at index 7 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a*
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "" starting at index 1 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "" starting at index 3 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.
I found the text "" starting at index 8 and ending at index 8.
I found the text "" starting at index 9 and ending at index 9.

Enter your regex: a+
Enter input string to search: ababaaaab
I found the text "a" starting at index 0 and ending at index 1.
I found the text "a" starting at index 2 and ending at index 3.
I found the text "aaaa" starting at index 4 and ending at index 8.

Символ «b» находится на 1, 3 и 8 позициях и программа находит совпадения нулевой длины на этих позициях. Регулярное выражение a? не обращает внимания на «b», а просто ищет присутствие (или отсутствие) символа «a». Если квантификатор допускает отсутствие «a» все символы в строке отличные от «a» будут показаны как совпадение нулевой длины.

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

Enter your regex: a{3}
Enter input string to search: aa
No match found.

Enter your regex: a{3}
Enter input string to search: aaa
I found the text "aaa" starting at index 0 and ending at index 3.

Enter your regex: a{3}
Enter input string to search: aaaa
I found the text "aaa" starting at index 0 and ending at index 3.

Регулярное выражение a{3} ищет последовательность из трех символов «a». В первой строке ничего не найдено, потому что в строке недостаточно символов a. Вторая содержит 3 символа, которые и находит программа. Третий тест также находит совпадение в начале строки. Все что находится за 3 символом не удовлетворяет регулярному выражению, в коде ниже — удовлетворяет и найдутся несколько совпадений:

Enter your regex: a{3}
Enter input string to search: aaaaaaaaa
I found the text "aaa" starting at index 0 and ending at index 3.
I found the text "aaa" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.

Для указания минимальной длины последовательности — используйте:

Enter your regex: a{3,}
Enter input string to search: aaaaaaaaa
I found the text "aaaaaaaaa" starting at index 0 and ending at index 9.

В этом примере программа находит только одно совпадение, потому что строка удовлетворяет требованию минимальной длины последовательности (3) символов «a».

Наконец, задание максимальной длины последовательности:

Enter your regex: a{3,6} // хотя бы 3 (но не больше 6) символов a
Enter input string to search: aaaaaaaaa
I found the text "aaaaaa" starting at index 0 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.

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

Использование групп символов и классов с квантификаторами

До этого момента мы тестировали квантификаторы на строках, содержащих один и тот же символ. Квантификаторы распространяют свое действие только на один символ, поэтому регулярное выражение «abc+» будет находить стоки содержащие «ab» и «c» один или более раз. Оно не будет означать «abc» один или более раз. Но квантификаторы могут применяться вместе с группами и классами символов, например, [abc]+ (a или b или c, один или более раз) или (abc)+ («abc» один или более раз).

Найдем группу символов (dog), три раза в строке:

Enter your regex: (dog){3}
Enter input string to search: dogdogdogdogdogdog
I found the text "dogdogdog" starting at index 0 and ending at index 9.
I found the text "dogdogdog" starting at index 9 and ending at index 18.

Enter your regex: dog{3}
Enter input string to search: dogdogdogdogdogdog
No match found.

В первом примере программа находит совпадение, т.к. квантификатор распространяется на группу символов. Если убрать скобки — квантификатор {3} будет распространяться только на букву «g».

Так же можно применять квантификаторы с классами символов:

Enter your regex: [abc]{3}
Enter input string to search: abccabaaaccbbbc
I found the text "abc" starting at index 0 and ending at index 3.
I found the text "cab" starting at index 3 and ending at index 6.
I found the text "aaa" starting at index 6 and ending at index 9.
I found the text "ccb" starting at index 9 and ending at index 12.
I found the text "bbc" starting at index 12 and ending at index 15.

Enter your regex: abc{3}
Enter input string to search: abccabaaaccbbbc
No match found.

Квантификатор {3} распространяет действие на класс символов в скобках в первом примере, а во втором — только на символ «c».

Различия жадных, ленивых и сверхжадных квантификаторов

Есть небольшие различия между жадными (greedy), ленивыми (reluctant) и ревнивыми (possessive) квантификаторами.

Жадные квантификаторы названы так, потому что пытаются найти максимально длинное совпадение: сначала программа пытается «съесть» всю строку, если совпадение не найдено, то отбрасывается один символ и поиск повторяется, пока не будет найдено совпадение или не останется больше символов.

Ленивые — наоборот, начинают с начала строки, добавляя символ за символом, пока не найдут совпадение.

Наконец, ревнивая квантификация просматривает сразу всю строку единожды, не убирая символы, как в жадной.

Для демонстрации будем использовать строку xfooxxxxxxfoo.

Enter your regex: .*foo  // жадная квантификация
Enter input string to search: xfooxxxxxxfoo
I found the text "xfooxxxxxxfoo" starting at index 0 and ending at index 13.

Enter your regex: .*?foo  // ленивая квантификация
Enter input string to search: xfooxxxxxxfoo
I found the text "xfoo" starting at index 0 and ending at index 4.
I found the text "xxxxxxfoo" starting at index 4 and ending at index 13.

Enter your regex: .*+foo // ревнивая квантификация
Enter input string to search: xfooxxxxxxfoo
No match found.

Первый пример использует жадный квантификатор .* для поиска любого символа, 0 или более раз, за которым расположены символы "f" "o" "o". Так как кантификатор жадный — найденное совпадение содержит всю строку. Жадный квантификатор не найдет все совпадения в строке, т.к. на первом шаге, просмотрев всю строку, он найдет совпадение и закончит работу.

Второй пример — ленивый и начинает с начала строки, добавляя символ за символом. Начинается работа программы с проверки «пустоты», но т.к. последовательности «foo» нет в начале строки, поиск продолжается с добавление символа «x», после которого будет найдено первое совпадение между индексами 0 и 4. Поиск продолжается до конца строки и второе совпадение будет найдено между индексами 4 и 13.

Третий пример не находит совпадений потому что квантификатор ревнивый. В этом случае регулярное выражение .*+ «съело» всю строку не оставив ничего для «foo». Используйте ревнивый квантификатор, когда нужно отбросить все не нужное в строке, он будет эффективнее эквивалентного жадного квантификатора.

На этом всё! 🙂

Хочешь получать статьи на почту?

Подпишись на обновления!
* Ваш email не будет разглашен/продан. Вы сможете отписаться в любое время.

Нет комментариев

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

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