JavaRush /Java блог /Java Developer /Регулярные выражения в Java, часть 2
Автор
Pavlo Plynko
Java-разработчик в CodeGym

Регулярные выражения в Java, часть 2

Статья из группы Java Developer
Предлагаем вашему вниманию перевод краткого руководства по регулярным выражениям в языке Java, написанного Джеффом Фрисеном (Jeff Friesen) для сайта javaworld. Для простоты чтения мы разделили статью на несколько частей. Регулярные выражения в Java, часть 2 - 1Регулярные выражения в Java, часть 1
Слияние нескольких диапазонов
Можно сливать несколько диапазонов в один диапазонный класс символов, размещая их бок о бок. Например, класс [a-zA-Z] соответствует всем латинским алфавитным символам в нижнем или верхнем регистре.

Слияние нескольких диапазонов

Можно сливать несколько диапазонов в один диапазонный класс символов путем размещения их бок о бок. Например, класс [a-zA-Z] соответствует всем латинским алфавитным символам в нижнем или верхнем регистре.

Объединение классов символов

Объединение классов символов состоит из нескольких вложенных классов символов и соответствует всем входящим в результирующее объединение символам. Например, класс [a-d[m-p]] соответствует символам от a до d и от m до p. Рассмотрим следующий пример: java RegexDemo [ab[c-e]] abcdef В этом примере будут найдены символы a, b, c, d и e, для которых есть соответствия в abcdef:

regex = [ab[c-e]]
input = abcdef
Found [a] starting at 0 and ending at 0
Found [b] starting at 1 and ending at 1
Found [c] starting at 2 and ending at 2
Found [d] starting at 3 and ending at 3
Found [e] starting at 4 and ending at 4

Пересечение классов символов

Пересечение классов символов состоит из символов, общих для всех вложенных классов и соответствует только общим символам. Например, класс [a-z&&[d-f]] соответствует символам d, e и f. Рассмотрим следующий пример: java RegexDemo "[aeiouy&&[y]]" party Обратите внимание, что на моей операционной системе Windows необходимы двойные кавычки, так как командная оболочка рассматривает & как разделитель команд. В этом примере будет найден только символ y, для которого есть соответствие в party:

regex = [aeiouy&&[y]]
input = party
Found [y] starting at 4 and ending at 4

Вычитание классов символов

Вычитание классов символов состоит из всех символов, кроме тех, которые содержатся во вложенных классах символов, и соответствует только этим остальным символам. Например, класс [a-z&&[^m-p]] соответствует символам от a до l и от q до z: java RegexDemo "[a-f&&[^a-c]&&[^e]]" abcdefg В этом примере будет найден символы d и f, для которых есть соответствия в abcdefg:

regex = [a-f&&[^a-c]&&[^e]]
input = abcdefg
Found [d] starting at 3 and ending at 3
Found [f] starting at 5 and ending at 5

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

Некоторые классы символов встречаются в регулярных выражениях достаточно часто, чтобы оправдать использование сокращенных обозначений. Класс Pattern предлагает в качестве подобных сокращений предопределенные классы символов. Вы можете воспользоваться ими, чтобы упростить свои регулярные выражения и минимизировать синтаксические ошибки. Имеется несколько категорий предопределенных классов символов: стандартные, POSIX, java.lang.Character и такие свойства Unicode, как сценарий, блок, категория и двоичное свойство. В следующем списке приведена только категория стандартных классов:
  • \d: Цифра. Эквивалентно [0-9].
  • \D: Нецифровой символ. Эквивалентно [^0-9].
  • \s: Пробельный символ. Эквивалентно [ \t\n\x0B\f\r].
  • \S: Не пробельный символ. Эквивалентно [^\s].
  • \w: Словообразующий символ. Эквивалентно [a-zA-Z_0-9].
  • \W: Не словообразующий символ. Эквивалентно [^\w].
В следующем примере используется предопределенный класс символов \w для описания всех словообразующих символов во входном тексте: java RegexDemo \w "aZ.8 _" Посмотрите внимательно на следующие результаты выполнения, показывающие, что символы точки и пробела не считаются словообразующим:

regex = \w
input = aZ.8 _
Found [a] starting at 0 and ending at 0
Found [Z] starting at 1 and ending at 1
Found [8] starting at 3 and ending at 3
Found [_] starting at 5 and ending at 5
Разделители строк
Документация по SDK класса Pattern описывает метасимвол точки как предопределенный класс символов, соответствующий любому символу, кроме разделителей строк (одно- или двухсимвольные последовательности, отмечающие конец строки). Исключение составляет режим dotall (который мы обсудим далее), в котором точка соответствуют и разделителям строк. Класс Pattern различает следующие разделители строк:
  • cимвол возврата каретки (\r);
  • cимвол новой строки (символ протяжки бумаги на одну строку) (\n);
  • cимвол возврата каретки, за которым непосредственно следует символ новой строки (\r\n);
  • cимвол следующей строки (\u0085);
  • cимвол разделения строк (\u2028);
  • cимвол разделения абзацев (\u2029)

Захватываемые группы

Захватываемая группа (capturing group) служит для сохранения найденного набора символов с целью дальнейшего использования при поиске по шаблону. Эта конструкция представляет собой последовательность символов, заключенную в метасимволы круглых скобок ( ( ) ). Все символы внутри захватываемой группы рассматриваются при поиске по шаблону как единое целое. Например, захватываемая группа (Java) объединяет буквы J, a, v и a в единое целое. Эта захватываемая группа находит все вхождения шаблона Java во входной текст. При каждом совпадении предыдущие сохраненные символы Java заменяются следующими. Захватываемые группы могут быть вложены в другие захватываемые группы. Например, в регулярном выражении (Java( language)) группа (language) вложена внутрь группы (Java). Каждой вложенной или не вложенной захватываемой группе присваивается свой номер, начиная с 1, причем нумерация идет слева направо. В предыдущем примере, (Java( language)) соответствует захватываемой группе номер 1, а (language) – захватываемой группе номер 2. В регулярном выражении (a)(b), (a) соответствует захватываемой группе номер 1, а (b) захватываемой группе номер 2. Регулярные выражения в Java, часть 2 - 2К сохраненным захватываемыми группами совпадениям можно позднее обратиться при помощи обратных ссылок. Задаваемая в виде символа обратной косой черты с последующим цифровым символом, соответствующим номеру захватываемой группы, обратная ссылка позволяет обратиться к символам захваченного группой текста. Наличие обратной ссылки приводит к обращению сопоставителя к сохраненному захватываемой группой результату поиска, на основе номера из неё, с последующим использованием символов из этого результата для попытки дальнейшего поиска. В следующем примере показано использование обратной ссылки для поиска грамматических ошибок в тексте: java RegexDemo "(Java( language)\2)" "The Java language language" В этом примере регулярное выражение (Java( language)\2) используется для поиска грамматической ошибки с дублированием слова language непосредственно после Java во входном тексте "The Java language language". В этом регулярном выражении заданы две захватываемые группы: номер 1 – (Java( language)\2), соответствующей Java language language и номер 2 – (language), соответствующей символу пробела, за которым следует language. Обратная ссылка \2 позволяет повторно обратиться к сохраненному результату группы номер 2, благодаря чему сопоставитель может выполнить поиск второго вхождения пробела, за которым следует language, непосредственно после первого вхождения пробела и language. Результаты работы сопоставителя RegexDemo представляют собой следующее:

regex = (Java( language)\2)
input = The Java language language
Found [Java language language] starting at 4 and ending at 25

Граничные сопоставители

Иногда бывает нужно выполнить сопоставление с шаблоном в начале строки, на границе слов, в конце текста и т.д. Сделать это можно с помощью одного из граничных сопоставителей класса Pattern, представляющих собой конструкции регулярных выражений для поиска совпадений в следующих местоположениях:
  • ^: Начало строки;
  • $: Конец строки;
  • \b: Граница слова;
  • \B: Граница псевдослова;
  • \A: Начало текста;
  • \G: Конец предыдущего совпадения;
  • \Z: Конец текста, не считая итогового разделителя строк (если таковой присутствует);
  • \z: Конец текста
В следующем примере используется метасимвол ^ граничного сопоставителя для поиска строк, начинающихся с The, за которым следует ноль или более словообразующих символов: java RegexDemo "^The\w*" Therefore Символ ^ указывает на то, что первые три символа входного текста должны соответствовать идущим один за другим символам шаблона T, h и e, за которыми может следовать любое число словообразующих символов. Вот результат выполнения:

regex = ^The\w*
input = Therefore
Found [Therefore] starting at 0 and ending at 8
Что произойдет, если изменить командную строку на java RegexDemo "^The\w*" " Therefore"? Совпадения найдено не будет, поскольку перед Therefore во входном тексте стоит символ пробела.

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

Иногда, при работе с граничными сопоставителями вы будете встречаться с совпадениями нулевой длины. Совпадение нулевой длины – это совпадение, не содержащее символов. Они могут встретиться в пустом входном тексте, в начале входного текста, после последнего символа входного текста и между любыми двумя символами этого текста. Совпадения нулевой длины легко распознать, поскольку они всегда начинаются и заканчиваются на одной и той же позиции. Рассмотрим следующий пример: java RegExDemo \b\b "Java is" В этом примере происходит поиск двух последовательных границ слова, его результаты выглядят следующим образом:

regex = \b\b
input = Java is
Found [] starting at 0 and ending at -1
Found [] starting at 4 and ending at 3
Found [] starting at 5 and ending at 4
Found [] starting at 7 and ending at 6
Мы видим в результатах несколько совпадений нулевой длины. Конечные позиции тут на единицу меньше начальных, поскольку в исходном коде RegexDemo в листинге 1 я указал end() – 1. Регулярные выражения в Java, часть 2 - 3

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

Квантификатор – это конструкция регулярного выражения, явно или неявно связывающая шаблон с числовым значением. Этот числовое значение определяет, сколько раз искать шаблон. Квантификаторы делятся на жадные, ленивые и сверхжадные:
  • Жадный квантификатор (?, * или +) предназначен для поиска самого длинного совпадения. Можно задать X? для поиска одного или менее вхождений X, X* для поиска нуля или более вхождений X, X+ для поиска одного или более вхождений X, X{n} для поиска n вхождений X, X{n,} для поиска не менее (а возможно, и более) n вхождений X и X{n,m} для поиска не менее n, но не более m вхождений X.
  • Ленивый квантификатор (??, *? или +?) предназначен для поиска самого короткого совпадения. Можно задать X?? для поиска одного или менее вхождений X, X*? для поиска нуля или более вхождений X, X+? для поиска одного или более вхождений X, X{n}? для поиска n вхождений X, X{n,}? для поиска не менее (а возможно, и более) n вхождений X и X{n,m}? для поиска не менее n, но не более m вхождений X.
  • Сверхжадный квантификатор (?+, *+ или ++) аналогичен жадному, за исключением того, что сверхжадный квантификатор выполняет только одну попытку найти самое длинное совпадение, в то время как жадный может выполнять несколько попыток. Можно задать X?+ для поиска одного или менее вхождений X, X*+ для поиска нуля или более вхождений X, X++ для поиска одного или более вхождений X, X{n}+ для поиска n вхождений X, X{n,}+ для поиска не менее (а возможно, и более) n вхождений X и X{n,m}+ для поиска не менее n, но не более m вхождений X.
Следующий пример иллюстрирует использование жадного квантификатора: java RegexDemo .*ox "fox box pox" Вот его результаты:

regex = .*ox
input = fox box pox
Found [fox box pox] starting at 0 and ending at 10
Жадный квантификатор (.*) находит самую длинную последовательность символов, завершающуюся на ox. Он поглощает весь входной текст, после чего откатывается вплоть до обнаружения того, что входной текст заканчивается этими символами. Рассмотрим теперь ленивый квантификатор: java RegexDemo .*?ox "fox box pox" Его результаты:

regex = .*?ox
input = fox box pox
Found [fox] starting at 0 and ending at 2
Found [ box] starting at 3 and ending at 6
Found [ pox] starting at 7 and ending at 10
Ленивый квантификатор (.*?) находит самую короткую последовательность символов, завершающуюся на ox. Он начинает с пустой строки и постепенно поглощает символы до тех пор, пока не находит соответствие. А затем продолжает работу вплоть до исчерпания входного текста. Наконец, рассмотрим сверхжадный квантификатор: java RegexDemo .*+ox "fox box pox" И вот его результаты:

regex = .*+ox
input = fox box pox
Сверхжадный квантификатор (.*+) не находит совпадений, поскольку он поглощает весь входной текст и не остается ничего, что могло бы соответствовать ox в конце регулярного выражения. В отличие от жадного квантификатора, сверхжадный квантификатор не выполняет откат назад.

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

Иногда при работе с квантификаторами вы будете сталкиваться с совпадениями нулевой длины. Например, использование следующего жадного квантификатора приводит к нескольким совпадениям нулевой длины: java RegexDemo a? abaa Результаты выполнения этого примера:

regex = a?
input = abaa
Found [a] starting at 0 and ending at 0
Found [] starting at 1 and ending at 0
Found [a] starting at 2 and ending at 2
Found [a] starting at 3 and ending at 3
Found [] starting at 4 and ending at 3
В результатах выполнения присутствуют пять совпадений. Хотя первое, третье и четвертое вполне ожидаемы (они соответствуют позициям трёх букв a в abaa), второе и пятое может вас удивить. Создается такое впечатление, будто они указывают, что a соответствует b и концу текста, но на самом деле всё не так. Регулярное выражение a? не ищет b в конце текста. Оно выполняет поиск наличия или отсутствия a. Когда a? не обнаруживает a, сообщает об этом в виде совпадения нулевой длины.

Вложенные флаговые выражения

Сопоставители принимают некоторые допущения по умолчанию, которые можно перекрыть при компиляции регулярного выражения в шаблон. Мы обсудим этот вопрос позже. Регулярное выражение позволяет перекрыть любое из умолчаний, используя вложенное флаговое выражение. Эта конструкция регулярного выражения задается в виде метасимволов круглых скобок вокруг метасимвола знака вопроса (?), с последующей латинской буквой в нижнем регистре. Класс Pattern понимает следующие вложенные флаговые выражения:
  • (?i): активирует нечувствительный к регистру поиск по шаблону. Например, при использовании команды java RegexDemo (?i)tree Treehouse последовательность символов Tree соответствует шаблону tree. По умолчанию используется поиск по шаблону с учетом регистра.
  • (?x): разрешает использование внутри шаблона пробельных символов и комментариев, начинающихся с метасимвола #. Сопоставитель будет игнорировать и те, и другие. Например, для java RegexDemo ".at(?x)#match hat, cat, and so on" matter последовательность символов mat соответствует шаблону .at. По умолчанию, пробельные символы и комментарии запрещены, сопоставитель рассматривает их как символы, участвующие в поиске.
  • (?s): активирует режим dotall, в котором метасимвол точки соответствует разделителям строк, помимо любого другого символа. Например, при команде java RegexDemo (?s). \n будет найден символ новой строки. По умолчанию используется противоположный dotall режим: разделители строк не будут находиться. Например, при команде Java RegexDemo . \n символ новой строки найден не будет.
  • (?m): активирует многострочный режим, при котором ^ соответствует началу, а $ – концу каждой строки. Например, java RegexDemo "(?m)^abc$" abc\nabc находит во входном тексте обе последовательности abc. По умолчанию используется однострочный режим: ^ соответствует началу всего входного текста, а $ — его концу. Например, java RegexDemo "^abc$" abc\nabc возвращает ответ об отсутствии совпадений.
  • (?u): активирует выравнивание регистра с учетом Unicode. Этот флаг, при совместном использовании с (?i), позволяет осуществлять поиск по шаблону без учета регистра в соответствии со стандартом Unicode. Настройка по умолчанию — поиск с учетом регистра и только по символам из набора US-ASCII.
  • (?d): активирует режим строк в стиле Unix, при котором сопоставитель распознает в контексте метасимволов ., ^ и $ только разделитель строк \n. По умолчанию используется режим строк в стиле не-Unix: сопоставитель распознает в контексте вышеупомянутых метасимволов все разделители строк.
Вложенные флаговые выражения напоминают захватываемые группы, поскольку их символы окружаются метасимволами круглых скобок. В отличие от захватываемых групп, вложенные флаговые выражения – пример не захватываемых групп, представляющих собой конструкцию регулярных выражений, не захватывающую текстовые символы. Они определяются как последовательности символов, окруженные метасимволами круглых скобок.
Указание нескольких вложенных флаговых выражений
Существует возможность указания нескольких вложенных флаговых выражений в регулярном выражении путем или расположения их бок о бок ((?m)(?i))), или размещения последовательно определяющих их букв ((?mi)).

Заключение

Как вы уже, наверное, поняли, регулярные выражения чрезвычайно полезны и становятся еще более полезны по мере освоения вами нюансов их синтаксиса. До сих пор я познакомил вас с основами регулярных выражений и классом Pattern. Во второй части мы заглянем глубже в API Regex и изучим методы классов Pattern, Matcher и PatternSyntaxException. Я также продемонстрирую вам два практических приложения API Regex, которые вы сможете сразу же использовать в своих программах. Регулярные выражения в Java, часть 3 Регулярные выражения в Java, часть 4 Регулярные выражения в Java, часть 5
Комментарии (36)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Islam Yunusov Уровень 31
18 марта 2024
"номер 1 – (Java( language)\2), соответствующей Java language language и номер 2 – (language), соответствующей символу пробела, за которым следует language" Чего? Какой ещё пробел? Какой language?
Сергей Удодов Уровень 30
18 августа 2022
Тот случай когда до прочтения статьи ты знал больше, чем после😂
Roman Уровень 33
28 октября 2021
Почему-то не отработала эта схема (с двойной косой чертой тоже не отработала). Ничего не вывела String result = "The Java language language"; Pattern pattern = Pattern.compile("Java( language)\2"); Matcher matcher = pattern.matcher(result); while (matcher.find()) { int start=matcher.start(); int end=matcher.end(); System.out.println(result.substring(start,end)); } и на схему "\\b\\b" - тоже никак не отреагировала. а при "(?m)^abc$" abc\nabc вывела только одно abc
Roman Уровень 33
28 октября 2021
Долго ржал над картинкой "reqular expression". До программиста ещё топать и топать, но основа проф. деформациям уже видимо заложена.
Daniil kukushkin Уровень 35
24 ноября 2020
Найти бы наглядный пример использования этого вложенго флага (?u).
Николай Т. Уровень 40
23 октября 2020
Режим dota 2? 😅
MR Уровень 22
2 сентября 2020
Что-то я не поняла смысл квантификаторов. Везде одни и те же действия, что в ленивом, что в жадном. что в сверхжадном. Нюансы? Не, не уловила.
Павел Бойко Уровень 41
26 августа 2020
зачем так сложно "[aeiouy&&[y]]", если можно просто "[y]" ? Очень похоже на ситуацию, когда одно ТЗ пишут 2 аналитика.
Ainika Уровень 25
7 июля 2020
Я уже читала лекции в рамках курса и все примеры тестировала на regex101.com, Читала статьи на сторонних ресурсах, смотрела видео, решала задачи на RegexOne, решала наши же задачи, но нигде не было так сложно как тут. Не могу понять в чем дело - до этого везде были азы и объяснено поверхностно, а в этой серии статей - глубоко и основательно? Насколько детально нужно изучить Regex? Применяется ли на практике так серьезно? Или у меня теперь действительно 2 проблемы? :)))