Търсене и замяна
Проблем
Извлечението от банката
Банката ти праща текст с плащанията за месеца. Искаш всяка сума поотделно, за да ги събереш:
String statement = "Наем 450,00 € на 01.10, ток 87,35 € на 05.10, вода 23,10 € на 12.10";System.out.println(statement.matches(".*\\d+,\\d{2} €.*"));truematches казва само „да, има сума някъде“. .* от двете страни е там, защото 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 до 5ERROR от 24 до 29Общо: 2start() е индексът на първия символ на съвпадението, а 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();\d+ е „една или повече цифри“, затова „12“ е едно съвпадение. Със \d без + всяка цифра би била отделно: [1][2][4][2]. Интервалът и запетаята прекъсват поредицата, затова 12 и 4 не се сливат.
Идеята
Групи: части от съвпадението
Скобите в шаблона правят група. 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.202615 / 11 / 20262026-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));}Стъпка по стъпка, с думи
- Шаблонът за дата има три двойки скоби: три групи.
- m е търсачка за този шаблон в този текст.
- find() намира датата: от индекс 7 до 17.
- Всяка група помни своята част: група 1 е 15, група 2 е 11, група 3 е 2026.
- Частите стоят в m, всяка под своя номер. group(0) е цялото съвпадение.
- group() е същото като group(0): 15.11.2026.
- Трите групи поотделно.
- Наредени в обратен ред, частите дават 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/2026Pattern и 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. На кой ред е грешката?
Избери реда с грешката.
if вика find() веднъж и намира само първото съвпадение. За всички трябва while (m.find()): всяко извикване продължава оттам, където е спряло предното.Идеята
Флагове и редове
Флаговете се дават като втори аргумент на 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. Намери и замени. Замени думата „старо“ с „ново“ навсякъде, където е цяла дума, и кажи колко замени си направил. Части от други думи („старомодно“) не се пипат.
Това е старо съдържание. Още старо съдържание, не старомодно.Това е ново съдържание. Още ново съдържание, не старомодно.Замени: 22. Думи с префикс. На първия ред е префиксът, на втория текстът. Отпечатай всяка дума, която започва с префикса.
appapple application banana appreciate snappleappleapplicationappreciate3. Адреси в текст. Извади всички уеб адреси, които започват с http:// или https://. Точката в края на изречението не е част от адреса.
Виж сайта https://www.example.com и този: http://www.test.com. Документите са на https://docs.example.com/guide/start.https://www.example.comhttp://www.test.comhttps://docs.example.com/guide/start4. Дати към 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.
Тест