Рубрики
Без рубрики

Как использовать регулярные выражения для замены токенов в строках в Java

Изучите некоторые стратегии регулярных выражений для замены токенов в строках

Автор оригинала: Ashley Frieze.

1. Обзор

Когда нам нужно найти или заменить значения в строке в Java, мы обычно используем регулярные выражения . Они позволяют нам определить, соответствует ли некоторая или вся строка шаблону. Мы могли бы легко применить одну и ту же замену к нескольким токенам в строке с помощью метода replaceAll | в обоих совпадениях и Строке .

В этом уроке мы рассмотрим, как применить другую замену для каждого токена, найденного в строке. Это позволит нам легко удовлетворить случаи использования, такие как экранирование определенных символов или замена значений заполнителей.

Мы также рассмотрим несколько приемов настройки наших регулярных выражений для правильной идентификации токенов.

2. Индивидуальная Обработка Совпадений

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

2.1. Пример титульного кейса

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

Ваш вклад может быть:

"First 3 Capital Words! then 10 TLAs, I Found"

Из определения заглавного слова это содержит совпадения:

  • Первый
  • Столица
  • Слова
  • I
  • Нашел

И регулярное выражение для распознавания этого шаблона будет:

"(?<=^|[^A-Za-z])([A-Z][a-z]*)(?=[^A-Za-z]|$)"

Чтобы понять это, давайте разберем его на составные части. Мы начнем с середины:

[A-Z]

распознает одну заглавную букву.

Мы допускаем односимвольные слова или слова, за которыми следуют строчные буквы, поэтому:

[a-z]*

распознает ноль или более строчных букв.

В некоторых случаях вышеперечисленных двух классов символов было бы достаточно, чтобы распознать наши токены. К сожалению, в нашем примере текста есть слово, которое начинается с нескольких заглавных букв. Поэтому нам нужно выразить, что единственная заглавная буква, которую мы находим, должна быть первой после не-букв.

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

Выражение [^A-Za-z] означает “без букв”. Мы поместили один из них в начале выражения в группу без захвата:

(?<=^|[^A-Za-z])

Группа без захвата, начинающаяся с (?<=, выполняет проверку, чтобы убедиться, что совпадение отображается на правильной границе. Его аналог в конце выполняет ту же работу для следующих символов.

Однако, если слова касаются самого начала или конца строки, то нам нужно учитывать это, и именно здесь мы добавили ^| в первую группу, чтобы это означало “начало строки или любые не буквенные символы”, и мы добавили |$ в конце последней группы без захвата, чтобы конец строки был границей.

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

Мы должны отметить, что даже в таком простом случае использования может быть много крайних случаев, поэтому важно проверить наши регулярные выражения . Для этого мы можем писать модульные тесты, использовать встроенные инструменты нашей IDE или использовать онлайн-инструмент, такой как Regexr .

2.2. Тестирование Нашего Примера

С нашим примером текста в константе с именем EXAMPLE_INPUT и нашим регулярным выражением в шаблоне с именем TITLE_CASE_PATTERN , давайте использовать find в классе Matcher для извлечения всех наших совпадений в модульном тесте:

Matcher matcher = TITLE_CASE_PATTERN.matcher(EXAMPLE_INPUT);
List matches = new ArrayList<>();
while (matcher.find()) {
    matches.add(matcher.group(1));
}

assertThat(matches)
  .containsExactly("First", "Capital", "Words", "I", "Found");

Здесь мы используем функцию matcher на Pattern для создания Matcher . Затем мы используем метод find в цикле, пока он не перестанет возвращать true , чтобы перебрать все совпадения.

Каждый раз, когда find возвращает true , состояние объекта Matcher устанавливается для представления текущего соответствия. Мы можем проверить все совпадение с помощью group(0) или проверить отдельные группы захвата с их индексом на основе 1 . В этом случае вокруг нужного фрагмента есть группа захвата, поэтому мы используем group(1) , чтобы добавить совпадение в наш список.

2.3. Проверка Соответствия немного больше

До сих пор нам удавалось находить слова, которые мы хотим обработать.

Однако, если бы каждое из этих слов было маркером, который мы хотели бы заменить, нам понадобилось бы больше информации о совпадении, чтобы построить результирующую строку. Давайте рассмотрим некоторые другие свойства Совпадений , которые могут нам помочь:

while (matcher.find()) {
    System.out.println("Match: " + matcher.group(0));
    System.out.println("Start: " + matcher.start());
    System.out.println("End: " + matcher.end());
}

Этот код покажет нам, где находится каждое совпадение. Он также показывает нам group(0) match, в котором все захвачено:

Match: First
Start: 0
End: 5
Match: Capital
Start: 8
End: 15
Match: Words
Start: 16
End: 21
Match: I
Start: 37
End: 38
... more

Здесь мы видим, что каждый матч содержит только те слова, которые мы ожидаем. Свойство start показывает нулевой индекс совпадения в строке. end показывает индекс символа сразу после. Это означает, что мы могли бы использовать подстроку(начало, конец-начало) для извлечения каждого совпадения из исходной строки. По сути, это то, как метод group делает это для нас.

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

3. Замена спичек Одна за другой

Давайте продолжим наш пример, используя наш алгоритм для замены каждого заглавного слова в исходной строке его эквивалентом в нижнем регистре. Это означает, что наша тестовая строка будет преобразована в:

"first 3 capital words! then 10 TLAs, i found"

Класс Pattern и Matcher не могут сделать это за нас, поэтому нам нужно построить алгоритм.

3.1. Алгоритм Замены

Вот псевдокод для алгоритма:

  • Начните с пустой выходной строки
  • Для каждого матча:
    • Добавьте в выходные данные все, что было до матча и после любого предыдущего матча
    • Обработайте это совпадение и добавьте его в выходные данные
    • Продолжайте до тех пор, пока все совпадения не будут обработаны
    • Добавьте в вывод все, что осталось после последнего совпадения

Следует отметить , что цель этого алгоритма состоит в том, чтобы найти все несогласованные области и добавить их в вывод , а также добавить обработанные совпадения.

3.2. Заменитель токенов в Java

Мы хотим преобразовать каждое слово в нижний регистр, чтобы мы могли написать простой метод преобразования:

private static String convert(String token) {
    return token.toLowerCase();
}

Теперь мы можем написать алгоритм для перебора совпадений. Это может использовать StringBuilder для вывода:

int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = TITLE_CASE_PATTERN.matcher(original);
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(convert(matcher.group(1)));

    lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
    output.append(original, lastIndex, original.length());
}
return output.toString();

Следует отметить, что StringBuilder предоставляет удобную версию append , которая может извлекать подстроки . Это хорошо работает со свойством end для Совпадений , чтобы мы могли подобрать все несогласованные символы с момента последнего совпадения.

4. Обобщение алгоритма

Теперь, когда мы решили проблему замены некоторых конкретных токенов, почему бы нам не преобразовать код в форму, в которой он может быть использован для общего случая? Единственное, что варьируется от одной реализации к другой, – это регулярное выражение для использования и логика преобразования каждого совпадения в его замену.

4.1. Используйте функцию и шаблон ввода

Мы можем использовать объект Java Function String> , чтобы позволить вызывающему объекту предоставить логику для обработки каждого совпадения. И мы можем взять вход под названием token Pattern , чтобы найти все токены: String>

// same as before
while (matcher.find()) {
    output.append(original, lastIndex, matcher.start())
      .append(converter.apply(matcher));

// same as before

Здесь регулярное выражение больше не жестко закодировано. Вместо этого функция converter предоставляется вызывающим и применяется к каждому совпадению в цикле find .

4.2. Тестирование общей версии

Давайте посмотрим, работает ли общий метод так же хорошо, как и оригинальный:

assertThat(replaceTokens("First 3 Capital Words! then 10 TLAs, I Found",
  TITLE_CASE_PATTERN,
  match -> match.group(1).toLowerCase()))
  .isEqualTo("first 3 capital words! then 10 TLAs, i found");

Здесь мы видим, что вызов кода прост. Функцию преобразования легко выразить в виде лямбды. И испытание проходит.

Теперь у нас есть заменитель токенов, так что давайте попробуем другие варианты использования.

5. Некоторые Варианты Использования

5.1. Экранирование Специальных Символов

Давайте представим, что мы хотели бы использовать escape-символ регулярного выражения \ для ручного цитирования каждого символа регулярного выражения, а не использовать метод кавычек . Возможно, мы цитируем строку как часть создания регулярного выражения для передачи в другую библиотеку или службу, поэтому блока цитирования выражения будет недостаточно.

Если мы можем выразить шаблон, который означает “символ регулярного выражения”, легко использовать наш алгоритм, чтобы избежать их всех:

Pattern regexCharacters = Pattern.compile("[<(\\[{\\\\^\\-=$!|\\]})?*+.>]");

assertThat(replaceTokens("A regex character like [",
  regexCharacters,
  match -> "\\" + match.group()))
  .isEqualTo("A regex character like \\[");

Для каждого совпадения мы ставим префикс символа \ . Поскольку \ является специальным символом в строках Java, он экранируется другим \ .

Действительно, этот пример покрыт дополнительными символами \ , поскольку класс символов в шаблоне для символов регулярных выражений должен цитировать многие специальные символы. Это показывает синтаксическому анализатору регулярных выражений, что мы используем их для обозначения их литералов, а не как синтаксис регулярных выражений.

5.2. Замена Заполнителей

Распространенным способом выражения заполнителя является использование синтаксиса типа ${name} . Давайте рассмотрим случай использования, когда шаблон “Hi ${name} at ${company}” должен быть заполнен с карты под названием placeholderValues :

Map placeholderValues = new HashMap<>();
placeholderValues.put("name", "Bill");
placeholderValues.put("company", "Baeldung");

Все, что нам нужно, – это хорошее регулярное выражение , чтобы найти ${…} токены:

"\\$\\{(?[A-Za-z0-9-_]+)}"

это один из вариантов. Он должен цитировать $ и начальную фигурную скобку, поскольку в противном случае они рассматривались бы как синтаксис регулярного выражения.

В основе этого шаблона лежит группа захвата для имени заполнителя. Мы использовали класс символов, который допускает буквенно-цифровые, тире и подчеркивания, которые должны соответствовать большинству вариантов использования.

Однако, чтобы сделать код более читабельным, мы назвали эту группу захвата |/заполнителем . Давайте посмотрим, как использовать эту именованную группу захвата:

assertThat(replaceTokens("Hi ${name} at ${company}",
  "\\$\\{(?[A-Za-z0-9-_]+)}",
  match -> placeholderValues.get(match.group("placeholder"))))
  .isEqualTo("Hi Bill at Baeldung");

Здесь мы видим, что получение значения именованной группы из Соответствует просто включает в себя использование group с именем в качестве входных данных, а не с номером.

6. Заключение

В этой статье мы рассмотрели, как использовать мощные регулярные выражения для поиска токенов в наших строках. Мы узнали, как метод find работает с Совпадениями , чтобы показать нам совпадения.

Затем мы создали и обобщили алгоритм, позволяющий нам выполнять замену токенов по токенам.

Наконец, мы рассмотрели несколько распространенных вариантов использования для экранирования символов и заполнения шаблонов.

Как всегда, примеры кода можно найти на GitHub .