Регулярные выражения
Позволяют обрабатывать строки, находить в них нужные части, выделять их или заменять на другие строки.
Вообще, регулярное выражение описывает множество строк.
Примеры:
abc
:"abc"
xyz 123
:"xyz 123"
abc|xyz|pqr
:"abc"
,"xyz"
,"pqr"
Проверить, что строка принадлежит множеству, описываемому регулярным выражением, можно с помощью функции matches
:
СТРОКА.matches(РЕГУЛЯРНОЕ ВЫРАЖЕНИЕ)
Это истина, если строка принадлежит множеству.
Продолжаем примеры:
a(x|y|z)b
:"axb"
,"ayb"
,"azb"
(a|b|c)(a|b|c)(a|b|c)
:"aaa"
,"bcc"
,"abc"
,…(a|b|c|)(a|b|c|xy)(a|b|c)
:"aaa"
,"xyc"
,"bb"
, …прыгающ(ий|его|ему|им|ем|ими)
:"прыгающими"
,"прыгающий"
, …a*
:""
,"a"
,"aa"
,"aaa"
, …(ab)*
:""
,"ab"
,"abab"
,"ababab"
, …ab*
:"a"
,"ab"
,"abb"
,"abbb"
, …(a|bc*)z
:"az"
,"bz"
,"bcz"
,"bccz"
,"bcccz"
(a|b)*
:""
,"a"
,"b"
,"aa"
,"bb"
,"ababaa"
\d
: эквивалентно0|1|2|3|4|5|6|7|8|9
\d\d:\d\d
- время, но может быть"33:12"
(0|1)\d:(0|1|2|3|4|5)\d
- время без 20, 21, 22, 23 часов((0|1)\d|20|21|22|23):(0|1|2|3|4|5)\d
Посмотрим сайт regexr.com
Пример 16 и 15 — примеры того, как лучше не делать. Регулярные выражения должны описывать «синтаксис» строки, а не «семантику». Если нужно искать время в тексте, лучше найти строки из примера 13, т.е. две цифры, двоеточие, две цифры, а потом отдельно проверить, что они образуют время, т.е. кол-во часов до 24, количество минут до 60. Иначе регулярные выражения становятся очень сложными.
Смотрим возможности регулярных выражений дальше.
- В квадратных скобках можно писать выбор одного символа из нескольких:
[0123456789]
— эквивалентно(0|1|2|3|4|5|6|7|8|9)
или\d
. [ABC][abc]
— это слова"Aa"
,"Ab"
,"Bb"
[ABC][abc]*
— это слова"Aabcccbacbacba"
,"A"
,"Bbbba"
[A-Z][a-z]*
— это слова, начинающиеся с заглавной буквы. Допустимы буквы из диапазона от ‘a’ до ‘z’.[A-Za-z.,!\d]
— любой символ латинская буква, точка, запятая, восклицательный знак или цифра.[A-Za-z]*
— это последовательность из латинских букв.cats?
илиcat(s)?
— буква s либо берется, либо не берется:"cat"
,"cats"
.ab+
— повторяется хотя бы 1 раз:"ab"
,"abb"
,"abbb"
, …ab{2,4}
—— “abb”,
“abbb”,
“abbbb”`.- Точка — любой символ:
.*
— под это подходит вообще любая строка. abc.xyz
—"abc#xyz"
,"abc xyz"
,"abc1xyz"
,"abcRxyz"
,"abc,xyz"
,"abcaxyz"
.
Все возможности регулярных выражений: Класс Pattern, Java Docs 17.
Как же пользоваться регулярными выражениями в Java
Как записать регулярное выражение в виде строки
Пусть у нас есть регулярное выражение \d{4}
, т.е.
строки, состоящие из 4ёх цифр. Если записать его в Java напрямую, будет странно: "\d{4}"
. Java выдаст ошибку, потому что она не знает, что такое \d
. \n
— это перевод строки, а \d
??
Но мы знаем, что если нам нужна строка \d{4}
, мы должны экранировать некоторые символы, в данном случае, \
. Поэтому в Java это регулярное выражение выглядит \\d{4}
.
В Python есть r-строки, там можно писать `r”\d{4}”. Вместо “\d{4}”.
Еще пример.
Выражение для путей на диске[A-Z]:\\[A-Za-z]+\\[A-Za-z]+\.txt
. (например, "C:\Windows\a.txt"
). Внутри регулярного выражения приходится экранировать обратные слэши, потому что иначе обратный слэш будет не обратным слэшом, а символом экранирования.
Чтобы записать это в виде Java String, нужно снова экранировать все обратные слэши: "[A-Z]:\\\\[A-Za-z]+\\\\[A-Za-z]+\\.txt"
.
Как проверить, что строка подходит под регулярное выражение.
Первый способ, метод matches
, см. выше.
Второй способ, мы можем создать переменную типа Pattern
, в значениях этого типа хранятся регулярные выражения.
Pattern p1 = Pattern.compile("выражение");
Pattern p2 = Pattern.compile("выражение", флаги);
Флаги управляют тем, как будет работать регулярное выражение, все возможные флаги перечислены далее: CASE_INSENSITIVE (игнорирование регистра), MULTILINE (см. ниже), DOTALL (точка соответствует любому символу, иначе точка означает всё кроме переводов строки), UNICODE_CASE (лучше указывать вместе с case_insensitive), CANON_EQ, UNIX_LINES, LITERAL, UNICODE_CHARACTER_CLASS and COMMENTS.
Про Multiline: ^
означает начало строки, $
означает конец строки. А без режима Multiline они означают начало ввода и конец ввода соответственно.
Чтобы создать регулярное выражение с нужным набором флагов, пишем, соединяя флаги через ‘+’.
Pattern p2 = Pattern.compile("(\d\d):(\d\d)", Pattern.MULTILINE + Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);
После создания регулярного выражения, его можно использовать для сопоставления с разными строками. Сопоставление со строками происходит не напрямую, а через объект Matcher
, он хранит информацию о процессе сопоставления регулярного выражения и строки. Создать этот объект можно следующим образом:
Pattern timePattern = Pattern.compile("\\d\\d:\\d\\d");
Matcher m = timePattern.matcher("23:16");
Чтобы проверить, сопоставляется ли строка с регулярным выражением, надо вызвать метод m.matches()
, вы получите логическое значение. В данном случае, это истина.
Объект после сопоставления содержит разную дополнительную информацию о том, как строка сопоставилась с выражением, но это мы обсудим позже.
Поиск подстрок, подходящих под регулярное выражение.
Допустим, мы имеем текст, котором встречаются подстроки, подходящие под регулярное выражение. Как их найти?
//Создаем matcher аналогично предыдущему
String text = "23:16 and 12:54 and 77:88, hello";
Pattern timePattern = Pattern.compile("\\d\\d:\\d\\d");
Matcher m = timePattern.matcher(text);
// Метод find() ищет очередное вхождение подстроки, подходящей под регулярное выражение
boolean found = m.find(); // вернет true, если нашел
System.out.println(found);
System.out.println(m.group()); // найденная подстрока
System.out.println(m.start()); // индекс начала
System.out.println(m.end()); // индекс конца
// чтобы найти следующий, делаем снова find
found = m.find(); // вернет true, если нашел еще один
System.out.println(found);
System.out.println(m.group()); // найденная подстрока
System.out.println(m.start()); // индекс начала
System.out.println(m.end()); // индекс конца
// обычно делают поиск в цикле
while (m.find()) {
System.out.println(m.group());
System.out.println(m.start()); // индекс начала
System.out.println(m.end()); // индекс конца
}
Напоминание
- Регулярные выражения — это строка, которая описывает множество строк. Например,
[a-z]+\d
описывает строки типа"xy5"
,"hfiuen9"
,"x8"
, … (несколько латинских букв и одна цифра). - Внутри Java программы регулярные выражения записать может быть непросто. Нужно экранировать все символы
\
. Прошлый пример будет записан так:Pattern.compile("[a-z]+\\d")
. - Функции по работе с регулярными выражениями:
"строка".matches(регулярное выражение)
"строка.replaceAll(...)
— см. далее- Общий метод такой:
Pattern regex = Pattern.compile("рег. выр"); Matcher m = regex.match("строка");
После этого все действия по сопоставлению строки с регулярным выражением производятся через Matcher.
Было
m.match()
сопоставить целиком,m.find()
найти подстроку, соответствующую выражению.
Группы
Каждая пара круглых скобок внутри регулярного выражения задаёт группу. Номер группы равен номеру первой круглой скобки. В выражении (\d\d):(\d\d)
есть две группы, у них номера 1 и 2. Каждый раз, когда регулярное выражение сопоставляется с каким-то текстом, каждая группа сопоставляется с кусочком этого текста.
В коде у Matcher можно спрашивать, с чем сопоставилась группа, например, m.group(1)
возвращает, с чем сопоставилась первая группа. Напомню, в прошлый раз мы использовали m.group(0)
, чтобы понять, с чем сопоставилось всё выражение. Т.е. 0-ая группа — это всё сопоставление.
//Проверим работу групп
Pattern wordNumber = Pattern.compile("([a-z]+)(\\d+)");
String text2 = """
Какой-то текст a23 со словами xyz42 с цифрами.
И еще таких слов немного pqr111.
""";
Matcher wordNumberMatcher = wordNumber.matcher(text2);
while (wordNumberMatcher.find()) {
System.out.println("Найдено " + wordNumberMatcher.group(0));
System.out.println("Буквы " + wordNumberMatcher.group(1));
System.out.println("Цифры " + wordNumberMatcher.group(2));
System.out.println();
}
Функция replaceAll
позволяет заменять текст в строке, который ищется с помощью регулярных выражений. При замене можно пользоваться найденными группами:
//заменим буквыцифры на символ точки
System.out.println("a23 xx bc42".replaceAll("([a-z]+)(\\d+)", "."));
//если в замене есть символ доллара, он указывает номер группы
System.out.println("a23 xx bc42".replaceAll("([a-z]+)(\\d+)", "$1"));
System.out.println("a23 xx bc42".replaceAll("([a-z]+)(\\d+)", "-$1-"));
System.out.println("a23 xx bc42".replaceAll("([a-z]+)(\\d+)", "$1$2$1"));
//0 группа = вся найденная подстрока
System.out.println("a23 xx bc42".replaceAll("([a-z]+)(\\d+)", "[$0]"));
Есть еще replaceFirst()
, который заменяет только первое найденное вхождение.
Более сложная замена найденных подстрок
Допустим, мы хотим заменить в найденных подстроках все буквы на верхний регистр. Т.е. заменить, например, в строке "abc111 xyz pqr222"
слова с цифрами на те же слова с цифрами, но в верхнем регистре. Должно получиться "ABC111 xyz PQR222"
.
Пример:
// Напомним
// Pattern wordNumber = Pattern.compile("([a-z]+)(\\d+)");
Matcher m4 = wordNumber.matcher("a23 xx bc42");
// нужно запомнить кусок кода:
StringBuilder sb = new StringBuilder(); //Аналог String, но изменяемый
while (m4.find()) {
// Выясняем, на что заменять
String letters = m4.group(1); // "a"
String digits = m4.group(2); // "23"
String replacement = letters.toUpperCase() + digits;
// даем команду на замену
m4.appendReplacement(sb, replacement);
}
m4.appendTail(sb);
String finalText = sb.toString();
System.out.println(finalText); // Преобразуем результат замен в строку
Класс Scanner
Мы использовали этот класс ранее, чтобы читать данные из файла. Им же можно читать данные из строки, причем можно настраивать, что Scanner считает разделителями.
Scanner in = new Scanner("""
Первое предложение.
Второе предложение! Еще 1 (одно), предложение, с запятыми.""");
System.out.println(in.next()); // Первое
System.out.println(in.next()); // предложении.
System.out.println(in.next()); // Второе
по-умолчанию Scanner считает разделителями любые пробельные символы. Но можно задать другой разделитесь
in.useDelimiter("[^а-яА-Я0-9]+"); //^ означает всё, кроме укзанных символов
System.out.println(in.next()); //предложение (без !)
System.out.println(in.next()); //Еще
System.out.println(in.hasNextInt()); //проверям, что дальше число
System.out.println(10 * in.nextInt()); //читаем это как число