Перейти к основному содержимому

Просмотр вперед и назад в регулярных выражениях Java

· 4 мин. чтения

1. Обзор

Иногда мы можем столкнуться с трудностями при сопоставлении строки с регулярным выражением . Например, мы можем не знать, что именно мы хотим сопоставить, но мы можем осознавать его окружение, например, что находится непосредственно перед ним или чего не хватает после него. В этих случаях мы можем использовать утверждения обхода. Эти выражения называются утверждениями, потому что они только указывают, является ли что-то совпадением или нет, но не включаются в результат.

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

2. Положительный взгляд вперед

Допустим, мы хотели бы проанализировать импорт java-файлов. Во-первых, давайте найдем операторы импорта, которые являются статическими , проверив, что ключевое слово static следует за ключевым словом import .

Давайте воспользуемся утверждением положительного просмотра вперед с синтаксисом (?=criteria) в нашем выражении, чтобы сопоставить группу статических символов после импорта нашего основного выражения :

Pattern pattern = Pattern.compile("import (?=static).+");

Matcher matcher = pattern
.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;");
assertTrue(matcher.find());
assertEquals("import static org.junit.jupiter.api.Assertions.assertEquals;", matcher.group());

assertFalse(pattern.matcher("import java.util.regex.Matcher;").find());

3. Отрицательный прогноз

Далее давайте сделаем прямо противоположное предыдущему примеру и поищем операторы импорта, которые не являются статическими . Давайте сделаем это, проверив, что ключевое слово static не следует за ключевым словом import .

Давайте воспользуемся отрицательным прогнозным утверждением с синтаксисом (?!criteria) в нашем выражении, чтобы гарантировать, что группа статических символов не может совпадать после импорта нашего основного выражения :

Pattern pattern = Pattern.compile("import (?!static).+");

Matcher matcher = pattern.matcher("import java.util.regex.Matcher;");
assertTrue(matcher.find());
assertEquals("import java.util.regex.Matcher;", matcher.group());

assertFalse(pattern
.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;").find());

4. Ограничения Lookbehind в Java

Вплоть до Java 8 мы могли столкнуться с ограничением, заключающимся в том, что несвязанные квантификаторы, такие как + и * , не разрешены в утверждении просмотра назад. То есть, например, следующие утверждения вызовут PatternSyntaxException вплоть до Java 8:

  • (?<!fo+)bar , где мы не хотим сопоставлять bar , если перед ним стоит fo с одним или несколькими символами o
  • (?<!fo*)bar , где мы не хотим сопоставлять bar , если ему предшествует символ f , за которым следует ноль или более символов o
  • (?<!fo{2,})bar , где мы не хотим сопоставлять bar , если foo с двумя или более символами o стоит перед ним

В качестве обходного пути мы можем использовать квантификатор фигурных скобок с указанным верхним пределом, например (?<!fo{2,4})bar , где мы максимизируем количество символов o , следующих за символом f , до 4.

Начиная с Java 9, мы можем использовать несвязанные квантификаторы в просмотре назад. Однако из-за потребления памяти реализацией регулярных выражений по-прежнему рекомендуется использовать квантификаторы в ретроспективных просмотрах только с разумным верхним пределом, например (?<!fo{2,20})bar вместо (?<!fo{ 2,2000})бар .

5. Положительный взгляд назад

Допустим, мы хотели бы различать импорт JUnit 4 и JUnit 5 в нашем анализе. Во-первых, давайте проверим, получен ли оператор импорта для метода assertEquals из пакета jupiter .

Давайте используем положительное ретроспективное утверждение с синтаксисом (?<=criteria) в нашем выражении, чтобы сопоставить группу символов jupiter перед нашим основным выражением .* assertEquals :

Pattern pattern = Pattern.compile(".*(?<=jupiter).*assertEquals;");

Matcher matcher = pattern
.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;");
assertTrue(matcher.find());
assertEquals("import static org.junit.jupiter.api.Assertions.assertEquals;", matcher.group());

assertFalse(pattern.matcher("import static org.junit.Assert.assertEquals;").find());

6. Отрицательный взгляд назад

Далее, давайте сделаем прямую противоположность предыдущему примеру и поищем операторы импорта, которые не входят в пакет jupiter .

Для этого давайте воспользуемся отрицательным просмотром назад с синтаксисом (?<!criteria) в нашем выражении, чтобы гарантировать, что группа символов jupiter.{0,30} не может совпасть перед нашим основным выражением assertEquals :

Pattern pattern = Pattern.compile(".*(?<!jupiter.{0,30})assertEquals;");

Matcher matcher = pattern.matcher("import static org.junit.Assert.assertEquals;");
assertTrue(matcher.find());
assertEquals("import static org.junit.Assert.assertEquals;", matcher.group());

assertFalse(pattern
.matcher("import static org.junit.jupiter.api.Assertions.assertEquals;").find());

7. Заключение

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

Как всегда, исходный код этой статьи доступен на GitHub .