Регулярные выражения в Java

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

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

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

Фактически, регулярное выражение – это шаблон для поиска строк в тексте. В Java этот шаблон всегда представляется строкой, то есть объектом класса String.

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

Регулярные выражения строятся с помощью букв, цифр, а также метасимволов (метахарактеров) – специальных символов, которые имеют особый смысл в регулярных выражениях. Например:

String regex = "java"; 
String regex = "\\d{3}"; 
Run Code

Разберемся с этими примерами подробнее:

  • “java” – обычная строка, которая ищет в тексте точное совпадение с “java”;
  • “\\d{3}” – это выражение означает «ровно три цифры подряд»:
  • \d – специальный символ, который означает любую цифру (0-9);
  • {3} – квантификатор, который говорит: «ровно три раза».

Таким образом, \\d{3} в Java будет соответствовать строкам “123”, “456”, “999”, но не “12” или “abcd”.

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

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

  1. Написать его в виде строки, которая соответствует синтаксису регулярных выражений.
  2. Скомпилировать эту строку в регулярное выражение.

В любой Java-программе мы начинаем работу с выражениями RegEx с создания объекта Pattern. Для этого нам нужно вызвать один из двух статических методов класса: compile:

Первый метод принимает один аргумент — строку, содержащую регулярное выражение.

Второй – дополнительный аргумент – 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); 
Run Code

В этом случае поиск слова “java” будет регистронезависимым – он найдёт и “java”, и “Java”, и “JAVA”.

По сути, класс Pattern – это конструктор регулярных выражений. Он не просто хранит строку-шаблон, а превращает ее во внутреннюю структуру, с которой Java может работать быстро и эффективно.

Когда вы вызываете Pattern.compile(), происходит следующее:

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

Такой подход позволяет создавать неизменяемые объекты (immutable). Это значит, что после создания объект Pattern нельзя изменить – он всегда остается в том виде, в каком был скомпилирован.

Это важно по двум причинам:

  1. Безопасность – особенно в многопоточном коде.
  2. Производительность – вы можете использовать один и тот же объект Pattern многократно, без перерасхода ресурсов.

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

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

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

МетасимволОписание
^начало строки
$конец строки
\bграница слов
\Bграница без слов
\Aначало входных данных
\Gконец предыдущего матча
\Zконец входных данных
\zконец входных данных (жесткое совпадение)

Например:

Pattern pattern = Pattern.compile("^Hello");
Matcher matcher = pattern.matcher("Hello, world!");
System.out.println(matcher.find());

Здесь используется символ ^, который указывает на начало строки. Таким образом, регулярное выражение ^Hello найдёт слово “Hello” только если оно стоит в самом начале текста. В строке "Hello, world!" оно действительно стоит первым, поэтому matcher.find() его находит.

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

МетасимволОписание
\dцифра (0-9)
\Dнецифровой
\sсимвол пробела (пробел, табуляция, перенос строки и т. д.)
\Sсимвол, не являющийся пробелом
\wбуква, цифра или подчёркивание (a-z, A-Z, 0-9, _)
\Wлюбой символ, кроме \w
.любой символ

Например:

Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("I have 25 apples.");
System.out.println(matcher.find());

Паттерн \\d+ соответствует одной или более цифрам подряд. В строке "I have 25 apples." такое совпадение есть – это число 25. Метод matcher.find() находит его.

3. Метасимволы для сопоставления управляющих символов

МетасимволОписание
\tсимвол табуляции
\nсимвол новой строки
\rвозврат каретки
\fсимвол перевода строки
\u0085символ следующей строки
\u2028разделитель строк
\u2029разделитель абзацев

Например:

Pattern pattern = Pattern.compile("Hello\\nWorld");
Matcher matcher = pattern.matcher("Hello\nWorld");
System.out.println(matcher.find());

В строке "Hello\nWorld" между словами находится символ перевода строки (\n). Паттерн Hello\\nWorld ищет точное совпадение с этой конструкцией – слово "Hello", затем \n, и за ним "World". Совпадение обнаружено.

4. Метасимволы для классов персонажей

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

Например:

Pattern pattern = Pattern.compile("[a-c]");
Matcher matcher = pattern.matcher("hello abc");
while (matcher.find()) {
    System.out.println(matcher.group());
}

В строке “hello abc” паттерн [a-c] ищет символы ‘a’, ‘b’ или ‘c’. Метод matcher.find() находит каждое вхождение этих символов по одному и выводит их отдельно. Найденные совпадения — ‘a’, ‘b’, ‘c’.

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

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

Например:

import java.util.regex.*;
public class QuantifierExample {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("a{2,3}"); // Найдёт 'aa' или 'aaa'
        Matcher matcher = pattern.matcher("a aa aaa aaaa");
        while (matcher.find()) {
            System.out.println(matcher.group()); // Выведет: aa, aaa, aaa
        }
    }
}

В строке “a aa aaa aaaa” паттерн a{2,3} ищет последовательности символов ‘a’, состоящие из 2 или 3 букв. Метод matcher.find() находит совпадения ‘aa’, ‘aaa’, и снова ‘aaa’. Совпадение найдено.

Жадные квантификаторы (Greedy Quantifiers)

Вы должны знать, что квантификаторы в регулярных выражениях бывают трех видов: жадные (greedy), поглощающие (possessive) и ленивые (reluctant).

Форма квантификатора зависит от символа, который идёт после него:

  • Квантификатор по умолчанию (без суффикса) – жадный.
  • Добавление символа + после квантификатора делает его поглощающим.
  • Добавление символа ? после квантификатора делает его ленивым.

Например:

"A.+a" // жадный
"A.++a" // поглащающий
"A.+?a" // ленивый
Run Code

Разберем поведение каждого типа на конкретном примере.

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

Например:

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 сравнивает ее с каждым символом текста, начиная с нулевого индекса. В нашем тексте символ F находится под нулевым индексом, поэтому Matcher перебирает все символы, пока не найдет совпадение с шаблоном. В нашем примере этот символ находится под индексом 5.

2. Когда совпадение с первым символом шаблона найдено, Matcher ищет совпадение со вторым символом. В нашем случае это символ “.“, который обозначает любой символ.

Символ n находится в шестой позиции. Он, безусловно, подходит для “любого символа”.

3. Матчер переходит к проверке следующего символа шаблона. В нашем шаблоне он включен в квантификатор, который применяется к предыдущему символу: “.+“. Поскольку количество повторений “любого символа” в нашем шаблоне составляет один или более раз, Matcher многократно берет следующий символ из строки и проверяет его на соответствие шаблону до тех пор, пока он совпадает с “любым символом”. В нашем примере – до конца строки (с индекса 7 по индекс 18).

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

4. После того как Matcher достиг конца текста и завершил проверку части шаблона"A.+“, он начинает проверять остальную часть шаблона: a. Дальше текста нет, поэтому проверка продолжается путем “отката”, начиная с последнего символа:

5. Матчер “запоминает” количество повторений в части “.+” шаблона. В этот момент он уменьшает количество повторений на единицу и сверяет большой шаблон с текстом, пока не будет найдено совпадение:

Поглощающие квантификаторы (Possessive)

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

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

1. Для этих квантификаторов, как и для жадных, код ищет совпадение по первому символу шаблона:

2. Затем он ищет совпадение со следующим символом шаблона (любым символом):

3. В отличие от жадного шаблонирования, при ленивом шаблонировании ищется кратчайшее совпадение. Это означает, что после нахождения совпадения со вторым символом шаблона (точкой, которая соответствует символу в позиции 6 в тексте), Matcher проверяет, соответствует ли текст остальной части шаблона – символу"a

4. Текст не соответствует шаблону (т.е. содержит символ"n” в позиции 7), поэтому Matcher добавляет еще один “любой символ”, поскольку квантификатор указывает на один или несколько. Затем он снова сравнивает шаблон с текстом в позициях с 5 по 8:

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

Соответственно, при использовании шаблона"A.+?aосновной метод дает следующий результат:

Anna
Alexa

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

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

Экранирование символов в регулярных выражениях

Регулярные выражения в Java записываются как обычные строки. А у строк в Java есть свои правила. Одно из главных — обратный слэш \ – это специальный символ. Он говорит: “Следующий за мной символ — особенный.”

Например:

String s = "The root directory is \nWindows"; 
String s = "The root directory is \u00A7Windows"; 
Run Code

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

Чтобы Java поняла, что вы хотите именно \s в регулярном выражении, нужно написать двойной слэш.

Например:

String regex = "\\s"; 
String regex = "\"Windows\"";  
Run Code

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

Чтобы Java поняла, что вы хотите именно \s в регулярном выражении, нужно написать двойной слэш:

String regex = "\\s";  
Run Code

То же самое с другими спецсимволами. Если вы хотите, например, искать вопросительный знак (а не использовать его как квантификатор), – его нужно экранировать.

String regex = "How\\?"; // шаблон для поиска "How?"

И еще пример с кавычками:

String regex = "\"Windows\""; // шаблон для поиска слова "Windows" в кавычках

Главное правило:

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

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

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

  • String pattern() – возвращает исходное строковое представление регулярного выражения, использованное для создания объекта Pattern:
Pattern pattern = Pattern.compile("abc");
System.out.println(pattern.pattern()); 
Run Code
  • static boolean matches(String regex, CharSequence input) – позволяет проверить регулярное выражение, переданное в качестве regex, на соответствие тексту, переданному в качестве input. Возвращает:

true – если текст соответствует шаблону;
false – если не соответствует

Например:

System.out.println(Pattern.matches("A.+a","Anna")); 
System.out.println(Pattern.matches("A.+a","Fred Anna Alexander"));
Run Code
  • int flags() – возвращает значение параметра flags, установленного при создании шаблона. Если флаги не задавались – вернет 0.

Например:

Pattern pattern = Pattern.compile("abc");
System.out.println(pattern.flags()); 
Pattern pattern = Pattern.compile("abc",Pattern.CASE_INSENSITIVE);
System.out.println(pattern.flags()); 
Run Code
  • String[] split(CharSequence text, int limit) – разбивает переданный текст на массив String. Параметр 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
Run Code

Ниже мы рассмотрим еще один метод класса, используемый для создания объекта Matcher.

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

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

public Matcher matcher(CharSequence input)

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

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

Пример создания матчика:

Pattern p = Pattern.compile("a*b"); 
Matcher m = p.matcher("aaaaab"); 
Run Code

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

  • boolean find() – ищет следующее совпадение в тексте. Обычно используется в цикле для анализа всего текста;
  • int start() и int end() – возвращают начальную и конечную позицию найденного совпадения;
  • String replaceFirst(String replacement) – заменяет первое совпадение;
  • String replaceAll(String 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 создают новый объект строки – то есть новую строку, в которой совпадения с шаблоном заменяются на переданный текст.

Метод replaceFirst заменяет только первое совпадение, а replaceAll – все совпадения. Важно, что оригинальный текст остается без изменений.

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

Простые шаблоны Regex в Java – практические примеры

Теперь, когда вы уже чувствуете себя увереннее с регулярными выражениями в Java, давайте посмотрим на несколько простых шаблонов, которые можно использовать прямо сейчас. Понимать теорию – это хорошо, но видеть её в действии – еще лучше! Готовы?

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 вместо [0-9], так что шаблон будет “^\\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-программе регулярное выражение определяется строкой, которая подчиняется определенным правилам сопоставления с образцом. При выполнении кода Java-машина компилирует эту строку в объект Pattern и использует объект Matcher для поиска совпадений в тексте. Как уже говорилось выше в самом начале, люди часто откладывают регулярные выражения на потом, считая их сложной темой. Но если вы поймете основной синтаксис, метасимволы и экранирование символов, а также изучите примеры регулярных выражений, то обнаружите, что они гораздо проще, чем кажутся на первый взгляд.

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

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

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

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