Регулярные выражения

Позволяют обрабатывать строки, находить в них нужные части, выделять их или заменять на другие строки.

Вообще, регулярное выражение описывает множество строк.

Примеры:

  1. abc: "abc"
  2. xyz 123: "xyz 123"
  3. abc|xyz|pqr: "abc", "xyz", "pqr"

Проверить, что строка принадлежит множеству, описываемому регулярным выражением, можно с помощью функции matches:

СТРОКА.matches(РЕГУЛЯРНОЕ ВЫРАЖЕНИЕ)

Это истина, если строка принадлежит множеству.

Продолжаем примеры:

  1. a(x|y|z)b: "axb", "ayb", "azb"
  2. (a|b|c)(a|b|c)(a|b|c): "aaa", "bcc", "abc",…
  3. (a|b|c|)(a|b|c|xy)(a|b|c): "aaa", "xyc", "bb", …
  4. прыгающ(ий|его|ему|им|ем|ими): "прыгающими", "прыгающий", …
  5. a*: "", "a", "aa", "aaa", …
  6. (ab)*: "", "ab", "abab", "ababab", …
  7. ab*: "a", "ab", "abb", "abbb", …
  8. (a|bc*)z: "az", "bz", "bcz", "bccz", "bcccz"
  9. (a|b)*: "", "a", "b", "aa", "bb", "ababaa"
  10. \d: эквивалентно 0|1|2|3|4|5|6|7|8|9
  11. \d\d:\d\d - время, но может быть "33:12"
  12. (0|1)\d:(0|1|2|3|4|5)\d - время без 20, 21, 22, 23 часов
  13. ((0|1)\d|20|21|22|23):(0|1|2|3|4|5)\d

Посмотрим сайт regexr.com

Пример 16 и 15 — примеры того, как лучше не делать. Регулярные выражения должны описывать «синтаксис» строки, а не «семантику». Если нужно искать время в тексте, лучше найти строки из примера 13, т.е. две цифры, двоеточие, две цифры, а потом отдельно проверить, что они образуют время, т.е. кол-во часов до 24, количество минут до 60. Иначе регулярные выражения становятся очень сложными.

Смотрим возможности регулярных выражений дальше.

  1. В квадратных скобках можно писать выбор одного символа из нескольких: [0123456789] — эквивалентно (0|1|2|3|4|5|6|7|8|9) или \d.
  2. [ABC][abc] — это слова "Aa", "Ab", "Bb"
  3. [ABC][abc]* — это слова "Aabcccbacbacba", "A", "Bbbba"
  4. [A-Z][a-z]* — это слова, начинающиеся с заглавной буквы. Допустимы буквы из диапазона от ‘a’ до ‘z’.
  5. [A-Za-z.,!\d] — любой символ латинская буква, точка, запятая, восклицательный знак или цифра.
  6. [A-Za-z]* — это последовательность из латинских букв.
  7. cats? или cat(s)? — буква s либо берется, либо не берется: "cat", "cats".
  8. ab+ — повторяется хотя бы 1 раз: "ab", "abb", "abbb", …
  9. ab{2,4} —— “abb”, “abbb”, “abbbb”`.
  10. Точка — любой символ: .* — под это подходит вообще любая строка.
  11. 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()); // индекс конца
}

Напоминание

  1. Регулярные выражения — это строка, которая описывает множество строк. Например, [a-z]+\d описывает строки типа "xy5", "hfiuen9", "x8", … (несколько латинских букв и одна цифра).
  2. Внутри Java программы регулярные выражения записать может быть непросто. Нужно экранировать все символы \. Прошлый пример будет записан так: Pattern.compile("[a-z]+\\d").
  3. Функции по работе с регулярными выражениями:
    1. "строка".matches(регулярное выражение)
    2. "строка.replaceAll(...) — см. далее
    3. Общий метод такой:
      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()); //читаем это как число