Рубрики
Без рубрики

Руководство По API Регулярных Выражений Java

Практическое руководство по API регулярных выражений в Java.

Автор оригинала: baeldung.

1. Обзор

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

В мире регулярных выражений существует множество различных вариантов на выбор, таких как grep, Perl, Python, PHP, awk и многое другое.

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

2. Настройка

Чтобы использовать регулярные выражения в Java, нам не нужна специальная настройка. JDK содержит специальный пакет java.util.regex , полностью посвященный операциям с регулярными выражениями. Нам нужно только импортировать его в наш код.

Более того, java.lang.Класс String также имеет встроенную поддержку регулярных выражений, которую мы обычно используем в нашем коде.

3. Пакет регулярных выражений Java

Пакет java.util.regex состоит из трех классов: Pattern, Matcher и PatternSyntaxException :

  • Pattern object-это скомпилированное регулярное выражение. Класс Pattern не предоставляет общедоступных конструкторов. Чтобы создать шаблон, мы должны сначала вызвать один из его общедоступных статических методов compile , который затем вернет объект Pattern . Эти методы принимают регулярное выражение в качестве первого аргумента.
  • Сопоставитель объект интерпретирует шаблон и выполняет операции сопоставления с входной строкой . Он также не определяет публичных конструкторов. Мы получаем объект Matcher , вызывая метод matches для объекта Pattern .
  • PatternSyntaxException объект-это непроверенное исключение, указывающее на синтаксическую ошибку в шаблоне регулярного выражения.

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

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

4. Простой Пример

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

Наиболее простой формой сопоставления шаблонов, поддерживаемой java.util.regex API, является сопоставление строки литерала . Например, если регулярное выражение foo и входная Строка является foo , совпадение будет успешным, потому что Строки идентичны:

@Test
public void givenText_whenSimpleRegexMatches_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foo");
 
    assertTrue(matcher.find());
}

Сначала мы создаем объект Pattern , вызывая его статический метод compile и передавая ему шаблон, который мы хотим использовать.

Затем мы создаем объект Matcher , вызывая метод Pattern объекта matcher и передавая ему текст, который мы хотим проверить на совпадения.

После этого мы вызываем метод find в объекте Matcher.

Метод find продолжает продвигаться по входному тексту и возвращает true для каждого совпадения, поэтому мы также можем использовать его для поиска количества совпадений:

@Test
public void givenText_whenSimpleRegexMatchesTwice_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foofoo");
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
 
    assertEquals(matches, 2);
}

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

public static int runTest(String regex, String text) {
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
    return matches;
}

Когда мы получаем 0 совпадений, тест должен провалиться, в противном случае он должен пройти.

5. Мета-символы

Метасимволы влияют на способ сопоставления шаблона, добавляя логику в шаблон поиска. Java API поддерживает несколько метасимволов, наиболее простым из которых является точка “.” , которая соответствует любому символу:

@Test
public void givenText_whenMatchesWithDotMetach_thenCorrect() {
    int matches = runTest(".", "foo");
    
    assertTrue(matches > 0);
}

Учитывая предыдущий пример, где регулярное выражение foo соответствовало тексту foo , а также food два раза. Если бы мы использовали метасимвол точки в регулярном выражении, мы не получили бы двух совпадений во втором случае:

@Test
public void givenRepeatedText_whenMatchesOnceWithDotMetach_thenCorrect() {
    int matches= runTest("foo.", "foofoo");
 
    assertEquals(matches, 1);
}

Обратите внимание на точку после foo в регулярном выражении. Сопоставитель соответствует каждому тексту, которому предшествует foo , поскольку последняя часть точки означает любой символ после. Таким образом , после нахождения первого для , остальные рассматриваются как любой символ. Вот почему существует только одно совпадение.

API поддерживает несколько других мета-символов <([{\^-=$!|]})?*+.> которые мы рассмотрим далее в этой статье.

6. Классы персонажей

Просматривая официальную спецификацию класса Pattern , мы обнаружим краткое описание поддерживаемых конструкций регулярных выражений. В классах символов у нас есть около 6 конструкций.

6.1. ИЛИ Класс

Построено как [abc] . Любой из элементов в наборе сопоставляется:

@Test
public void givenORSet_whenMatchesAny_thenCorrect() {
    int matches = runTest("[abc]", "b");
 
    assertEquals(matches, 1);
}

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

@Test
public void givenORSet_whenMatchesAnyAndAll_thenCorrect() {
    int matches = runTest("[abc]", "cab");
 
    assertEquals(matches, 3);
}

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

@Test
public void givenORSet_whenMatchesAllCombinations_thenCorrect() {
    int matches = runTest("[bcr]at", "bat cat rat");
 
    assertEquals(matches, 3);
}

6.2. НИ Класс

Вышеуказанный набор отрицается путем добавления каретки в качестве первого элемента:

@Test
public void givenNORSet_whenMatchesNon_thenCorrect() {
    int matches = runTest("[^abc]", "g");
 
    assertTrue(matches > 0);
}

Еще один случай:

@Test
public void givenNORSet_whenMatchesAllExceptElements_thenCorrect() {
    int matches = runTest("[^bcr]at", "sat mat eat");
 
    assertTrue(matches > 0);
}

6.3. Класс дальности

Мы можем определить класс, который определяет диапазон, в который должен попадать сопоставленный текст, используя дефис ( -), аналогично, мы также можем отрицать диапазон.

Соответствие прописных букв:

@Test
public void givenUpperCaseRange_whenMatchesUpperCase_
  thenCorrect() {
    int matches = runTest(
      "[A-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

Соответствие строчных букв:

@Test
public void givenLowerCaseRange_whenMatchesLowerCase_
  thenCorrect() {
    int matches = runTest(
      "[a-z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 26);
}

Совпадение как прописных, так и строчных букв:

@Test
public void givenBothLowerAndUpperCaseRange_
  whenMatchesAllLetters_thenCorrect() {
    int matches = runTest(
      "[a-zA-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 28);
}

Соответствие заданному диапазону чисел:

@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect() {
    int matches = runTest(
      "[1-5]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

Сопоставление другого диапазона чисел:

@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect2(){
    int matches = runTest(
      "[30-35]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 1);
}

6.4. Класс Объединения

Класс символов объединения является результатом объединения двух или более классов символов:

@Test
public void givenTwoSets_whenMatchesUnion_thenCorrect() {
    int matches = runTest("[1-3[7-9]]", "123456789");
 
    assertEquals(matches, 6);
}

Приведенный выше тест будет соответствовать только 6 из 9 целых чисел, потому что набор объединения пропускает 4, 5 и 6.

6.5. Класс пересечения

Подобно классу объединения, этот класс является результатом выбора общих элементов между двумя или более наборами. Чтобы применить пересечение, мы используем && :

@Test
public void givenTwoSets_whenMatchesIntersection_thenCorrect() {
    int matches = runTest("[1-6&&[3-9]]", "123456789");
 
    assertEquals(matches, 4);
}

Мы получаем 4 совпадения, потому что пересечение двух множеств имеет только 4 элемента.

6.6. Класс вычитания

Мы можем использовать вычитание для отрицания одного или нескольких классов символов, например, для сопоставления набора нечетных десятичных чисел:

@Test
public void givenSetWithSubtraction_whenMatchesAccurately_thenCorrect() {
    int matches = runTest("[0-9&&[^2468]]", "123456789");
 
    assertEquals(matches, 5);
}

Только 1,3,5,7,9 будут совпадать.

7. Предопределенные Классы Символов

API регулярных выражений Java также принимает предопределенные классы символов. Некоторые из вышеперечисленных классов символов могут быть выражены в более короткой форме, хотя и делают код менее интуитивно понятным. Одним из особых аспектов Java-версии этого регулярного выражения является escape-символ.

Как мы увидим, большинство символов будут начинаться с обратной косой черты, которая имеет особое значение в Java. Чтобы они были скомпилированы классом Pattern – начальная обратная косая черта должна быть экранирована, т. е. \d становится \\d .

Совпадающие цифры, эквивалентные [0-9] :

@Test
public void givenDigits_whenMatches_thenCorrect() {
    int matches = runTest("\\d", "123");
 
    assertEquals(matches, 3);
}

Совпадающие не-цифры, эквивалентные [^0-9] :

@Test
public void givenNonDigits_whenMatches_thenCorrect() {
    int mathces = runTest("\\D", "a6c");
 
    assertEquals(matches, 2);
}

Соответствующие пробелы:

@Test
public void givenWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\s", "a c");
 
    assertEquals(matches, 1);
}

Соответствие небелому пространству:

@Test
public void givenNonWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\S", "a c");
 
    assertEquals(matches, 2);
}

Сопоставление символа слова, эквивалентного [a-zA-Z_0-9] :

@Test
public void givenWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\w", "hi!");
 
    assertEquals(matches, 2);
}

Сопоставление несловесного символа:

@Test
public void givenNonWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\W", "hi!");
 
    assertEquals(matches, 1);
}

8. Кванторы

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

Чтобы сопоставить текст нулю или один раз, мы используем ? квантификатор:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\a?", "hi");
 
    assertEquals(matches, 3);
}

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

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\a{0,1}", "hi");
 
    assertEquals(matches, 3);
}

В этом примере вводится понятие совпадений нулевой длины. Бывает так, что если количественный порог соответствия равен нулю, он всегда соответствует всему тексту, включая пустую строку | в конце каждого ввода. Это означает, что даже если входные данные пусты, они вернут одно совпадение нулевой длины.

Это объясняет, почему в приведенном выше примере мы получаем 3 совпадения, несмотря на то, что у нас есть строка длины два. Третье совпадение-пустая Строка нулевой длины .

Чтобы сопоставить текст нулю или неограниченному времени, мы используем квантор*, он просто похож на ?:

@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect() {
     int matches = runTest("\\a*", "hi");
 
     assertEquals(matches, 3);
}

Поддерживаемая альтернатива:

@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\a{0,}", "hi");
 
    assertEquals(matches, 3);
}

Квантор с разницей равен +, он имеет порог соответствия 1. Если требуемая Строка вообще не встречается, совпадения не будет, даже строки нулевой длины |:

@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\a+", "hi");
 
    assertFalse(matches);
}

Поддерживаемая альтернатива:

@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\a{1,}", "hi");
 
    assertFalse(matches);
}

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

@Test
public void givenBraceQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("a{3}", "aaaaaa");
 
    assertEquals(matches, 2);
}

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

@Test
public void givenBraceQuantifier_whenFailsToMatch_thenCorrect() {
    int matches = runTest("a{3}", "aa");
 
    assertFalse(matches > 0);
}

Когда мы используем диапазон в скобке, совпадение будет жадным, совпадающим с более высоким концом диапазона:

@Test
public void givenBraceQuantifierWithRange_whenMatches_thenCorrect() {
    int matches = runTest("a{2,3}", "aaaa");
 
    assertEquals(matches, 1);
}

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

Однако API позволяет нам указать ленивый или неохотный подход, так что сопоставитель может начинаться с нижнего конца диапазона, и в этом случае сопоставление двух вхождений как aa и aa :

@Test
public void givenBraceQuantifierWithRange_whenMatchesLazily_thenCorrect() {
    int matches = runTest("a{2,3}?", "aaaa");
 
    assertEquals(matches, 2);
}

9. Захват Групп

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

Он будет прикреплять номера к группам захвата и разрешать обратные ссылки с использованием этих номеров.

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

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

@Test
public void givenCapturingGroup_whenMatches_thenCorrect() {
    int maches = runTest("(\\d\\d)", "12");
 
    assertEquals(matches, 1);
}

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

@Test
public void givenCapturingGroup_whenMatches_thenCorrect2() {
    int matches = runTest("(\\d\\d)", "1212");
 
    assertEquals(matches, 2);
}

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

@Test
public void givenCapturingGroup_whenMatchesWithBackReference_
  thenCorrect() {
    int matches = runTest("(\\d\\d)\\1", "1212");
 
    assertEquals(matches, 1);
}

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

@Test
public void givenCapturingGroup_whenMatches_thenCorrect3() {
    int matches = runTest("(\\d\\d)(\\d\\d)", "1212");
 
    assertEquals(matches, 1);
}

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

@Test
public void givenCapturingGroup_whenMatchesWithBackReference_
  thenCorrect2() {
    int matches = runTest("(\\d\\d)\\1\\1\\1", "12121212");
 
    assertEquals(matches, 1);
}

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

@Test
public void givenCapturingGroupAndWrongInput_
  whenMatchFailsWithBackReference_thenCorrect() {
    int matches = runTest("(\\d\\d)\\1", "1213");
 
    assertFalse(matches > 0);
}

Важно не забывать об обратных косых чертах, это очень важно в синтаксисе Java.

10. Сопоставители границ

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

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

Этот тест не пройдет, так как текст dog можно найти в начале:

@Test
public void givenText_whenMatchesAtBeginning_thenCorrect() {
    int matches = runTest("^dog", "dogs are friendly");
 
    assertTrue(matches > 0);
}

Следующий тест не пройдет:

@Test
public void givenTextAndWrongInput_whenMatchFailsAtBeginning_
  thenCorrect() {
    int matches = runTest("^dog", "are dogs are friendly?");
 
    assertFalse(matches > 0);
}

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

@Test
public void givenText_whenMatchesAtEnd_thenCorrect() {
    int matches = runTest("dog$", "Man's best friend is a dog");
 
    assertTrue(matches > 0);
}

И здесь не будет найдено ни одного совпадения:

@Test
public void givenTextAndWrongInput_whenMatchFailsAtEnd_thenCorrect() {
    int matches = runTest("dog$", "is a dog man's best friend?");
 
    assertFalse(matches > 0);
}

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

Пространство – это граница слова:

@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "a dog is friendly");
 
    assertTrue(matches > 0);
}

Пустая строка в начале строки также является границей слова:

@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect2() {
    int matches = runTest("\\bdog\\b", "dog is man's best friend");
 
    assertTrue(matches > 0);
}

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

@Test
public void givenWrongText_whenMatchFailsAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "snoop dogg is a rapper");
 
    assertFalse(matches > 0);
}

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

@Test
public void givenText_whenMatchesAtWordAndNonBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\B", "snoop dogg is a rapper");
    assertTrue(matches > 0);
}

11. Методы класса Шаблонов

Ранее мы создавали объекты Pattern только базовым способом. Однако этот класс имеет другой вариант метода compile , который принимает набор флагов вместе с аргументом регулярного выражения, влияющим на способ сопоставления шаблона.

Эти флаги являются просто абстрагированными целочисленными значениями. Давайте перегрузим метод runTest в тестовом классе, чтобы он мог принимать флаг в качестве третьего аргумента:

public static int runTest(String regex, String text, int flags) {
    pattern = Pattern.compile(regex, flags);
    matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()){
        matches++;
    }
    return matches;
}

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

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

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

Рассмотрим символ Юникода с ударением é . Его составной кодовой точкой является u00E9 . Однако в Юникоде также есть отдельная кодовая точка для составляющих его символов e , u0065 и острого акцента u0301 . В этом случае составной символ u 00E9 неотличим от последовательности из двух символов u 0065 у 0301 .

По умолчанию сопоставление не учитывает каноническую эквивалентность:

@Test
public void givenRegexWithoutCanonEq_whenMatchFailsOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301");
 
    assertFalse(matches > 0);
}

Но если мы добавим флаг, то тест пройдет:

@Test
public void givenRegexWithCanonEq_whenMatchesOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301", Pattern.CANON_EQ);
 
    assertTrue(matches > 0);
}

Но если мы добавим флаг, то тест пройдет:

Этот флаг позволяет сопоставлять независимо от регистра. По умолчанию сопоставление учитывает регистр:

@Test
public void givenRegexWithDefaultMatcher_whenMatchFailsOnDifferentCases_thenCorrect() {
    int matches = runTest("dog", "This is a Dog");
 
    assertFalse(matches > 0);
}

Поэтому, используя этот флаг, мы можем изменить поведение по умолчанию:

@Test
public void givenRegexWithCaseInsensitiveMatcher
  _whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest(
      "dog", "This is a Dog", Pattern.CASE_INSENSITIVE);
 
    assertTrue(matches > 0);
}

Мы также можем использовать эквивалентное встроенное выражение флага для достижения того же результата:

@Test
public void givenRegexWithEmbeddedCaseInsensitiveMatcher
  _whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest("(?i)dog", "This is a Dog");
 
    assertTrue(matches > 0);
}

Мы также можем использовать эквивалентное встроенное выражение флага для достижения того же результата:

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

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

@Test
public void givenRegexWithComments_whenMatchFailsWithoutFlag_thenCorrect() {
    int matches = runTest(
      "dog$  #check for word dog at end of text", "This is a dog");
 
    assertFalse(matches > 0);
}

Это связано с тем, что совпадения будут искать все регулярное выражение во входном тексте, включая пробелы и символ#. Но когда мы используем флаг, он будет игнорировать лишние пробелы, и каждый текст, начинающийся с#, будет рассматриваться как комментарий, который будет игнорироваться для каждой строки:

@Test
public void givenRegexWithComments_whenMatchesWithFlag_thenCorrect() {
    int matches = runTest(
      "dog$  #check end of text","This is a dog", Pattern.COMMENTS);
 
    assertTrue(matches > 0);
}

Для этого также существует альтернативное выражение встроенного флага:

@Test
public void givenRegexWithComments_whenMatchesWithEmbeddedFlag_thenCorrect() {
    int matches = runTest(
      "(?x)dog$  #check end of text", "This is a dog");
 
    assertTrue(matches > 0);
}

Для этого также существует альтернативное выражение встроенного флага:

По умолчанию, когда мы используем выражение точки “.” в регулярном выражении, мы сопоставляем каждый символ во входной строке | до тех пор, пока не встретим новый символ строки.

Используя этот флаг, матч также будет включать в себя терминатор линии. Мы лучше поймем это на следующих примерах. Эти примеры будут немного отличаться. Поскольку мы заинтересованы в утверждении против совпадающей строки , мы будем использовать метод matcher ‘s group , который возвращает предыдущее совпадение.

Во-первых, мы увидим поведение по умолчанию:

@Test
public void givenRegexWithLineTerminator_whenMatchFails_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)");
    Matcher matcher = pattern.matcher(
      "this is a text" + System.getProperty("line.separator") 
        + " continued on another line");
    matcher.find();
 
    assertEquals("this is a text", matcher.group(1));
}

Как мы видим, сопоставляется только первая часть ввода перед терминатором строки.

Теперь в режиме dotall будет сопоставлен весь текст, включая терминатор строки:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithDotall_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)", Pattern.DOTALL);
    Matcher matcher = pattern.matcher(
      "this is a text" + System.getProperty("line.separator") 
        + " continued on another line");
    matcher.find();
    assertEquals(
      "this is a text" + System.getProperty("line.separator") 
        + " continued on another line", matcher.group(1));
}

Мы также можем использовать встроенное выражение флага для включения dotall mode:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithEmbeddedDotall
  _thenCorrect() {
    
    Pattern pattern = Pattern.compile("(?s)(.*)");
    Matcher matcher = pattern.matcher(
      "this is a text" + System.getProperty("line.separator") 
        + " continued on another line");
    matcher.find();
 
    assertEquals(
      "this is a text" + System.getProperty("line.separator") 
        + " continued on another line", matcher.group(1));
}

Мы также можем использовать встроенное выражение флага для включения || dotall || mode:

В этом режиме совпадения не придают особого значения каким-либо метасимволам, escape-символам или синтаксису регулярных выражений. Без этого флага сопоставитель будет сопоставлять следующее регулярное выражение с любыми входными данными String :

@Test
public void givenRegex_whenMatchesWithoutLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text");
 
    assertTrue(matches > 0);
}

Это поведение по умолчанию, которое мы наблюдаем во всех примерах. Однако с этим флагом совпадение не будет найдено, так как совпадения будут искать (.*) вместо его интерпретации:

@Test
public void givenRegex_whenMatchFailsWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text", Pattern.LITERAL);
 
    assertFalse(matches > 0);
}

Теперь, если мы добавим необходимую строку, тест пройдет:

@Test
public void givenRegex_whenMatchesWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text(.*)", Pattern.LITERAL);
 
    assertTrue(matches > 0);
}

Нет встроенного символа флага для включения разбора литералов.

Нет встроенного символа флага для включения разбора литералов.

По умолчанию ^ и $ метасимволы полностью совпадают в начале и в конце соответственно всей входной строки . Сопоставитель игнорирует любые терминаторы строк:

@Test
public void givenRegex_whenMatchFailsWithoutMultilineFlag_thenCorrect() {
    int matches = runTest(
      "dog$", "This is a dog" + System.getProperty("line.separator") 
      + "this is a fox");
 
    assertFalse(matches > 0);
}

Совпадение не удается, потому что сопоставитель ищет dog в конце всей строки , но dog присутствует в конце первой строки строки.

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

@Test
public void givenRegex_whenMatchesWithMultilineFlag_thenCorrect() {
    int matches = runTest(
      "dog$", "This is a dog" + System.getProperty("line.separator") 
      + "this is a fox", Pattern.MULTILINE);
 
    assertTrue(matches > 0);
}

Вот версия встроенного флага:

@Test
public void givenRegex_whenMatchesWithEmbeddedMultilineFlag_
  thenCorrect() {
    int matches = runTest(
      "(?m)dog$", "This is a dog" + System.getProperty("line.separator") 
      + "this is a fox");
 
    assertTrue(matches > 0);
}

12. Соответствует методам класса

В этом разделе мы рассмотрим некоторые полезные методы класса Matcher . Мы сгруппируем их в соответствии с функциональными возможностями для ясности.

12.1. Методы индексации

Методы индекса предоставляют полезные значения индекса, которые точно показывают, где было найдено совпадение во входной строке |. В следующем тесте мы подтвердим начальный и конечный индексы совпадения для dog во входной строке :

@Test
public void givenMatch_whenGetsIndices_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("This dog is mine");
    matcher.find();
 
    assertEquals(5, matcher.start());
    assertEquals(8, matcher.end());
}

12.2. Методы исследования

Методы исследования проходят через вход String и возвращают логическое значение, указывающее, найден ли шаблон или нет. Обычно используются методы matches и lookingAt .

Методы matches и lookingAt пытаются сопоставить входную последовательность с шаблоном. Разница в том, что соответствует требует, чтобы вся входная последовательность была согласована, в то время как смотрит на нет.

Оба метода начинаются с начала ввода String :

@Test
public void whenStudyMethodsWork_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dogs are friendly");
 
    assertTrue(matcher.lookingAt());
    assertFalse(matcher.matches());
}

Метод matches вернет true в таком случае:

@Test
public void whenMatchesStudyMethodWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dog");
 
    assertTrue(matcher.matches());
}

12.3. Методы замены

Методы замены полезны для замены текста во входной строке. Наиболее распространенными являются replaceFirst и replaceAll .

Методы replaceFirst и replaceAll заменяют текст, соответствующий заданному регулярному выражению. Как показывают их имена, replaceFirst заменяет первое вхождение, а replaceAll заменяет все вхождения:

@Test
public void whenReplaceFirstWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceFirst("cat");
 
    assertEquals(
      "cats are domestic animals, dogs are friendly", newStr);
}

Замените все вхождения:

@Test
public void whenReplaceAllWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceAll("cat");
 
    assertEquals("cats are domestic animals, cats are friendly", newStr);
}

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

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

В этой статье мы узнали, как использовать регулярные выражения в Java, а также изучили наиболее важные функции пакета java.util.regex .

Полный исходный код проекта, включая все примеры кода, используемые здесь, можно найти в проекте GitHub .