Регулярные выражения в Java
Научимся работать с шаблонами поиска текста: от простых совпадений до сложных валидаций email и телефонов. Разберём классы Pattern и Matcher, все основные конструкции regex и множество практических примеров.
Что такое регулярные выражения и зачем они нужны
Представьте, что вы ищете特定ное слово в огромной книге. Без регулярных выражений вы читали бы страницу за страницей, вручную сравнивая каждое слово. А теперь представьте, что у вас есть волшебная лупа, которая сама находит все слова по вашему описанию — например: «найди все слова, которые начинаются на букву «А», содержат ровно 5 букв и заканчиваются на «ов». Это и есть регулярные выражения!
Регулярные выражения (или «regex») — это специальные шаблоны (паттерны), которые описывают набор строк. Они позволяют искать, проверять и заменять части текста по сложным правилам. Вот несколько реальных ситуаций, где они незаменимы:
- Проверка email-адреса: ввели ли пользователь адрес в правильном формате?
- Поиск телефонных номеров: найти все номера телефона в документе, даже если они записаны по-разному: +7(999)123-45-67, 8 999 123 45 67, +79991234567
- Валидация пароля: содержит ли пароль минимум 8 символов, хотя бы одну заглавную букву и одну цифру?
- Замена текста: заменить все повторяющиеся пробелы на один
- Извлечение данных: извлечь все даты из текста в формате ДД.ММ.ГГГГ
Давайте начнём с самого простого примера. Допустим, у нас есть строка и мы хотим проверить, содержит ли она слово «привет»:
public class RegexBasic {
public static void main(String[] args) {
String text = "Привет, мир!";
// Самый простой способ — использовать matches()
boolean contains = text.matches(".*привет.*");
System.out.println(contains);
}
}
Здесь .*привет.* — это регулярное выражение. Оно означает: «любые символы (.*), затем слово «привет», затем снова любые символы (.*).» Метод matches() проверяет, соответствует ли вся строка этому шаблону.
Важно понимать: Регулярные выражения — это не просто «поиск текста». Это язык описания шаблонов. Вы описываете, КАКИМ должен быть текст, а компьютер сам ищет все совпадения. Это гораздо мощнее простого поиска по строке!
Ещё один простой пример — проверка, является ли строка числом:
public class RegexIsDigit {
public static void main(String[] args) {
String number1 = "12345";
String number2 = "12abc";
String number3 = "12.5";
// \d+ означает: одна или более цифр
System.out.println(number1.matches("\\d+")); // true
System.out.println(number2.matches("\\d+")); // false
System.out.println(number3.matches("\\d+")); // false
// А это — любое количество цифр (включая 0)
System.out.println(number1.matches("\\d*")); // true
System.out.println(number2.matches("\\d*")); // false
}
}
Здесь \\d+ означает «одна или более цифр». Обратите внимание на двойной обратный слэш — в Java строках символ \ является экранирующим, поэтому чтобы получить один обратный слэш в regex, нужно написать \\ в строке Java.
Класс Pattern: компиляция регулярных выражений
В Java регулярные выражения работают через два основных класса: Pattern и Matcher. Класс Pattern — это скомпилированное регулярное выражение. Подумайте о нём как о «рецепте»: вы сначала записываете рецепт (регулярное выражение), затем компилируете его (превращаете в объект Pattern), а потом используете этот объект многократно.
Почему бы не использовать регулярное выражение прямо из строки? Дело в том, что компиляция — это процесс разбора шаблона и создания внутренней структуры данных. Если вы компилируете Pattern один раз и используете его много раз, это работает быстрее, чем каждый раз разбирать строку заново.
Создадим наш первый Pattern:
import java.util.regex.Pattern;
public class PatternExample {
public static void main(String[] args) {
// Компилируем регулярное выражение в объект Pattern
Pattern pattern = Pattern.compile("привет");
// Проверяем, что строка соответствует шаблону
boolean matches = pattern.matcher("привет, мир!").matches();
System.out.println(matches); // false — entire string must match
}
}
Здесь метод matches() проверяет, соответствует ли ВСЯ строка шаблону. Строка «привет, мир!» не соответствует шаблону «привет» целиком — там ещё есть «, мир!». Поэтому результат false. Чтобы найти подстроку, нужен метод find() класса Matcher, который мы разберём позже.
Флаги Pattern: настройка поведения
При компиляции можно указать «флаги» — параметры, которые меняют поведение regex. Это как настройки в приложении: вы можете включить/выключить определённые опции.
CASE_INSENSITIVE — игнорировать регистр букв. Это очень полезно, когда вы ищете слово, но не знаете, в каком регистре оно написано:
import java.util.regex.Pattern;
public class CaseInsensitiveExample {
public static void main(String[] args) {
// Без флага — регистр важен
Pattern caseSensitive = Pattern.compile("привет");
System.out.println(caseSensitive.matcher("Привет").matches()); // false
// С флагом — регистр не важен
Pattern caseInsensitive = Pattern.compile("привет", Pattern.CASE_INSENSITIVE);
System.out.println(caseInsensitive.matcher("Привет").matches()); // true
System.out.println(caseInsensitive.matcher("ПРИВЕТ").matches()); // true
System.out.println(caseInsensitive.matcher("привет").matches()); // true
}
}
MULTILINE — позволяет символам ^ и $ работать для каждой строки, а не только для всей строки:
import java.util.regex.Pattern;
public class MultilineExample {
public static void main(String[] args) {
String text = "Первая строка\nВторая строка\nТретья строка";
// Без MULTILINE — ^ означает начало всей строки
Pattern withoutMultiline = Pattern.compile("^Вторая");
System.out.println(withoutMultiline.matcher(text).find()); // false
// С MULTILINE — ^ означает начало каждой строки
Pattern withMultiline = Pattern.compile("^Вторая", Pattern.MULTILINE);
System.out.println(withMultiline.matcher(text).find()); // true
}
}
DOTALL — позволяет символу «точка» (.) совпадать с символом новой строки (\n):
import java.util.regex.Pattern;
public class DotAllExample {
public static void main(String[] args) {
String text = "Привет\nмир";
// Без DOTALL — точка не совпадает с \n
Pattern withoutDotAll = Pattern.compile("Привет.*мир");
System.out.println(withoutDotAll.matcher(text).matches()); // false
// С DOTALL — точка совпадает с любым символом, включая \n
Pattern withDotAll = Pattern.compile("Привет.*мир", Pattern.DOTALL);
System.out.println(withDotAll.matcher(text).matches()); // true
}
}
Можно комбинировать несколько флагов, используя побитовое ИЛИ (|):
import java.util.regex.Pattern;
public class MultipleFlagsExample {
public static void main(String[] args) {
// CASE_INSENSITIVE | MULTILINE — оба флага одновременно
Pattern pattern = Pattern.compile(
"^привет$",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
);
String text = "Привет\nПРИВЕТ\nпривет\nДругое";
System.out.println(pattern.matcher(text).find()); // true
}
}
Аналогия: Подумайте о Pattern как о «штампе». Вы создаёте штамп (компилируете regex), а потом прикладываете его к разным документам (строкам), чтобы проверить, подходят ли они. Штамп можно настроить (флаги): например, сделать его нечувствительным к регистру.
Класс Matcher: поиск и извлечение совпадений
Если Pattern — это «рецепт», то Matcher — это «повар», который этот рецепт выполняет. Matcher применяет шаблон к конкретной строке и находит все совпадения.
Создадим Matcher и разберём его основные методы:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class MatcherBasic {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("Абырвалг 123 строка 456");
// find() — ищет следующее совпадение
while (matcher.find()) {
System.out.println("Найдено: " + matcher.group());
System.out.println("Начало: " + matcher.start());
System.out.println("Конец: " + matcher.end());
System.out.println("---");
}
}
}
Вывод программы:
Найдено: 123
Начало: 8
Конец: 11
---
Найдено: 456
Начало: 20
Конец: 23
---
Метод matches() — проверка всей строки
Метод matches() проверяет, соответствует ли ВСЯ строка шаблону. Это строгая проверка — от первого до последнего символа:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class MatchesExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\d{3}-\\d{2}-\\d{2}");
// matches() требует, чтобы ВСЯ строка соответствовала шаблону
Matcher m1 = pattern.matcher("123-45-67");
System.out.println(m1.matches()); // true — вся строка совпадает
Matcher m2 = pattern.matcher("Номер: 123-45-67");
System.out.println(m2.matches()); // false — есть лишний текст
Matcher m3 = pattern.matcher("123-45-67 лишний текст");
System.out.println(m3.matches()); // false — есть лишний текст
}
}
Метод find() — поиск подстрок
Метод find() ищет следующее вхождение шаблона в строке. Он возвращает true, если совпадение найдено, и false, если больше совпадений нет. Это как листать книгу страница за страницей и отмечать нужные места:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class FindExample {
public static void main(String[] args) {
String text = "В слове «привет» 6 букв, а в слове «мир» — 3";
Pattern pattern = Pattern.compile("\\b\\w+\\b");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Слово: «" + matcher.group() + "»");
}
}
}
Здесь \b обозначает границу слова, а \w+ — одно или более буквенно-цифровых символов. Программа выведет все слова из текста.
Метод group() — извлечение совпадения
После каждого успешного вызова find() вы можете получить найденную подстроку методом group():
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class GroupExample {
public static void main(String[] args) {
String text = "Телефоны: +7(999)123-45-67 и +7(911)987-65-43";
Pattern pattern = Pattern.compile("\\+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Найден телефон: " + matcher.group());
}
}
}
Программа выведет оба найденных телефона. Метод group() без аргумента возвращает всю строку, соответствующую шаблону (эквивалент group(0)).
Методы start() и end() — позиция совпадения
Методы start() и end() возвращают позиции начала и конца совпадения в строке. Это полезно, когда нужно знать, ГДЕ именно в тексте находится найденный фрагмент:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class PositionExample {
public static void main(String[] args) {
String text = "Я люблю Java и Python";
Pattern pattern = Pattern.compile("\\b(Java|Python)\\b");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Язык: " + matcher.group());
System.out.println("Позиция: " + matcher.start() + " - " + matcher.end());
System.out.println("Контекст: ..." + text.substring(
Math.max(0, matcher.start() - 5),
Math.min(text.length(), matcher.end() + 5)
) + "...");
System.out.println("---");
}
}
}
Этот пример найдёт оба языка программирования и покажет их позицию в строке, а также небольшой контекст вокруг каждого совпадения.
Важно: Метод find() можно вызывать многократно — каждый раз он ищет следующее совпадение после предыдущего. Когда совпадений больше нет, он возвращает false. Обычно его используют в цикле while(matcher.find()).
Основная синтаксика регулярных выражений
Теперь давайте разберём основные конструкции регулярных выражений. Каждая из них — это «кирпичик», из которого вы строите шаблоны. Начнём с самых базовых:
Точка (.) — любой символ
Точка совпадает с ЛЮБЫМ одиночным символом (кроме символа новой строки, если не установлен флаг DOTALL). Это как « wild карта » в карточных играх:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class DotExample {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("пр.вет");
System.out.println(pattern.matcher("привет").matches()); // true
System.out.println(pattern.matcher("прпвет").matches()); // true
System.out.println(pattern.matcher("пр-вет").matches()); // true
System.out.println(pattern.matcher("прывет").matches()); // true
System.out.println(pattern.matcher("првет").matches()); // false — нет символа
System.out.println(pattern.matcher("привети").matches()); // false — лишний символ
}
}
Квадратные скобки [] — набор символов
Квадратные скобки определяют «набор» символов. Регулярное выражение совпадёт с любым ОДНИМ символом из этого набора:
import java.util.regex.Pattern;
public class CharacterSetExample {
public static void main(String[] args) {
// [abc] — совпадает с a, b или c
Pattern vowels = Pattern.compile("[aeiouy]");
System.out.println(vowels.matcher("a").matches()); // true
System.out.println(vowels.matcher("e").matches()); // true
System.out.println(vowels.matcher("x").matches()); // false
// [a-z] — диапазон от a до z
Pattern lowerLetter = Pattern.compile("[a-z]");
System.out.println(lowerLetter.matcher("m").matches()); // true
System.out.println(lowerLetter.matcher("M").matches()); // false
// [a-zA-Z] — любая латинская буква
Pattern anyLetter = Pattern.compile("[a-zA-Z]");
System.out.println(anyLetter.matcher("A").matches()); // true
System.out.println(anyLetter.matcher("z").matches()); // true
// [0-9] — любая цифра
Pattern digit = Pattern.compile("[0-9]");
System.out.println(digit.matcher("5").matches()); // true
System.out.println(digit.matcher("a").matches()); // false
}
}
^ и $ — начало и конец строки
Символ ^ обозначает начало строки, а $ — конец строки. Это как проверка «от края до края»:
import java.util.regex.Pattern;
public class AnchorsExample {
public static void main(String[] args) {
// ^Hello — строка должна НАЧИНАТЬСЯ с Hello
Pattern startsWith = Pattern.compile("^Hello");
System.out.println(startsWith.matcher("Hello World").matches()); // true
System.out.println(startsWith.matcher("Say Hello").matches()); // false
// World$ — строка должна ЗАКАНЧИВАТЬСЯ на World
Pattern endsWith = Pattern.compile("World$");
System.out.println(endsWith.matcher("Hello World").matches()); // true
System.out.println(endsWith.matcher("World Hello").matches()); // false
// ^Hello$ — строка должна быть ТОЧНО "Hello"
Pattern exact = Pattern.compile("^Hello$");
System.out.println(exact.matcher("Hello").matches()); // true
System.out.println(exact.matcher("Hello World").matches()); // false
}
}
Аналогия: Подумайте о ^ и $ как о «стенам» по краям строки. ^Hello — значит «слово должно упираться в левую стену», а World$ — «упираться в правую».
Скобки () — группы
Скобки создают «группы захвата» — они позволяют выделить часть совпадения и работать с ней отдельно. Это как выделить фрагмент текста маркером:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class GroupsExample {
public static void main(String[] args) {
// Захватываем день, месяц и год из даты
Pattern pattern = Pattern.compile("(\\d{2})\\.(\\d{2})\\.(\\d{4})");
Matcher matcher = pattern.matcher("Дата: 25.12.2023");
if (matcher.find()) {
System.out.println("Полное совпадение: " + matcher.group(0)); // 25.12.2023
System.out.println("День: " + matcher.group(1)); // 25
System.out.println("Месяц: " + matcher.group(2)); // 12
System.out.println("Год: " + matcher.group(3)); // 2023
}
}
}
Каждая пара скобок — это отдельная группа. Группа нумеруется с 1. Группа 0 — это всегда всё совпадение целиком.
Фигурные скобки {} — количество повторений
Фигурные скобки позволяют указать точное количество повторений или диапазон:
import java.util.regex.Pattern;
public class QuantifierBracesExample {
public static void main(String[] args) {
// {3} — ровно 3 раза
Pattern exactly3 = Pattern.compile("а{3}");
System.out.println(exactly3.matcher("ааа").matches()); // true
System.out.println(exactly3.matcher("аааа").matches()); // false
// {2,4} — от 2 до 4 раз
Pattern between2and4 = Pattern.compile("а{2,4}");
System.out.println(between2and4.matcher("аа").matches()); // true
System.out.println(between2and4.matcher("аааааа").matches()); // false — 6 раз
// {3,} — 3 и более раз
Pattern atLeast3 = Pattern.compile("а{3,}");
System.out.println(atLeast3.matcher("ааа").matches()); // true
System.out.println(atLeast3.matcher("аааааааа").matches()); // true
}
}
Экранирование спецсимволов
Некоторые символы имеют специальное значение в regex: . * + ? ^ $ [] () {} \ |. Если вы хотите найти ЛИТЕРАЛЬНЫЙ символ (а не использовать его как спецсимвол), нужно «экранировать» его обратным слэшем:
import java.util.regex.Pattern;
public class EscapeExample {
public static void main(String[] args) {
// Ищемliteralную точку (не "любой символ")
Pattern dot = Pattern.compile("\\.");
System.out.println(dot.matcher(".").matches()); // true
System.out.println(dot.matcher("a").matches()); // false
// Ищемliteralную звёздочку
Pattern star = Pattern.compile("\\*");
System.out.println(star.matcher("*").matches()); // true
System.out.println(star.matcher("a").matches()); // false
// Ищемliteralную скобку
Pattern parenthesis = Pattern.compile("\\(");
System.out.println(parenthesis.matcher("(").matches()); // true
System.out.println(parenthesis.matcher(")").matches()); // false
}
}
В Java строках обратный слэш сам по себе является спецсимволом, поэтому нужно удваивать: \\. для точки, \\* для звёздочки и т.д.
Классы символов: \\d, \\w, \\s и другие
Вместо того чтобы перечислять все возможные символы в квадратных скобках, можно использовать «классы символов» — готовые наборы, обозначённые специальными последовательностями. Это как сокращения: вместо того чтобы писать «все цифры», вы просто пишете \d.
\d — любая цифра (digit)
Эквивалент [0-9]. Совпадает с любой цифрой от 0 до 9:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class DigitExample {
public static void main(String[] args) {
String text = "В моём номере 123456, а в твоём 789012";
Pattern pattern = Pattern.compile("\\d");
Matcher matcher = pattern.matcher(text);
int count = 0;
while (matcher.find()) {
count++;
System.out.println("Цифра " + count + ": " + matcher.group() + " на позиции " + matcher.start());
}
System.out.println("Всего цифр: " + count);
}
}
\D — любая не-цифра
Это «инверсия» \d. Совпадает с любым символом, КРОМЕ цифры:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NotDigitExample {
public static void main(String[] args) {
String text = "АБ123ВГ";
Pattern pattern = Pattern.compile("\\D");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Не-цифра: «" + matcher.group() + "» на позиции " + matcher.start());
}
}
}
\w — буква, цифра или подчёркивание (word)
Эквивалент [a-zA-Z0-9_]. Совпадает с любой «словесной» буквой (латиницей), цифрой или подчёркиванием:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class WordCharExample {
public static void main(String[] args) {
String text = "variable_name = 123; // comment";
Pattern pattern = Pattern.compile("\\w+");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Слово: «" + matcher.group() + "»");
}
}
}
Этот пример найдёт все «слова» (буквенно-цифровые последовательности) в строке кода.
\W — не «словесный» символ
Инверсия \w. Совпадает с любым символом, КРОМЕ букв, цифр и подчёркивания — то есть пробелы, знаки препинания, специальные символы:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NotWordCharExample {
public static void main(String[] args) {
String text = "Привет, мир!";
Pattern pattern = Pattern.compile("\\W");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Не-словесный символ: «" + matcher.group() + "» на позиции " + matcher.start());
}
}
}
\s — пробел или другой whitespace
Совпадает с любым пробельным символом: обычный пробел, табуляция (\t), перенос строки (\n), возврат каретки (\r) и другие:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class WhitespaceExample {
public static void main(String[] args) {
String text = "Привет\tмир\n新的一行";
Pattern pattern = Pattern.compile("\\s");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Пробел на позиции " + matcher.start() +
" (тип: " + (int) matcher.group().charAt(0) + ")");
}
}
}
\S — непробельный символ
Инверсия \s. Совпадает с любым символом, КРОМЕ пробельных:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NotWhitespaceExample {
public static void main(String[] args) {
String text = "А Б В";
Pattern pattern = Pattern.compile("\\S+");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Непробельная часть: «" + matcher.group() + "»");
}
}
}
\b — граница слова
Совпадает с позицией «на границе слова» — между «словесным» и «не-словесным» символом. Это не символ, а «невидимая позиция». Очень полезно для поиска точных слов:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class WordBoundaryExample {
public static void main(String[] args) {
String text = "кот каталог катушка";
// Ищем точное слово "кот" (без "каталог" и "катушка")
Pattern pattern = Pattern.compile("\\bкот\\b");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Точное совпадение: «" + matcher.group() + "»");
}
System.out.println();
// Без \b — найдёт "кот" и в "каталог", и в "катушка"
Pattern pattern2 = Pattern.compile("кот");
Matcher matcher2 = pattern2.matcher(text);
while (matcher2.find()) {
System.out.println("Простой поиск: «" + matcher2.group() + "» на позиции " + matcher2.start());
}
}
}
Аналогия: Подумайте о классах символов как о «коробках с инструментами». Вместо того чтобы перечислять каждый гаечный ключ отдельно, вы берёте коробку \d (все цифры) или \w (все «словесные» символы). Буква в верхнем регистре (\D, \W, \S) — это «инвертированная» коробка, содержащая ВСЁ, КРОМЕ содержимого нижнерегистровой версии.
Сводная таблица классов символов
| Символ | Описание | Эквивалент |
|---|---|---|
\d | Цифра | [0-9] |
\D | Не-цифра | [^0-9] |
\w | Буква, цифра или _ | [a-zA-Z0-9_] |
\W | Не «словесный» символ | [^a-zA-Z0-9_] |
\s | Пробел или whitespace | [ \t\n\r\f] |
\S | Непробельный символ | [^ \t\n\r\f] |
\b | Граница слова | — |
Квантификаторы: сколько раз повторять
Квантификаторы определяют, СКОЛЬКО РАЗ предыдущий элемент должен повторяться. Это как «множитель»: вы говорите «букву «а» нужно встретить минимум 2 раза, максимум 5». Давайте разберём все квантификаторы:
* — ноль или более повторений
Звёздочка означает «ноль или более». Элемент может отсутствовать совсем, а может повторяться бесконечно:
import java.util.regex.Pattern;
public class StarExample {
public static void main(String[] args) {
// "а*" — ноль или более "а"
Pattern pattern = Pattern.compile("^а*$");
System.out.println(pattern.matcher("").matches()); // true — ноль "а"
System.out.println(pattern.matcher("а").matches()); // true — одно "а"
System.out.println(pattern.matcher("ааааа").matches()); // true — пять "а"
System.out.println(pattern.matcher("аб").matches()); // false — есть "б"
}
}
+ — одно или более повторений
Плюс означает «одно или более». Элемент ДОЛЖЕН встретиться хотя бы один раз:
import java.util.regex.Pattern;
public class PlusExample {
public static void main(String[] args) {
// "а+" — одно или более "а"
Pattern pattern = Pattern.compile("^а+$");
System.out.println(pattern.matcher("").matches()); // false — ни одного "а"
System.out.println(pattern.matcher("а").matches()); // true — одно "а"
System.out.println(pattern.matcher("ааааа").matches()); // true — пять "а"
System.out.println(pattern.matcher("аб").matches()); // false — есть "б"
}
}
? — ноль или одно повторение
Вопросительный знак означает «ноль или одно». Элемент необязателен — он может быть, а может и не быть:
import java.util.regex.Pattern;
public class QuestionExample {
public static void main(String[] args) {
// "кол-?ор" — "colo" или "color"
Pattern pattern = Pattern.compile("кол-?ор");
System.out.println(pattern.matcher("колор").matches()); // true
System.out.println(pattern.matcher("кол-ор").matches()); // true
System.out.println(pattern.matcher("кол--ор").matches()); // false — два минуса
}
}
{n} — ровно n повторений
import java.util.regex.Pattern;
public class ExactlyExample {
public static void main(String[] args) {
// \d{4} — ровно 4 цифры (год)
Pattern year = Pattern.compile("\\d{4}");
System.out.println(year.matcher("2023").matches()); // true
System.out.println(year.matcher("23").matches()); // false
System.out.println(year.matcher("20235").matches()); // false
}
}
{n,m} — от n до m повторений
import java.util.regex.Pattern;
public class RangeExample {
public static void main(String[] args) {
// \d{2,4} — от 2 до 4 цифр
Pattern pattern = Pattern.compile("\\d{2,4}$");
System.out.println(pattern.matcher("12").matches()); // true
System.out.println(pattern.matcher("123").matches()); // true
System.out.println(pattern.matcher("1234").matches()); // true
System.out.println(pattern.matcher("1").matches()); // false
System.out.println(pattern.matcher("12345").matches()); // false
}
}
{n,} — n и более повторений
import java.util.regex.Pattern;
public class AtLeastExample {
public static void main(String[] args) {
// \d{3,} — 3 и более цифр (минимум 3)
Pattern pattern = Pattern.compile("\\d{3,}$");
System.out.println(pattern.matcher("123").matches()); // true
System.out.println(pattern.matcher("1234567").matches()); // true
System.out.println(pattern.matcher("12").matches()); // false
}
}
Жадный vs ленивый (greedy vs reluctant)
Это один из самых важных и самых непонятных для новичков моментов. По умолчанию все квантификаторы являются жадными (greedy) — они стараются захватить как МОЖНО БОЛЬШЕ символов. Но иногда нужно обратное — захватить как МОЖНО МЕНЬШЕ. Для этого после квантификатора ставят вопросительный знак:
*— жадный (максимум)*?— ленивый (минимум)+— жадный+?— ленивый{n,m}— жадный{n,m}?— ленивый
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class GreedyVsReluctant {
public static void main(String[] args) {
String text = "<p>Привет</p><p>Мир</p>";
// Жадный: .* захватит всё до последнего </p>
Pattern greedy = Pattern.compile("<p>(.*)</p>");
Matcher m1 = greedy.matcher(text);
if (m1.find()) {
System.out.println("Жадный: «" + m1.group(1) + "»");
}
// Ленивый: .*? захватит до первого </p>
Pattern reluctant = Pattern.compile("<p>(.*?)</p>");
Matcher m2 = reluctant.matcher(text);
while (m2.find()) {
System.out.println("Ленивый: «" + m2.group(1) + "»");
}
}
}
Жадный квантификатор захватит «Привет</p><p>Мир» целиком, потому что он «жадный» и хочет взять как можно больше. Ленивый захватит только «Привет», а потом на втором проходе — «Мир». Это принципиальная разница!
Аналогия: Жадный квантификатор — как голодный человек за столом: он берёт ВСЁ, что может поместиться в тарелку. Ленивый — как диетолог: он берёт ровно столько, сколько нужно, не больше. Если вам нужно найти содержимое между тегами HTML, ленивый квантификатор часто работает правильнее, потому что он останавливается на ПЕРВОМ закрывающем теге.
Группы и захват подвыражений
Группы — одна из самых мощных возможностей регулярных выражений. Они позволяют не просто находить совпадения, но и «выделять» части совпадения для дальнейшей обработки. Представьте, что вы нашли телефонный номер, но хотите отдельно получить код страны, код города и сам номер — группы позволяют именно это.
Простые группы (нумерованные)
Каждая пара скобок в regex создаёт группу. Группы нумеруются слева направо, начиная с 1:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NumberedGroups {
public static void main(String[] args) {
// Разбираем email на имя пользователя и домен
String email = "user@example.com";
Pattern pattern = Pattern.compile("(.+)@(.+\\.+)");
Matcher matcher = pattern.matcher(email);
if (matcher.matches()) {
System.out.println("Полное совпадение (group 0): " + matcher.group(0));
System.out.println("Имя пользователя (group 1): " + matcher.group(1));
System.out.println("Домен (group 2): " + matcher.group(2));
System.out.println("Всего групп: " + matcher.groupCount());
}
}
}
Результат:
Полное совпадение (group 0): user@example.com
Имя пользователя (group 1): user
Домен (group 2): example.com
Всего групп: 2
Именованные группы
Для больших и сложных шаблонов нумерованные группы становятся неудобными — легко запутаться, какая группа что содержит. Именованные группы решают эту проблему:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NamedGroups {
public static void main(String[] args) {
// Извлекаем компоненты URL
String url = "https://www.example.com:8080/path/to/page?key=value";
Pattern pattern = Pattern.compile(
"(?<scheme>https?)://(?<host>[^:]+)(?::(?<port>\\d+))?(?<path>[^?]*)"
);
Matcher matcher = pattern.matcher(url);
if (matcher.matches()) {
System.out.println("Протокол: " + matcher.group("scheme"));
System.out.println("Хост: " + matcher.group("host"));
System.out.println("Порт: " + matcher.group("port"));
System.out.println("Путь: " + matcher.group("path"));
}
}
}
Вот как это работает пошагово:
(?<scheme>https?)— группа с именем «scheme» захватывает «http» или «https»://— литеральная строка «://»(?<host>[^:]+)— группа «host» захватывает всё до двоеточия или конца(?::(?<port>\\d+))?— опциональная группа «port» захватывает число после двоеточия(?<path>[^?]*)— группа «path» захватывает путь до вопросительного знака
Незахватывающие группы (?:...)
Иногда вам нужны скобки для группировки (чтобы применить квантификатор к группе символов), но вы НЕ хотите захватывать эту группу. Для этого используют (?:...):
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NonCapturingGroups {
public static void main(String[] args) {
String text = "cat cats dog dogs";
// Захватывающая группа — создаёт group(1)
Pattern capturing = Pattern.compile("(cat|dog)s?");
Matcher m1 = capturing.matcher(text);
while (m1.find()) {
System.out.println("Захватывающая: совпадение=" + m1.group(0) + ", group(1)=" + m1.group(1));
}
System.out.println("---");
// Незахватывающая группа — group(1) не создаётся
Pattern nonCapturing = Pattern.compile("(?:cat|dog)s?");
Matcher m2 = nonCapturing.matcher(text);
while (m2.find()) {
System.out.println("Незахватывающая: совпадение=" + m2.group(0) + ", groupCount=" + m2.groupCount());
}
}
}
Вложенные группы
Группы могут быть вложенными друг в друга. Нумерация идёт по порядку открывающих скобок:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class NestedGroups {
public static void main(String[] args) {
String text = "2023-12-25";
Pattern pattern = Pattern.compile("((\\d{4})-(\\d{2})-(\\d{2}))");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("group(0) - всё: " + matcher.group(0)); // 2023-12-25
System.out.println("group(1) - вся дата: " + matcher.group(1)); // 2023-12-25
System.out.println("group(2) - год: " + matcher.group(2)); // 2023
System.out.println("group(3) - месяц: " + matcher.group(3)); // 12
System.out.println("group(4) - день: " + matcher.group(4)); // 25
}
}
}
Важно: Если совпадение не найдено (например, опциональная группа не сработала), то matcher.group(n) вернёт null, а не пустую строку. Всегда проверяйте на null, если группа не обязательная!
Методы String, работающие с regex
Помимо классов Pattern и Matcher, в Java есть методы класса String, которые принимают регулярные выражения. Они удобны для простых операций, когда вам не нужна полная мощь Pattern/Matcher:
String.matches() — полное соответствие
Этот метод проверяет, соответствует ли ВСЯ строка шаблону. Он создаёт временный Pattern и Matcher под капотом:
public class StringMatches {
public static void main(String[] args) {
String phone = "+7(999)123-45-67";
// Проверяем формат телефона
boolean valid = phone.matches("\\+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}");
System.out.println("Номер валиден: " + valid);
// Простые проверки
String number = "12345";
System.out.println(number.matches("\\d+")); // true
System.out.println(number.matches("[a-z]+")); // false
System.out.println(number.matches("[a-zA-Z0-9]+")); // true
}
}
String.split() — разбиение строки
Метод split() разбивает строку по шаблону, как нож разрезает батон на кусочки:
public class StringSplit {
public static void main(String[] args) {
String csv = "яблоко,банан, вишня, дыня";
// Разбиваем по запятой с возможными пробелами
String[] fruits = csv.split(",\\s*");
for (String fruit : fruits) {
System.out.println("Фрукт: «" + fruit + "»");
}
System.out.println("---");
// Разбиваем строку с несколькими разделителями
String mixed = "один:два;три|четыре";
String[] parts = mixed.split("[:;|]");
for (String part : parts) {
System.out.println("Часть: «" + part + "»");
}
System.out.println("---");
// Разбиваем по цифрам
String withNumbers = "abc123def456ghi";
String[] letters = withNumbers.split("\\d+");
for (String letter : letters) {
System.out.println("Буквы: «" + letter + "»");
}
}
}
String.replaceAll() — замена по шаблону
Этот метод заменяет все вхождения шаблона на указанную строку:
public class StringReplaceAll {
public static void main(String[] args) {
String text = "Привет мир! Как дела?";
// Заменяем несколько пробелов на один
String cleaned = text.replaceAll("\\s+", " ");
System.out.println("До: «" + text + "»");
System.out.println("После: «" + cleaned + "»");
System.out.println("---");
// Заменяем все буквы на звёздочки
String secret = "Hello World".replaceAll("[a-zA-Z]", "*");
System.out.println("Секрет: " + secret);
System.out.println("---");
// Используем группы в замене ($1, $2 и т.д.)
String name = "Иванов Иван Иванович";
String formatted = name.replaceAll("(\\w+)\\s(\\w)\\w+\\s(\\w)\\w+", "$1 $2. $3.");
System.out.println("Форматированное: " + formatted);
}
}
Обратите внимание на последний пример: $1, $2, $3 ссылаются на группы захвата. Это позволяет «перекомбинировать» текст по-новому!
Совет: Если вам нужно использовать regex многократно с одной и той же строкой, лучше скомпилировать Pattern заранее, а не использовать методы String. Методы String каждый раз создают новый Pattern, что менее эффективно. Но для одноразовых операций методы String удобнее.
Практический пример: валидация email
Давайте создадим полноценную проверку email-адреса. Email имеет формат: local@domain.extension. Разберём каждый компонент:
Анализ структуры email
- Локальная часть (до @): может содержать буквы, цифры, точки, подчёркивания, дефисы, плюсы. Длина от 1 до 64 символов.
- Символ @: ровно один.
- Домен (после @): буквы, цифры, дефисы. Длина от 1 до 253 символов.
- Точка: ровно одна перед расширением.
- Расширение: буквы, длина от 2 до 63 символов.
Создаём regex для email
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class EmailValidator {
// Паттерн для проверки email
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9]" + // Первая буква/цифра
"[a-zA-Z0-9._%+-]*" + // Остальные символы локальной части
"@" + // Символ @
"[a-zA-Z0-9]" + // Первая буква/цифра домена
"[a-zA-Z0-9.-]*" + // Остальные символы домена
"\\." + // Точка перед расширением
"[a-zA-Z]{2,63}$" // Расширение (2-63 буквы)
);
public static boolean isValidEmail(String email) {
if (email == null || email.isEmpty()) {
return false;
}
return EMAIL_PATTERN.matcher(email).matches();
}
public static void main(String[] args) {
// Тестовые email-адреса
String[] emails = {
"user@example.com", // валидный
"user.name@example.com", // валидный (точка в локальной части)
"user+tag@example.com", // валидный (плюс в локальной части)
"user@sub.example.com", // валидный (поддомен)
"@example.com", // невалидный (нет локальной части)
"user@", // невалидный (нет домена)
"user@example", // невалидный (нет расширения)
"user@example.c", // невалидный (расширение 1 буква)
"user@@example.com", // невалидный (двойной @)
"user name@example.com", // невалидный (пробел)
"user@exam ple.com", // невалидный (пробел в домене)
};
for (String email : emails) {
System.out.println(email + " → " + (isValidEmail(email) ? "✓" : "✗"));
}
}
}
Разбор regex пошагово
Давайте разберём каждый компонент нашего regex:
^[a-zA-Z0-9] — строка должна НАЧИНАТЬСЯ с буквы или цифры
[a-zA-Z0-9._%+-]* — за ней могут идти буквы, цифры, точки, подчёркивания,
проценты, плюсы, дефисы (ноль или более)
@ — буквально символ @
[a-zA-Z0-9] — домен должен НАЧИНАТЬСЯ с буквы или цифры
[a-zA-Z0-9.-]* — за ней могут идти буквы, цифры, точки, дефисы
\\. — буквальная точка (экранирована!)
[a-zA-Z]{2,63} — расширение: от 2 до 63 букв
$ — строка должна ЗАКАНЧИВАТЬСЯ здесь
Почему мы НЕ используем сложный regex? Существуют regex для email длиной в 10+ строк, которые пытаются покрыть ВСЕ стандарты. Но на практике лучше использовать простой regex + дополнительную проверку (например, попробовать отправить письмо). Слишком сложные regex могут быть неточными и медленными.
Извлечение компонентов email
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class EmailParser {
private static final Pattern EMAIL_PARSE = Pattern.compile(
"^([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+)\\.([a-zA-Z]{2,63})$"
);
public static void parseEmail(String email) {
Matcher matcher = EMAIL_PARSE.matcher(email);
if (matcher.matches()) {
String localPart = matcher.group(1);
String domain = matcher.group(2);
String extension = matcher.group(3);
System.out.println("Email: " + email);
System.out.println(" Локальная часть: " + localPart);
System.out.println(" Домен: " + domain);
System.out.println(" Расширение: " + extension);
System.out.println();
} else {
System.out.println("Невалидный email: " + email);
}
}
public static void main(String[] args) {
parseEmail("user@example.com");
parseEmail("ivan.petrov@mail.ru");
parseEmail("support@google.com");
parseEmail("invalid-email");
}
}
Практический пример: валидация телефонных номеров
Телефонные номера записываются по-разному: +7(999)123-45-67, 8 999 1234567, +7-999-123-45-67, 8(999)1234567. Наша задача — распознать все эти форматы. Это отличная демонстрация того, как regex справляется с реальными задачами.
Анализ форматов телефонов
Российские номера обычно начинаются с +7 или 8, затем 10 цифр. Разделители могут быть любыми: пробелы, дефисы, скобки.
Создаём regex для телефона
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class PhoneValidator {
// Паттерн для российских номеров
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^" + // начало строки
"(\\+7|8)" + // код страны: +7 или 8
"[\\s\\-\\(]*" + // возможные разделители
"(\\d{3})" + // код города/оператора (3 цифры)
"[\\s\\-\\()]*" + // возможные разделители
"(\\d{3})" + // первые 3 цифры номера
"[\\s\\-]*" + // возможные разделители
"(\\d{2})" + // следующие 2 цифры
"[\\s\\-]*" + // возможные разделители
"(\\d{2})" + // последние 2 цифры
"$" // конец строки
);
public static boolean isValidPhone(String phone) {
return PHONE_PATTERN.matcher(phone).matches();
}
public static void parsePhone(String phone) {
Matcher matcher = PHONE_PATTERN.matcher(phone);
if (matcher.matches()) {
String countryCode = matcher.group(1);
String regionCode = matcher.group(2);
String part1 = matcher.group(3);
String part2 = matcher.group(4);
String part3 = matcher.group(5);
String formatted = String.format(
"%s (%s) %s-%s-%s",
countryCode, regionCode, part1, part2, part3
);
System.out.println("Исходный: " + phone);
System.out.println("Форматированный: " + formatted);
System.out.println();
} else {
System.out.println("Невалидный номер: " + phone);
}
}
public static void main(String[] args) {
String[] phones = {
"+7(999)123-45-67", // валидный
"8 999 123 45 67", // валидный
"+7-999-123-45-67", // валидный
"8(999)1234567", // валидный
"+79991234567", // валидный (без разделителей)
"7-999-123-45-67", // невалидный (нет +)
"+7(99)123-45-67", // невалидный (2 цифры в коде)
"+7(999)12-45-67", // невалидный (2 цифры в части)
};
for (String phone : phones) {
System.out.println("Телефон: «" + phone + "» → " +
(isValidPhone(phone) ? "валиден" : "невалиден"));
}
System.out.println("\n--- Форматирование ---\n");
for (String phone : phones) {
if (isValidPhone(phone)) {
parsePhone(phone);
}
}
}
}
Извлечение всех телефонов из текста
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class PhoneExtractor {
private static final Pattern PHONE_EXTRACT = Pattern.compile(
"(?:\\+7|8)[\\s\\-\\(]*\\d{3}[\\s\\-\\()]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}"
);
public static void main(String[] args) {
String text = "Свяжитесь с нами: +7(495)123-45-67 или 8-800-555-35-35. " +
"Также доступен номер 8(916)987-65-43.";
Matcher matcher = PHONE_EXTRACT.matcher(text);
System.out.println("Найденные телефоны:");
while (matcher.find()) {
System.out.println(" " + matcher.group() +
" (позиция " + matcher.start() + "-" + matcher.end() + ")");
}
}
}
Дополнительные полезные конструкции
Опережающая (lookahead) и отстающая (lookbehind) проверки
Это «проверки условия» — они проверяют, что после/перед совпадением идёт (или не идёт) определённая последовательность, но НЕ включают её в результат:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class LookaroundExample {
public static void main(String[] args) {
String text = "Цена: 100$ или 200€ или 300₽";
// Positive lookahead: найти числа, за которыми СЛЕДУЕТ "$"
Pattern dollars = Pattern.compile("\\d+(?=\\$)");
Matcher m1 = dollars.matcher(text);
while (m1.find()) {
System.out.println("В долларах: " + m1.group());
}
System.out.println("---");
// Negative lookahead: найти числа, за которыми НЕ СЛЕДУЕТ "€"
Pattern notEuros = Pattern.compile("\\d+(?!€)");
Matcher m2 = notEuros.matcher(text);
while (m2.find()) {
System.out.println("Не в евро: " + m2.group());
}
System.out.println("---");
// Positive lookbehind: найти числа, перед КОТОРЫМИ стоит "€"
Pattern afterEuro = Pattern.compile("(?<=€)\\d+");
Matcher m3 = afterEuro.matcher(text);
while (m3.find()) {
System.out.println("После евро: " + m3.group());
}
}
}
Оператор альтернации (|)
Вертикальная черта | означает «ИЛИ». Совпадёт левая ИЛИ правая часть:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class AlternationExample {
public static void main(String[] args) {
String text = "Java, JavaScript, Python, C++, C#";
// Найти "Java" или "C++" или "C#"
Pattern pattern = Pattern.compile("Java|C\\+\\+|C#");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Найден язык: " + matcher.group());
}
System.out.println("---");
// Скобки для группировки альтернации
Pattern pattern2 = Pattern.compile("(Java|C)(Script|\\+\\+|#)");
Matcher matcher2 = pattern.matcher(text);
while (matcher2.find()) {
System.out.println("Язык: " + matcher2.group(1) + ", модификация: " + matcher2.group(2));
}
}
}
Флаги встраивания
Вместо того чтобы передавать флаги в Pattern.compile(), можно встроить их прямо в regex:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class InlineFlags {
public static void main(String[] args) {
// (?i) — case insensitive
Pattern pattern1 = Pattern.compile("(?i)привет");
System.out.println(pattern1.matcher("ПРИВЕТ").matches()); // true
// (?m) — multiline
String text = "Строка 1\nСтрока 2";
Pattern pattern2 = Pattern.compile("(?m)^Строка 2$");
System.out.println(pattern2.matcher(text).find()); // true
// (?s) — dotall
String text2 = "Привет\nмир";
Pattern pattern3 = Pattern.compile("(?s)Привет.*мир");
System.out.println(pattern3.matcher(text2).matches()); // true
}
}
Итоги урока
- ✓ Регулярные выражения — это шаблоны для поиска и проверки текста, гораздо мощнее простого поиска по строке
- ✓ Класс
Patternкомпилирует regex, аMatcherприменяет его к строкам - ✓ Метод
find()ищет подстроки,matches()проверяет всю строку - ✓ Основные конструкции:
.(любой символ),[](набор символов),^/$(начало/конец строки) - ✓ Классы символов:
\d(цифры),\w(буквы+цифры),\s(пробелы) — и их инверсии\D,\W,\S - ✓ Квантификаторы:
*(ноль+),+(один+),?(ноль/один),{n,m}(от n до m). Добавьте?для ленивого варианта - ✓ Группы
()захватывают части совпадения для дальнейшего использования - ✓ Методы String
matches(),split(),replaceAll()удобны для простых операций - ✓ В реальных проектах regex используют для валидации данных, поиска информации, замены текста и парсинга
📖 Следующий урок: Мы продолжим изучение регулярных выражений и разберём более сложные паттерны, оптимизацию производительности и работу с Unicode. А также рассмотрим реальные кейсы использования regex вEnterprise-приложениях.
Тест: Регулярные выражения
10 вопросов