Регулярные выражения в Java: подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь в огромной библиотеке, где хранятся миллионы книг. К тебе приходит читатель и говорит: «Мне нужны все книги, где на обложке есть слово "Приключения", а на 25-й странице — упоминание "золото", и чтобы автор был не старше 1980 года рождения». Если бы ты просматривал каждую книгу вручную, это заняло бы годы. Но если бы у тебя был волшебный сканер, которому можно просто сказать: «Найди всё, что подходит под такой-то шаблон», — работа была бы сделана за секунды.
Регулярные выражения (Regular Expressions, или сокращённо regex) — это и есть такой «волшебный сканер» для текста. Это специальный язык шаблонов, который позволяет:
- Находить в тексте фрагменты по сложным условиям
- Проверять, соответствует ли строка определённому формату (например, email или номер телефона)
- Заменять одни части текста на другие
- Разбивать текст на части по заданным разделителям
💡 Простая аналогия: Регулярное выражение — это как маска для поиска файлов в проводнике. Если ты напишешь *.txt, компьютер найдёт все текстовые файлы. Регулярные выражения — это та же идея, но в сотни раз мощнее и гибче.
В Java регулярные выражения появились начиная с версии 1.4 (2002 год) и с тех пор стали незаменимым инструментом любого разработчика. Они используются везде: от проверки паролей в приложениях до парсинга сложных логов серверов.
Теория: как это работает изнутри
Чтобы понять, как работают регулярные выражения, нужно разобраться с двумя ключевыми понятиями: шаблон (pattern) и конечный автомат.
Шаблон — это программа для поиска
Когда ты пишешь регулярное выражение, ты фактически создаёшь мини-программу. Эта программа говорит: «Пройди по тексту символ за символом и проверь, совпадает ли текущая позиция с моими правилами». Java берёт твой шаблон и компилирует его в специальную внутреннюю структуру данных — конечный автомат.
Конечный автомат — сердце regex
Представь себе робота, который стоит на рельсах (строка текста). У робота есть несколько состояний:
- Состояние 0: «Я ещё ничего не нашёл, ищу начало совпадения»
- Состояние 1: «Я нашёл первую букву "к", проверяю дальше»
- Состояние 2: «Я нашёл "ко", продолжаю»
- Состояние 3 (финальное): «Я нашёл "кот"! Записываю результат и ищу следующее»
Робот двигается по тексту, переключая состояния в зависимости от того, какие символы встречает. Если он доходит до финального состояния — совпадение найдено. Если встречает символ, который не подходит ни под одно правило, — он возвращается назад и пробует другой путь. Этот процесс называется backtracking (возврат).
⚠️ Важно: Механизм возврата — самая ресурсоёмкая часть регулярных выражений. Плохо написанный шаблон может заставить компьютер перебирать миллионы комбинаций, что приведёт к зависанию программы. Это называется «катастрофический возврат» (catastrophic backtracking).
Движок регулярных выражений в Java
Java использует NFA (Nondeterministic Finite Automaton) — недетерминированный конечный автомат. Это значит, что в одной и той же позиции может быть несколько вариантов продолжения, и движок пробует их по очереди. Альтернатива — DFA (детерминированный автомат), который работает быстрее, но менее гибкий. Java выбрала NFA, потому что он поддерживает больше возможностей: обратные ссылки, lookahead и lookbehind.
Основные компоненты любого regex
| Компонент | Описание | Пример |
|---|---|---|
| Литералы | Обычные символы, которые ищут сами себя | cat найдёт строку "cat" |
| Метасимволы | Символы со специальным значением | . — любой символ, d — цифра |
| Квантификаторы | Указывают, сколько раз повторяется символ | * — 0 или более, + — 1 или более |
| Группы | Объединяют часть шаблона в одно целое | (ab)+ — "ab", "abab", "ababab" |
| Якоря | Привязывают поиск к началу/концу строки | ^ — начало, $ — конец |
Синтаксис в Java: все формы и варианты
В Java работа с регулярными выражениями сосредоточена в двух классах: java.util.regex.Pattern и java.util.regex.Matcher. Есть также удобные методы в классе String.
Класс Pattern — фабрика шаблонов
Этот класс нельзя создать через new. Вместо этого используется статический метод compile():
// Компилируем шаблон
Pattern pattern = Pattern.compile("\d+"); // Шаблон: одна или более цифр
// Можно добавить флаги (опции)
Pattern patternWithFlags = Pattern.compile("[a-z]+", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Класс Matcher — исполнитель
Matcher создаётся из Pattern и применяется к конкретной строке:
Pattern pattern = Pattern.compile("\d+");
Matcher matcher = pattern.matcher("Год 2024, месяц 12");
// Основные методы Matcher:
// find() — ищет следующее совпадение (возвращает boolean)
// matches() — проверяет, совпадает ли ВСЯ строка с шаблоном
// group() — возвращает найденное совпадение
// start() / end() — позиции начала и конца совпадения
while (matcher.find()) {
System.out.println("Найдено: " + matcher.group());
System.out.println("Позиция: " + matcher.start() + "-" + matcher.end());
}
// Вывод:
// Найдено: 2024
// Позиция: 4-8
// Найдено: 12
// Позиция: 15-17
Методы класса String
Для простых задач не нужно создавать Pattern и Matcher — у String есть встроенные методы:
| Метод | Описание | Пример |
|---|---|---|
matches(regex) |
Проверяет, совпадает ли вся строка с regex | "123".matches("\d+") → true |
replaceAll(regex, replacement) |
Заменяет все совпадения на replacement | "a1b2c3".replaceAll("\d", "*") → "a*b*c*" |
replaceFirst(regex, replacement) |
Заменяет только первое совпадение | "a1b2c3".replaceFirst("\d", "*") → "a*b2c3" |
split(regex) |
Разбивает строку по разделителю | "a,b,c".split(",") → ["a", "b", "c"] |
Справочник метасимволов
| Символ | Значение | Пример |
|---|---|---|
. |
Любой символ (кроме перевода строки) | c.t найдёт "cat", "cot", "c3t" |
d |
Цифра [0-9] | d{3} — три цифры подряд |
D |
Не цифра | D+ — один или более не-цифр |
w |
Буква, цифра или подчёркивание [a-zA-Z0-9_] | w+ — слово |
W |
Всё, кроме букв, цифр и подчёркивания | W — знак пунктуации |
s |
Пробельный символ (пробел, табуляция, новая строка) | s+ — один или более пробелов |
S |
Непробельный символ | S{3} — три непробельных символа |
^ |
Начало строки | ^Привет — строка начинается с "Привет" |
$ |
Конец строки | конец$ — строка заканчивается на "конец" |
|
Граница слова | кот — слово "кот", но не "котёнок" |
Квантификаторы — указатели количества
| Квантификатор | Значение | Пример |
|---|---|---|
* |
0 или более раз | ab* — "a", "ab", "abb", "abbb"... |
+ |
1 или более раз | ab+ — "ab", "abb", но не "a" |
? |
0 или 1 раз (необязательно) | colou?r — "color" и "colour" |
{n} |
Ровно n раз | d{3} — ровно три цифры |
{n,} |
n или более раз | d{2,} — две или более цифр |
{n,m} |
От n до m раз | d{2,4} — от двух до четырёх цифр |
Группы и обратные ссылки
Круглые скобки () создают группы. Группы нумеруются с 1. Можно ссылаться на группу внутри самого regex:
// Поиск повторяющихся слов
Pattern pattern = Pattern.compile("\b(\w+)\s+\1\b");
Matcher matcher = pattern.matcher("кот кот собака");
// \1 — ссылка на первую группу, ищет то же самое слово
while (matcher.find()) {
System.out.println("Повтор: " + matcher.group());
}
// Вывод: Повтор: кот кот
Lookahead и Lookbehind — проверка без захвата
Эти конструкции позволяют проверить, что перед или после совпадения есть определённый текст, но не включать его в результат:
// Положительный lookahead: X(?=Y) — X, за которым следует Y
Pattern pattern = Pattern.compile("\d+(?= рублей)");
Matcher matcher = pattern.matcher("100 рублей, 200 долларов");
// Найдёт "100", потому что после него идёт " рублей"
// "200" не найдёт, так как после него " долларов"
// Отрицательный lookahead: X(?!Y) — X, за которым НЕ следует Y
Pattern pattern2 = Pattern.compile("\d+(?! рублей)");
// Найдёт "200", так как после него не " рублей"
// Lookbehind: (?<=X)Y — Y, перед которым идёт X
Pattern pattern3 = Pattern.compile("(?<=\$)\d+");
// Найдёт цифры, перед которыми стоит знак доллара
Примеры кода — от простого к сложному
Пример 1: Проверка, является ли строка числом
public class Example1 {
public static void main(String[] args) {
// Задача: проверить, состоит ли строка только из цифр
String text1 = "12345";
String text2 = "12a45";
String text3 = " 123 "; // с пробелами
// Шаблон: ^ — начало, d+ — одна или более цифр, $ — конец
String regex = "^\d+$";
System.out.println(text1 + " — число? " + text1.matches(regex)); // true
System.out.println(text2 + " — число? " + text2.matches(regex)); // false
System.out.println(text3 + " — число? " + text3.matches(regex)); // false
// Объяснение:
// ^ — якорь начала строки. Без него строка могла бы начинаться с чего угодно
// d+ — одна или несколько цифр
// $ — якорь конца строки. Без него строка могла бы заканчиваться чем угодно
// Вместе ^ и $ гарантируют, что ВСЯ строка состоит только из цифр
}
}
Пример 2: Извлечение всех email-адресов из текста
import java.util.regex.*;
import java.util.ArrayList;
public class Example2 {
public static void main(String[] args) {
String text = "Контакты: ivan@mail.ru, petr@yandex.ru. " +
"Также пишите на support@company.com или admin@site.org";
// Шаблон для email:
// [w.-]+ — имя пользователя (буквы, цифры, точки, дефисы)
// @ — собачка
// [w.-]+ — домен
// . — точка (экранированная)
// [a-zA-Z]{2,} — доменная зона (ru, com, org и т.д.)
String regex = "[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
ArrayList emails = new ArrayList<>();
// find() ищет следующее совпадение, пока они есть
while (matcher.find()) {
emails.add(matcher.group()); // group() возвращает найденный текст
}
System.out.println("Найденные email-адреса:");
for (String email : emails) {
System.out.println(" - " + email);
}
// Вывод:
// Найденные email-адреса:
// - ivan@mail.ru
// - petr@yandex.ru
// - support@company.com
// - admin@site.org
}
}
Пример 3: Замена всех номеров телефонов на скрытый формат
public class Example3 {
public static void main(String[] args) {
String text = "Позвоните по номеру +7 (123) 456-78-90 или 8-800-555-35-35";
// Шаблон для российского номера:
// +?7|8 — начинается с +7 или 8
// [s-()]* — возможные разделители (пробелы, дефисы, скобки)
// d{3} — три цифры (код города или первые три)
// [s-()]* — снова разделители
// d{3}[s-]*d{2}[s-]*d{2} — остальные цифры
String regex = "(\+?7|8)[\s\-\(\)]*(\d{3})[\s\-\(\)]*(\d{3})[\s\-]*(\d{2})[\s\-]*(\d{2})";
// Заменяем на скрытый формат, используя группы
// $1 — первая группа (+7 или 8)
// $2 — вторая группа (первые три цифры)
// Остальные заменяем на ***
String result = text.replaceAll(regex, "$1 ($2) ***-**-**");
System.out.println("Исходный текст: " + text);
System.out.println("После замены: " + result);
// Вывод:
// Исходный текст: Позвоните по номеру +7 (123) 456-78-90 или 8-800-555-35-35
// После замены: Позвоните по номеру +7 (123) ***-**-** или 8 (800) ***-**-**
}
}
Пример 4: Разбор лога сервера
import java.util.regex.*;
public class Example4 {
public static void main(String[] args) {
// Представим, что у нас есть лог веб-сервера
String logLine = "192.168.1.1 - - [10/Oct/2023:13:55:36 +0300] "GET /index.html HTTP/1.1" 200 2326";
// Разобьём лог на части с помощью групп
// (d+.d+.d+.d+) — IP-адрес (группа 1)
// [([^]]+)] — дата в квадратных скобках (группа 2)
// "([^"]+)" — запрос в кавычках (группа 3)
// (d+) — статус-код (группа 4)
// (d+) — размер ответа (группа 5)
String regex = "(\d+\.\d+\.\d+\.\d+).*\[([^\]]+)\]\s*"([^"]+)"\s*(\d+)\s*(\d+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(logLine);
if (matcher.matches()) { // matches() — вся строка должна совпасть
System.out.println("IP-адрес: " + matcher.group(1));
System.out.println("Дата и время: " + matcher.group(2));
System.out.println("Запрос: " + matcher.group(3));
System.out.println("Статус-код: " + matcher.group(4));
System.out.println("Размер ответа: " + matcher.group(5) + " байт");
}
// Вывод:
// IP-адрес: 192.168.1.1
// Дата и время: 10/Oct/2023:13:55:36 +0300
// Запрос: GET /index.html HTTP/1.1
// Статус-код: 200
// Размер ответа: 2326 байт
}
}
Пример 5: Валидация пароля
public class Example5 {
public static void main(String[] args) {
// Задача: пароль должен содержать:
// - минимум 8 символов
// - хотя бы одну заглавную букву
// - хотя бы одну строчную букву
// - хотя бы одну цифру
// - хотя бы один спецсимвол (!@#$%^&*)
String[] passwords = {
"weak", // слишком короткий
"12345678", // только цифры
"Password", // нет цифр и спецсимволов
"Password1", // нет спецсимвола
"Pass1!", // слишком короткий
"StrongP@ss1" // подходит!
};
// Используем lookahead для проверки каждого условия
// (?=.*[A-Z]) — где-то есть заглавная буква
// (?=.*[a-z]) — где-то есть строчная буква
// (?=.*d) — где-то есть цифра
// (?=.*[!@#$%^&*]) — где-то есть спецсимвол
// .{8,} — минимум 8 символов
String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$";
for (String password : passwords) {
boolean isValid = password.matches(regex);
System.out.println(password + " -> " + (isValid ? "✅ подходит" : "❌ не подходит"));
}
// Вывод:
// weak -> ❌ не подходит
// 12345678 -> ❌ не подходит
// Password -> ❌ не подходит
// Password1 -> ❌ не подходит
// Pass1! -> ❌ не подходит
// StrongP@ss1 -> ✅ подходит
}
}
Пример 6: Поиск и подсчёт слов в тексте
import java.util.regex.*;
import java.util.HashMap;
import java.util.Map;
public class Example6 {
public static void main(String[] args) {
String text = "Кот и собака. Кот любит молоко, а собака любит кости. " +
"Кот и собака — друзья?";
// Найдём все слова (только буквы, минимум 2 символа)
// — граница слова
// [а-яА-ЯёЁ]+ — одна или более русских букв
// {2,} — минимум 2 символа (чтобы исключить союзы "и", "а")
String regex = "\b[а-яА-ЯёЁ]{2,}\b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text.toLowerCase()); // приводим к нижнему регистру
Map wordCount = new HashMap<>();
while (matcher.find()) {
String word = matcher.group();
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
System.out.println("Статистика слов:");
for (Map.Entry entry : wordCount.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
// Вывод:
// Статистика слов:
// кот: 3
// собака: 2
// любит: 2
// молоко: 1
// кости: 1
// друзья: 1
}
}
Пример 7: Работа с многострочным текстом
import java.util.regex.*;
public class Example7 {
public static void main(String[] args) {
String multilineText = "Строка 1: Привет
" +
"Строка 2: Мир
" +
"Строка 3: Java
" +
"Строка 4: Regex
" +
"Строка 5: Конец";
// Найдём все строки, которые начинаются с "Строка"
// Без флага MULTILINE ^ означает начало всей строки
// С флагом MULTILINE ^ означает начало каждой строки
Pattern patternWithoutFlag = Pattern.compile("^Строка.*");
Pattern patternWithFlag = Pattern.compile("^Строка.*", Pattern.MULTILINE);
System.out.println("Без MULTILINE:");
Matcher m1 = patternWithoutFlag.matcher(multilineText);
while (m1.find()) {
System.out.println(" " + m1.group());
}
// Вывод: только первая строка
System.out.println("
С MULTILINE:");
Matcher m2 = patternWithFlag.matcher(multilineText);
while (m2.find()) {
System.out.println(" " + m2.group());
}
// Вывод: все пять строк
// Ещё один полезный флаг — DOTALL
// Без него точка (.) не совпадает с переводом строки
// С DOTALL точка совпадает с любым символом, включая
Pattern patternDotAll = Pattern.compile("Строка.*Конец", Pattern.DOTALL);
Matcher m3 = patternDotAll.matcher(multilineText);
if (m3.find()) {
System.out.println("
С DOTALL найдено: " + m3.group());
}
// Найдёт весь текст от "Строка" до "Конец", включая переносы строк
}
}
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python или планируешь изучать его позже, полезно знать различия в работе с регулярными выражениями.
| Характеристика | Java | Python |
|---|---|---|
| Импорт | import java.util.regex.* |
import re |
| Компиляция шаблона | Pattern.compile("regex") |
re.compile(r"regex") |
| Поиск первого совпадения | matcher.find() |
re.search(pattern, text) |
| Поиск всех совпадений | Цикл while(matcher.find()) |
re.findall(pattern, text) |
| Проверка полного совпадения | str.matches(regex) или matcher.matches() |
re.fullmatch(pattern, text) |
| Замена | str.replaceAll(regex, replacement) |
re.sub(pattern, replacement, text) |
| Сырые строки (raw strings) | Нет. Нужно экранировать обратные слеши: "\d" |
Есть. r"d" — обратные слеши не экранируются |
| Флаги | Константы класса Pattern: Pattern.CASE_INSENSITIVE |
Флаги в виде букв: re.IGNORECASE или re.I |
| Lookbehind | Поддерживается, но длина должна быть фиксированной | Поддерживается, длина может быть переменной (в новых версиях) |
| Производительность | Высокая, но требуется явная компиляция Pattern | Хорошая, но при многократном использовании лучше компилировать |
💡 Главное отличие для новичка: В Java нужно экранировать обратные слеши. Если в Python ты пишешьr"d", то в Java —"\d". Это связано с тем, что в Java строки тоже используют обратный слеш для escape-последовательностей (,). Поэтому для передачи одного обратного слеша в regex нужно написать два.
Типичные ошибки и подводные камни
Ошибка 1: Забыли экранировать обратный слеш
// ❌ Неправильно
Pattern pattern = Pattern.compile("d+"); // Java думает, что d — это неизвестная escape-последовательность
// ✅ Правильно
Pattern pattern = Pattern.compile("\d+"); // Два слеша — один для Java, один для regex
Ошибка 2: Путаница между matches() и find()
String text = "Привет, мир!";
String regex = "мир";
// matches() проверяет, совпадает ли ВСЯ строка
System.out.println(text.matches(regex)); // false — строка не равна "мир"
// find() ищет подстроку