Към съдържанието
wannacode

Търсене и замяна

Проблем

Извлечението от банката

Банката ти праща текст с плащанията за месеца. Искаш всяка сума поотделно, за да ги събереш:

String statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10";
System.out.println(statement.matches(".*\\d+,\\d{2} €.*"));
Изход
true
matches казва само „да, има сума някъде“. .* от двете страни е там, защото matches иска шаблонът да покрие целия текст. replaceAll би сменил сумите, но не ти ги дава. Трябва ти начин да намериш всяко съвпадение и да го вземеш.

Идеята

Pattern и Matcher

За търсене има два класа от java.util.regex:

String statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10";
Pattern amount = Pattern.compile("\\d+,\\d{2} €");
Matcher m = amount.matcher(statement);
while (m.find()) {
System.out.println(m.group());
}
Изход
450,00 €
87,35 €
23,10 €
  • Pattern.compile(...) превръща шаблона в обект Pattern. Прави го веднъж и го ползвай колкото пъти искаш.
  • amount.matcher(text) дава Matcher: търсачка за този шаблон в този текст.
  • m.find() намира следващото съвпадение. Връща true, ако има такова, и false, когато свършат. Затова е в while.
  • m.group() е текстът на току-що намереното съвпадение.

Във файла горе трябват import java.util.regex.Matcher; и import java.util.regex.Pattern;.

По-точно, за любопитните
  • String.matches, replaceAll и split всъщност правят Pattern.compile всеки път. В цикъл с хиляди редове е по-бързо шаблонът да е компилиран веднъж, например в static final Pattern поле.
  • m.matches() проверява целия текст, като String.matches. m.lookingAt() проверява само дали текстът започва със съвпадение.

Виж го

Къде е съвпадението

Освен текста, Matcher знае и къде е намерено:

String log = "ERROR диск; INFO старт; ERROR мрежа";
Matcher m = Pattern.compile("ERROR").matcher(log);
int count = 0;
while (m.find()) {
count++;
System.out.println(m.group() + " от " + m.start() + " до " + m.end());
}
System.out.println("Общо: " + count);
Изход
ERROR от 0 до 5
ERROR от 24 до 29
Общо: 2
start() е индексът на първия символ на съвпадението, а end() е индексът след последния, както при substring. Затова log.substring(m.start(), m.end()) е същото като m.group().

Пробвай

Цифра или число?

Какво ще отпечата програмата?

Matcher m = Pattern.compile("\\d+").matcher("Маса 12, 4 души, 2 бутилки");
while (m.find()) {
System.out.print("[" + m.group() + "]");
}
System.out.println();
Отговор

Идеята

Групи: части от съвпадението

Скобите в шаблона правят група. Matcher помни какво е съвпаднало с всяка група поотделно:

Pattern date = Pattern.compile("(\\d{2})\\.(\\d{2})\\.(\\d{4})");
Matcher m = date.matcher("Падеж: 15.11.2026");
if (m.find()) {
System.out.println(m.group());
System.out.println(m.group(1) + " / " + m.group(2) + " / " + m.group(3));
System.out.println(m.group(3) + "-" + m.group(2) + "-" + m.group(1));
}
Изход
15.11.2026
15 / 11 / 2026
2026-11-15
  • Групите се номерират по отварящите скоби, отляво надясно, от 1.
  • group(0) или group() е цялото съвпадение.
  • if (m.find()) е за случаите, когато ти трябва само първото съвпадение.

Така от една дата вземаш поотделно деня, месеца и годината и ги нареждаш както ти трябва.

Pattern date = Pattern.compile("(\\d{2})\\.(\\d{2})\\.(\\d{4})");
Matcher m = date.matcher("Падеж: 15.11.2026");
if (m.find()) {
System.out.println(m.group());
System.out.println(m.group(1) + " / " + m.group(2) + " / " + m.group(3));
System.out.println(m.group(3) + "-" + m.group(2) + "-" + m.group(1));
}
Стъпка по стъпка, с думи
  1. Шаблонът за дата има три двойки скоби: три групи.
  2. m е търсачка за този шаблон в този текст.
  3. find() намира датата: от индекс 7 до 17.
  4. Всяка група помни своята част: група 1 е 15, група 2 е 11, група 3 е 2026.
  5. Частите стоят в m, всяка под своя номер. group(0) е цялото съвпадение.
  6. group() е същото като group(0): 15.11.2026.
  7. Трите групи поотделно.
  8. Наредени в обратен ред, частите дават 2026-11-15.
По-точно, за любопитните
  • (?:...) е група, която само държи частите заедно, без да ги помни. Например (?:\+359|0)8[789]\d{7} не прави група 1.
  • m.groupCount() дава броя на групите в шаблона.
  • Ако група не е участвала в съвпадението (например (,\d{2})? я е нямало), m.group(1) е null.

Идеята

Групи с имена и замяна

Когато групите са много, номерата се бъркат. Можеш да ги кръстиш: (?<име>...), а после m.group("име"). Ред от сметка в кафене:
Pattern item = Pattern.compile("(?<qty>\\d+) x (?<name>[А-Яа-я ]+) по (?<price>\\d+\\.\\d{2})");
Matcher m = item.matcher("2 x Кафе с мляко по 3.20; 1 x Кроасан по 2.90");
while (m.find()) {
int qty = Integer.parseInt(m.group("qty"));
double price = Double.parseDouble(m.group("price"));
System.out.println(m.group("name") + ": " + qty * price + " €");
}
Изход
Кафе с мляко: 6.4 €
Кроасан: 2.9 €
Групите работят и в replaceAll: в текста за замяна $1 е група 1, а ${име} е групата с това име. Датите от български към ISO вид и към американски:
String text = "Срок: 15.11.2026, напомняне: 08.11.2026";
System.out.println(text.replaceAll("(\\d{2})\\.(\\d{2})\\.(\\d{4})", "$3-$2-$1"));
System.out.println(text.replaceAll("(?<d>\\d{2})\\.(?<m>\\d{2})\\.(?<y>\\d{4})", "${m}/${d}/${y}"));
Изход
Срок: 2026-11-15, напомняне: 2026-11-08
Срок: 11/15/2026, напомняне: 11/08/2026
Тук не ти трябват Pattern и Matcher: String.replaceAll разбира групите сам.
По-точно, за любопитните
  • $ в текста за замяна е специален. Ако искаш истински знак за долар, пиши \\$, или подай текста през Matcher.quoteReplacement(...), ако идва от потребителя.
  • Групата може да се използва и в самия шаблон: \1 значи „същото като група 1“. Така се хващат повторени думи. Виж картата за флаговете.

Пробвай

Колко взима +?

Искаш всички HTML етикети от реда:

Какво ще отпечата програмата?

String html = "<b>Цена:</b> <i>12 €</i>";
Matcher m = Pattern.compile("<.+>").matcher(html);
while (m.find()) {
System.out.println(m.group());
}
Отговор

Идеята

Жадно и нежадно

? след квантификатор го прави нежаден: той взима колкото може по-малко и спира при първата възможност.

String html = "<b>Цена:</b> <i>12 €</i>";
Matcher m = Pattern.compile("<.+?>").matcher(html);
while (m.find()) {
System.out.println(m.group());
}
System.out.println(html.replaceAll("<[^>]+>", ""));
Изход
<b>
</b>
<i>
</i>
Цена: 12 €
  • <.+?> е „<, най-малко един символ и първото > след него“.
  • <[^>]+> прави същото по друг начин: „<, символи, които не са >, и >“. Той не може да прескочи >, затова е и по-бърз. С него махаш етикетите и остава само текстът.
Жаден Нежаден
* *?
+ +?
? ??
{2,5} {2,5}?

Пробвай

Намери грешката

Програмата трябва да отпечата трите номера на поръчки, а отпечатва само #1042. На кой ред е грешката?

Избери реда с грешката.

Идеята

Флагове и редове

Флаговете се дават като втори аргумент на Pattern.compile, събрани с |. От лог файл с няколко реда вземаш съобщенията за грешки, написани с главни или малки букви:

String log = "INFO: Старт\nERROR: Няма връзка с базата\nINFO: Повторен опит\nerror: Диск 95%";
Pattern p = Pattern.compile("^error: (.+)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher(log);
while (m.find()) {
System.out.println(m.group(1));
}
Изход
Няма връзка с базата
Диск 95%
  • Pattern.CASE_INSENSITIVE е същото като (?i): главните и малките букви са еднакви.
  • Pattern.MULTILINE прави ^ и $ начало и край на всеки ред. Без него те са само началото и краят на целия текст и шаблонът няма да намери нищо.
  • Pattern.DOTALL позволява на . да хваща и нов ред.
По-точно, за любопитните

Две неща, които ще срещнеш в чужди шаблони:

  • Обратна препратка (backreference) \1: „същият текст като група 1“. Така се хващат повторени думи.
  • Поглед напред (lookahead) (?=...): „оттук нататък някъде има ...“, без да взима символи. Няколко такива един след друг са удобни за пароли: „има цифра“ и „има главна буква“, и поне 8 символа.
Pattern twice = Pattern.compile("(?U)\\b(\\w+) \\1\\b");
Matcher m = twice.matcher("Плащането е е прието, моля моля проверете.");
while (m.find()) {
System.out.println("Повторено: " + m.group(1));
}
String strong = "(?=.*\\d)(?=.*[A-Z]).{8,}";
System.out.println("parola12".matches(strong) + " " + "Parola12".matches(strong));
Изход
Повторено: е
Повторено: моля
false true

(?!...) е обратното: „оттук нататък няма ...“. Има и поглед назад: (?<=...) и (?<!...).

Упражнение

Намери, извлечи, замени

Реши задачите в редактора си. Текстът е на един ред от клавиатурата. В урока „Файлове“ ще ги направиш и с файл.

1. Намери и замени. Замени думата „старо“ с „ново“ навсякъде, където е цяла дума, и кажи колко замени си направил. Части от други думи („старомодно“) не се пипат.

Вход
Това е старо съдържание. Още старо съдържание, не старомодно.
Изход
Това е ново съдържание. Още ново съдържание, не старомодно.
Замени: 2

2. Думи с префикс. На първия ред е префиксът, на втория текстът. Отпечатай всяка дума, която започва с префикса.

Вход
app
apple application banana appreciate snapple
Изход
apple
application
appreciate

3. Адреси в текст. Извади всички уеб адреси, които започват с http:// или https://. Точката в края на изречението не е част от адреса.

Вход
Виж сайта https://www.example.com и този: http://www.test.com. Документите са на https://docs.example.com/guide/start.
Изход
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Дати към ISO. Превърни всяка дата дд.мм.гггг в текста във вида гггг-мм-дд с един replaceAll.

Вход
Договор от 03.10.2026, плащане до 15.10.2026.
Изход
Договор от 2026-10-03, плащане до 2026-10-15.

Още задачи

  • Сбор от извлечението. Вземи сумите от първата карта с група за еврото и група за центовете, събери ги и отпечатай „Общо: 560,45 €“.
  • Хаштагове. Извади всички думи, които започват с #, от публикация на кирилица и латиница. Помисли защо ти трябва (?U).
  • Без етикети. Махни HTML етикетите от <p>Срокът е <b>15.11</b>.</p> и отпечатай чистия текст.
  • Телефони в текст. Намери всички мобилни номера в текст и ги отпечатай във вида +359 88 812 3456 с групи и replaceAll.

Тест

Провери се

1. Какво прави m.find()?
2. Шаблонът е (\d{2})\.(\d{2})\.(\d{4}), а текстът е 15.11.2026. Какво е group(2)?
3. Какво дава "05.03.2026".replaceAll("(\\d+)\\.(\\d+)\\.(\\d+)", "$2/$1")?
4. Защо "<.+>" хваща целия текст "<b>Цена</b>", а не само "<b>"?
5. Какво прави Pattern.MULTILINE?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·