Регулярные выражения. Базовая теория

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

Регулярные выражения – это тема, которую программисты, даже опытные, часто откладывают на потом. Тем не менее, рано или поздно большинство Java-разработчиков сталкивается с необходимостью обработать текстовую информацию. Без регулярных выражений эффективный и компактный код для таких целей просто немыслим, так что приступим!)

Что такое регулярное выражение?

По сути, регулярное выражение – это шаблон для поиска строки в тексте. В Java исходным представлением этого шаблона всегда является строка, то есть объект класса String. У регулярных выражений свой синтаксис, который мы тщательно рассмотрим. В них используются буквы и цифры, а также метасимволы (символы, которые имеют особое значение). Например:

String regex = "java"; // Это строка для вхождений "java";
String regex = "\\d{3}"; // Здесь зашифрованы цифры;
Run Code

Создание регулярных выражений

Нужно выполнить всего 2 шага:

  1. запишите регулярное выражение как строку, соответствующую синтаксису регулярных выражений;
  2. скомпилируйте строку в регулярное выражение;

Начнем с создания объекта класса Pattern. Для этого нужно вызвать статический метод compile. Он может принимать как один аргумент (String literal, строковый литерал, содержащий регулярное выражение (строковый литерал – последовательность символов, заключённая в кавычки), так и – вместе с ним – аргумент (int flags), настраивающий некоторые дополнительные опции сопоставления (например, без учета регистра). Ниже даны два метода:

public static Pattern compile (String literal)
public static Pattern compile (String literal, int flags)
Run Code

Список потенциальных значений параметра flags определен в классе Pattern и доступен нам в виде статических переменных класса. Например:

Pattern pattern = Pattern.compile("java", Pattern.CASE_INSENSITIVE); // Сопоставление с шаблоном будет происходить без учета регистра.

По сути, класс Pattern – это конструктор для регулярных выражений. Метод compile вызывает частный конструктор класса Pattern для создания скомпилированного представления. Таким образом создаются неизменяемые объекты. При конструировании регулярного выражения происходит синтаксическая проверка. Если строка содержит ошибки, генерируется исключение PatternSyntaxException.

Синтаксис регулярных выражений

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

1. Метасимволы для соответствия границам строк или текста

МетасимволОписание
^начало строки
$конец строки
\bграница слова
\Bграница не-слова
\Aначало ввода
\Gконец предыдущего совпадения
\Zконец ввода
\zконец ввода

комментарий к \G: конец предыдущего совпадения – ситуация, когда, например, у нас есть символы 12-34-56 и регулярное выражение \G\d{2}[-]*. Повторяющиеся паттерны – пары чисел 12, 34, 56. Значит, дефис между 2 и 3 является концом предыдущего совпадения. После – поиск соответствия с регулярным выражением продолжается. Без \G был бы найден только первый паттерн

2. Метасимволы для поиска символьных классов

МетасимволОписание
\dцифра
\Dне-цифра
\sпробельный символ
\Sне-пробел
\wбуква, цифра или нижнее подчеркивание
\Wлюбой символ, кроме букв, цифр и нижнего подчеркивания
.любой символ

3. Метасимволы для нахождения символов, отвечающих за редактуру текста

МетасимволОписание
\tсимвол табуляции
\nсимвол новой строки
\rсимвол, который переводит каретку в начало текущей строки
(*каретка – это мигающая черточка, показывающая, куда будет вставлен текст)
\fсимвол новой страницы
\u0085символ следующей строки
\u2028разделитель строк
\u2029разделитель абзацев

4. Метасимволы для групп символов

МетасимволОписание
[abc]любой из перечисленных символов (a, b или c)
[^abc]любой символ, кроме перечисленных (не a, b или c)
[a-zA-Z]объединенные диапазоны всех алтинских букв как строчных, так и прописных
[a-d[m-p]]объединение символов (от a до d и от m до p)
[a-z&&[def]]пересечение символов (d, e, f)
[a-z&&[^bc]]вычитание символов (a, dz)

5. Метасимволы для указания на количество символов (квантификаторов). Квантору всегда предшествует символ или группа символов.

МетасимволОписание
?один/ни одного
*ноль/более раз
+один/несколько раз
{n}n раз
{n,}n или более раз
{n,m}не менее n раз и не более m раз

Типы квантификаторов

Есть три вида квантификаторов: жадные, сверхжадные и ленивые. Сверхжадный квантификатор задается при добавлении символа «+» после квантора (+ после точки в примере ниже). Для ленивого квантификатора нужен символ «?». Например:

"A.+a" // жадный квантификатор
"A.++a" // сверхжадный квантификатор
"A.+?a" // ленивый квантификатор
Run Code

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

По умолчанию квантификаторы являются жадными. Это означает, что они ищут самое длинное совпадение в строке. Если мы выполним следующий код:

public static void main(String[] args) {
    String text = "Fred Anna Alexander";
    Pattern pattern = Pattern.compile("A.+a");
    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
        System.out.println(text.substring(matcher.start(), matcher.end()));
    }
}
Run Code

то получим:

Anna Alexa

Жадный квантификатор

Для регулярного выражения"A.+a” поиск совпадений по шаблону выполняется следующим образом:

  1. Регулярное выражение начинается с латинской буквы A, которую Matcher сравнивает с каждым символом текста, начиная с нулевого индекса, пока не найдет совпадение с шаблоном. В нашей строке искомый символ находится под индексом 5.
  1. Далее Matcher ищет совпадения в строке со вторым символом в нашем регулярном выражении (таковым является точка “.“, которая обозначает любой символ)

Так, буква n под индексом 6 безусловно подходит под определение “любого символа”.

  1. Следующий символ в нашем регулярном выражении – плюс (+). Он означает, что “любой символ” может повторяться один и более раз. Далее происходит многократная проверка на соответсвие символов строки “любому символу”, что, очевидно, приводит нас к концу строки (вплоть до 18 индекса).

По сути, Matcher проглатывает строку до конца – именно это и подразумевается под словом “жадный”.

  1. В регулярном выражении остался незатронутым символ a. Так как мы дошли до конца строки, проверка на совпадение символа а с символами в строке продолжается путем “отката”. Иными словами, мы начинаем с последнего символа:
  1. Matcher “запоминает” количество повторений, которое было сделано ранее при сверке с символами “.+“. Он начинает уменьшать количество повторений каждый раз на единицу и сверяет все регулярное выражение с текстом, пока не будет найдено совпадение:

Сверхжадный квантификатор

Принцип почти такой же, как и у жадного квантификатора. Разница заключается в том, что, если строка захвачена до конца, то при “отступлении” сопоставления не происходит. Иными словами, первые три этапа совпадают, а потом начинается поиск по полному регулярному выражению, то есть по "A.++a“. Из-за дополнительного плюса совпадений не обнаруживается.

Ленивый квантификатор

  1. Код, как и в случае с жадным и сверхжадных классификаторами, ищет совпадение по первому символу регулярного выражения:
  1. Затем Matcher ищет совпадение со следующим символом шаблона (им, как мы помним, является точка, указывающая на любой символ):
  1. Теперь происходит поиск кратчайшего совпадения. Это значит, что после того, как будет найдено совпадение со вторым символом регулярного выражения (индекс 6), Matcher проверяет, соответствует ли текст строки остальной части шаблона (символу "a“)
  1. Соответствий не нашлось, поэтому добавляется еще один “любой символ”, и после этого происходит повторное сравнение:

В результате при использовании регулярного выражения "A.+?a” программа выдаст следующий результат:

Anna Alexa 

Разные типы квантификаторов выдают разные результаты, на что стоит обращать внимание.

Экранирование

Поскольку регулярное выражение в Java является строковым литералом, нам необходимо учитывать правила Java, касающиеся строковых литералов. В частности, обратная косая черта “\” в строковых литералах интерпретируется как символ, который сообщает компилятору, что следующий за чертой символ является специальным и что он должен быть интерпретирован особым образом. Например:

String s = "The root directory is \nWindows"; // Перемещает слово "Windows" на новую строку
String s = "The root directory is \u00A7Windows"; // Вставка символа абзаца до слова "Windows"
Run Code

Это значит, что в регулярных выражениях, использующих символы “\” (которые указывают на метасимволы), необходимо повторять обратные косые черты, чтобы компилятор не интерпретировал строку неправильно. Например:

String regex = "\\s"; // Регулярное выражение для пробела
String regex = "\"Windows\"";  // Регулярное выражение для слова "Windows"
Run Code

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

String regex = "How\\?";  // Регулярное выражение для "How?"

Методы класса Pattern

В классе Pattern есть и другие методы для работы с регулярными выражениями:

  • String pattern() – метод возвращает исходное строковое представление регулярного выражения, которое было использовано для создания объекта Pattern:
Pattern pattern = Pattern.compile("abc");
System.out.println(pattern.pattern()); // "abc"
Run Code
  • static boolean matches(String regex, CharSequence input) – этот метод позволяет проверить регулярное выражение, переданное в параметре regex, на соответствие тексту, переданному в параметре input. Метод возвращает true, если соответствие было выявлено, и false в обратном случае, например:
System.out.println(Pattern.matches("A.+a","Anna")); // true
System.out.println(Pattern.matches("A.+a","Fred Anna Alexander")); // false
Run Code
  • int flags() – метод возвращает числовое значение заданного параметра flags или 0, если параметр задан не был. Например:
Pattern pattern = Pattern.compile("abc");
System.out.println(pattern.flags()); // 0
Pattern pattern = Pattern.compile("abc",Pattern.CASE_INSENSITIVE);
System.out.println(pattern.flags()); // 2
Run Code
  • String[] split(CharSequence text, int limit) – этот метод разбивает переданный текст (параметр text) на массив элементов в String. Параметр limit указывает на максимальное количество совпадений, которые ищутся в тексте:
    • если limit > 0, то выполняется поиск limit-1 совпадений
    • если limit < 0, то выполняется поиск всех совпадений в тексте
    • если limit = 0 , то выполняется поиск всех совпадений в тексте, причем пустые строки в конце массива отбрасываются. Например:
public static void main(String[] args) {
    String text = "Fred Anna Alexa";
    Pattern pattern = Pattern.compile("\\s");
    String[] strings = pattern.split(text,2);
    for (String s : strings) {
        System.out.println(s);
    }
    System.out.println("---------");
    String[] strings1 = pattern.split(text);
    for (String s : strings1) {
        System.out.println(s);
    }
}
Run Code

Вывод в консоль:

Fred
Anna Alexa
---------
Fred
Anna
Alexa

Методы класса Matcher

Экземпляры класса Matcher создаются для поиска в тексте соответствий заданному регулярному выражению. Чтобы создать такой объект, класс Pattern предоставляет следующий метод:

public Matcher matcher(CharSequence input)

Метод принимает последовательность символов, по которой будет производиться поиск. В данном случае мы имеем дело с экземпляром класса, реализующим интерфейс CharSequence. В качестве аргумента можно передавать не только String, но и StringBuffer, StringBuilder, Segment или CharBuffer.

Шаблоном является объект класса Pattern, на котором вызывается метод matcher.

Pattern p = Pattern.compile("a*b"); // Создаем скомпилированное регулярное выражение
Matcher m = p.matcher("aaaaab"); // Создаем "поисковую систему" для поиска в тексте "aaaaab" структуры типа "a*b"
Run Code

Такую “поисковую систему” можно использовать для выявления совпадений, получения их позиций в тексте, а также для замены текста с помощью методов класса.

Метод find() выполняет поиск следующего совпадения с шаблоном в тексте. Используя его в цикле, мы можем последовательно обработать весь текст. Для работы с выявленными совпадениями мы можем использовать методы int start() и int end(), чтобы определить их местоположение в тексте.

А с помощью методов String replaceFirst(String replacement) и String replaceAll(String replacement) возможно заменить совпадения на значение параметра replacement:

public static void main(String[] args) {
    String text = "Fred Anna Alexa";
    Pattern pattern = Pattern.compile("A.+?a");

    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
        int start=matcher.start();
        int end=matcher.end();
        System.out.println("Match found: " + text.substring(start, end) + " from index "+ start + " through " + (end-1));
    }
    System.out.println(matcher.replaceFirst("Ira"));
    System.out.println(matcher.replaceAll("Mary"));
    System.out.println(text);
}
Run Code

Вывод:

Match found: Anna from index 5 through 8
Match found: Alexa from index 10 through 14
Fred Ira Alexa
Fred Mary Mary
Fred Anna Alexa

Пример показывает, что методы replaceFirst и replaceAll создают новый объект String. Текст, переданный методу в качестве аргумента, заменяет те части строки, которые совпадают с заданным шаблоном. Стоит отметить, что метод replaceFirst заменяет только первое совпадение, а метод replaceAll заменяет все совпадения в тексте. При этом исходный текст остается неизменным!

Наиболее часто используемые в классах Pattern и Matcher операции с регулярными выражениями встроены прямо в класс String. Речь о таких методах, как split, matches, replaceFirst и replaceAll. “Под капотом” же эти методы используют классы Pattern и Matcher. Для сложных задач лучше работать с этими классами напрямую. Если же нужно заменить текст или сравнить строки без написания дополнительного кода, лучше использовать методы класса String.

Практические примеры

1. Только буквы

Представьте, что вы хотите проверить, содержит ли строка только алфавитные символы. Проверка имени пользователя или названия страны – очень распространенный сценарий. Вот как это можно сделать на Java:

import java.util.regex.Pattern;

public class RegexLettersOnly {
    public static void main(String[] args) {
        String pattern = "^[A-Za-z]+$"; // Только буквы, как минимум от одной
        String testString = "HelloWorld";

        boolean matches = Pattern.matches(pattern, testString);
        System.out.println("Is letters only? " + matches);
    }
}
Run Code

Регулярное выражение ^[A-Za-z]+$ проверяет, что строка (от начала ^ до конца $) состоит из букв ([A-Za-z]) и содержит хотя бы один символ (+). Если в строке оказывается что-то еще, например, цифры или знаки препинания, такие случаи проверку не проходят.

2. Только цифры

Что делать при проверке почтового индекса или номера телефона? Давайте посмотрим:

import java.util.regex.Pattern;

public class RegexDigitsOnly {
    public static void main(String[] args) {
        String pattern = "^[0-9]+$";
        String testString = "12345";

        boolean matches = Pattern.matches(pattern, testString);
        System.out.println("Is digits only? " + matches);
    }
}
Run Code

Регулярное выражение ^[0-9]+$ гарантирует, что строка будет от начала до конца состоять строго из цифр. Можно также использовать \\d, тогда регулярное выражение получится таким: "^\\d+$", что более читабельно.

3. Электронная почта

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

import java.util.regex.Pattern;

public class RegexEmailCheck {
    public static void main(String[] args) {
        // Простой шаблон для демонстрации, покрывающий далеко не все случаи
        String pattern = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
        String testEmail = "hello@example.com";

        boolean matches = Pattern.matches(pattern, testEmail);
        System.out.println("Is valid email format? " + matches);
    }
}
Run Code

Этот шаблон проверяет наличие текста перед символом @, затем доменного имени и, наконец, домена верхнего уровня (например, .com или .org).

Заключение

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

Перевод статьи «Regular expressions in Java».

Для усвоения и закрепления теории вам может пригодиться статья «Регулярные выражения в Java»

Оставьте комментарий

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

Прокрутить вверх