Статьи

Java Regex API объяснил

Это было долгое время, но пакет java.util.regex был значительным и чрезвычайно полезным дополнением к Java 1.4. Для веб-разработчиков, постоянно работающих с текстовым контентом, это значительно повышает производительность и эффективность. Регулярные выражения Java могут использоваться в клиентских апплетах Java, а также в коде J2EE и JSP на стороне сервера.

Используя регулярные выражения и пакет регулярных выражений, вы можете легко описывать, находить и обрабатывать сложные шаблоны текста. Поверьте мне, это определенно «Как я обходился без этого?» Такие вещи.

В этой статье я объясню общую идею регулярных выражений, объясню, как работает пакет java.util.regex, а затем кратко расскажу о том, как класс String был модифицирован для использования преимуществ регулярных выражений.

Прежде чем мы перейдем к деталям самого API Java regex, давайте кратко рассмотрим, что на самом деле является регулярным выражением, или для тех, кто в торговле, «регулярным выражением». Если вы уже знаете, что такое регулярное выражение, смело просматривайте следующий раздел.

Что такое регулярное выражение?

Регулярное выражение — это серия метасимволов и литералов, которые позволяют вам описывать подстроки в тексте, используя шаблон. Эти метасимволы фактически сами по себе образуют миниатюрный язык. Фактически, во многих отношениях вы можете думать о регулярных выражениях как о неком SQL-запросе для свободно текущего текста. Рассмотрим следующее предложение:

My name is Will and I live in williamstown. 

Как мы можем найти все вхождения текста «Воля», независимо от того, использовался ли верхний или нижний регистр «w»? С помощью регулярных выражений вы можете описать это требование, составив шаблон из последовательности метасимволов и литералов. Вот такая картина:

 [Ww]ill 

Это довольно просто. Интересной частью является группировка [Ww] — она ​​указывает на то, что любая из букв, заключенных в квадратные скобки (в данном случае, заглавная буква W или строчная буква w), является приемлемой. Таким образом, это регулярное выражение будет соответствовать тексту, который начинается с заглавной или строчной буквы w , и за ним следуют литералы i , затем l , а затем еще один l .

Давайте сделаем это на ступеньку выше. Вышеупомянутое регулярное выражение будет фактически совпадать с 2 вхождениями will — именем Will и первыми 4 символами текста в williamstown . Возможно, мы хотели только искать will и Will , а не слова, которые просто содержат эти 4 символа в последовательности. Вот улучшенная версия:

 b[Ww]illb 

b — это то, как мы описываем границу слова. Граница слова будет соответствовать подобным пробелам, символам табуляции, а также начальной и конечной точкам линии. Это фактически исключает williamstown как совпадение, потому что за вторым l в williamtown не следует граница слова — за ним следует i .

Я мог бы посвятить целую статью изобразительному искусству создания регулярных выражений, но здесь я сосредоточился на самом пакете регулярных выражений Java. Итак, давайте рассмотрим еще одно регулярное выражение — мы будем придерживаться этого в оставшейся части статьи.

   (w+)@(w+.)(w+)(.w+)? 

Давайте возьмем подход «разделяй и властвуй» для анализа этого паттерна. Группировка (w+) (она появляется дважды — изучите в начале) ищет слова, обозначенные буквой w . Знак + означает, что должен появиться один или несколько символов слова (не обязательно один и тот же). За этим должен следовать буквальный символ @ . Скобки здесь на самом деле не требуются, но они делят выражение на группы, и вы скоро увидите, что формирование логических группировок таким образом может быть чрезвычайно полезным.

Основываясь на этой первой части нашего примера регулярного выражения, (w+)@ , вот несколько примеров, которые до сих пор отвечают требованиям:

   billy@  joe@  francisfordcoppola@ 

Давайте перейдем к следующей части. Группировка (w+.) Аналогична, но ожидает совпадения периода для достижения соответствия. Период был экранирован с помощью обратной косой черты, потому что символ точки сам по себе является метасимволом регулярного выражения (подстановочный знак, соответствующий любому символу). Вы всегда должны избегать метасимволов таким образом, если вы хотите сопоставить их буквальное значение.
Давайте рассмотрим несколько примеров, которые бы соответствовали требованиям:

   billy@webworld.  joe@optus.  francisfordcoppola@myisp. 

Группировка (w+) идентична первой группировке — она ​​ищет один или несколько символов слова. Итак, как вы уже без сомнения поняли, наше регулярное выражение предназначено для соответствия адресам электронной почты.

Несколько примеров, которые соответствуют требованиям на данный момент:

   billy@webworld.com  joe@optus.net  francisfordcoppola@myisp.com 

Мы почти у цели. (.w+)* основном должна иметь смысл на этом этапе — мы ищем точку, за которой следует один или несколько символов слова. Но что с * после закрывающих скобок? В мире регулярных выражений мы используем * для обозначения того, что предыдущий метасимвол, литерал или группа могут встречаться ноль или более раз. Например, wd* будет соответствовать символу слова, за которым следует ноль или более цифр. В нашем примере мы используем скобки, чтобы сгруппировать серию метасимволов, поэтому * применяется ко всей группе. Таким образом, вы можете интерпретировать (.w+)* как «сопоставить точку, за которой следует один или несколько символов слова, и сопоставить эту комбинацию ноль или более раз».

Несколько примеров, которые отвечают требованиям полного регулярного выражения:

   fred@vianet.com  barney@comcorp.net.au  wilma@mjinteractive.iinet.net.au 

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

Безопасные регулярные выражения Java

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

   String emailRegEx = "(\w+)@(\w+\.)(\w+)(\.\w+)*"; 

Имейте в виду, что если вам действительно нужно сравнить с буквальным обратным слешем, вы должны снова удвоиться. Может быть сложнее читать безопасное регулярное выражение Java, поэтому вы можете сначала создать «регулярное» регулярное выражение (возможно, regregex?) И сохранить его под рукой — возможно, внутри комментария кода.

Итак, как мы можем использовать все это для достижения чего-то полезного? В некоторых ситуациях вы можете просто вызывать методы, такие как replace() и replaceAll() непосредственно в классе String — позже мы replaceAll() рассмотрим этот подход. Тем не менее, для более сложных операций регулярных выражений вы будете гораздо лучше обслуживаться, если использовать более объектно-ориентированный подход.

Класс Pattern

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

  1. Скомпилируйте вашу строку регулярного выражения, используя класс Pattern.
  2. Используйте класс Pattern, чтобы получить объект Matcher.
  3. Вызовите методы на Matcher, чтобы получить на любые совпадения.

Далее мы рассмотрим класс Matcher, но давайте углубимся в изучение класса Pattern. Этот класс позволяет вам скомпилировать ваше регулярное выражение — это эффективно оптимизирует его для эффективности и использования несколькими целевыми строками (строками, с которыми вы хотите проверить скомпилированное регулярное выражение). Рассмотрим следующий пример:

       String emailRegEx = "(\w+)@(\w+\.)(\w+)(\.\w+)*";      // Compile and get a reference to a Pattern object.      Pattern pattern = Pattern.compile(emailRegEx);      // Get a matcher object - we cover this next.      Matcher matcher = pattern.matcher(emailRegEx); 

Обратите внимание, что объект Pattern был получен с помощью статического метода компиляции класса Pattern — вы не можете создать экземпляр объекта Pattern с помощью new . Если у вас есть объект Pattern, вы можете использовать его для получения ссылки на объект Matcher. Мы смотрим на Matcher дальше.

Класс Matcher

Ранее я предположил, что регулярные выражения являются своего рода запросом SQL для свободно текущего текста. Аналогия не совсем идеальна, но при использовании regex API это может помочь мыслить в этом направлении. Если вы думаете, что Pattern.compile(myRegEx) является своего рода JDBC PreparedStatement, то вы можете рассматривать метод matcher(targetString) классов Pattern matcher(targetString) как своего рода оператор SQL SELECT. Изучите следующий код:

     // Compile the regex.    String regex = "(\w+)@(\w+\.)(\w+)(\.\w+)*";    Pattern pattern = Pattern.compile(regex);    // Create the 'target' string we wish to interrogate.    String targetString = "You can email me at g_andy@example.com or andy@example.net to get more info";    // Get a Matcher based on the target string.    Matcher matcher = pattern.matcher(targetString);     // Find all the matches.    while (matcher.find()) {      System.out.println("Found a match: " + matcher.group());      System.out.println("Start position: " + matcher.start());      System.out.println("End position: " + matcher.end());    } 

Здесь происходит несколько интересных вещей. Прежде всего, обратите внимание, что мы использовали метод matcher() класса Pattern для получения объекта Matcher. Этот объект, все еще использующий нашу аналогию с SQL, — это место, где хранятся полученные совпадения — вспомним JDBC ResultSet. Записи, конечно, являются частями текста, которые соответствуют нашему регулярному выражению.

Цикл while выполняется условно на основе результатов метода find() класса Matcher. Этот метод будет анализировать только целевую строку, достаточную для сопоставления, и в этот момент он вернет true. Будьте осторожны: любые попытки использовать matcher перед вызовом find() приведут к тому, что непроверенная IllegalStateException будет выброшена во время выполнения.

В теле нашего цикла while мы получили соответствующую подстроку с помощью метода group() класса Matcher. Наш цикл while выполняется дважды: один раз для каждого адреса электронной почты в нашей целевой строке. В каждом случае он печатает соответствующий адрес электронной почты, возвращенный методом group() , и информацию о расположении подстроки. Посмотрите на вывод:

 Found a match: g_andy@example.com  Start position: 20  End position: 38  Found a match: andy@example.net  Start position: 42  End position: 58 

Как вы можете видеть, это был просто вопрос использования методов start() и end() Matcher, чтобы выяснить, где совпадающие подстроки произошли в целевой строке. Далее подробнее рассмотрим метод group() .

Понимание групп

Как вы узнали, Matcher.group() будет получать полное совпадение из целевой строки. Но что, если вас также интересуют подразделы или «подгруппы» сопоставляемого текста? В нашем примере электронной почты, возможно, было бы желательно извлечь часть имени хоста из адреса электронной почты и часть имени пользователя. Взгляните на пересмотренную версию нашего цикла Matcher, управляемого:

     while (matcher.find()) {      System.out.println("Found a match: " + matcher.group(0) +                         ". The Username is " +                         matcher.group(1) + " and the ISP is " +                         matcher.group(2));    } 

Как вы помните, группы представлены в виде набора круглых скобок, заключенных в подраздел вашего шаблона. Первая группа, расположенная с использованием Matcher.group() или, как в примере, более конкретного Matcher.group(0) , представляет все совпадение. Другие группы можно найти с помощью того же метода group(int index) . Вот вывод для приведенного выше примера:

 Found a match: g_andy@example.com.. The Username is g_andy and the ISP is example.  Found a match: andy@example.net.. The Username is andy and the ISP is example. 

Как вы можете видеть, group(1) получает часть имени пользователя из адреса электронной почты, а group(2) — часть ISP. Разрабатывая собственные регулярные выражения, вы, конечно же, решаете, как логически подгруппировать свои шаблоны. Небольшой недосмотр в этом примере заключается в том, что сам период фиксируется как часть подгруппы, возвращаемой group(2) !

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

Немного больше о шаблонах и классах Matcher

Это в значительной степени ядро ​​этого очень маленького, но очень способного Java API. Тем не менее, есть несколько других частей, которые вы должны изучить, как только у вас будет возможность поэкспериментировать с основами. Класс Pattern имеет несколько флагов, которые вы можете использовать в качестве второго аргумента для метода compile() . Например, вы можете использовать Pattern.CASE_INSENSITIVE чтобы Pattern.CASE_INSENSITIVE механизм регулярных выражений сопоставлять символы ASCII независимо от регистра.

  Pattern.MULTILINE является еще одним полезным.  Иногда вы захотите сообщить движку регулярных выражений, что ваша целевая строка не является ни одной строкой кода;  скорее он содержит несколько строк, которые имеют свои собственные символы завершения. 
Если вам нужно, вы можете объединить несколько флагов с помощью java | (вертикальная черта) оператора. Например, если вы хотите скомпилировать регулярное выражение с поддержкой многострочной и нечувствительности к регистру, вы можете сделать следующее:
 Pattern.compile(myRegEx, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE ); 

Класс Matcher также имеет ряд интересных методов: String replaceAll(String replacementString) , в частности, стоит упомянуть String replaceFirst(String replacementString) String replaceAll(String replacementString) и String replaceFirst(String replacementString) .

Метод replaceAll() принимает строку замены и заменяет все совпадения ей. Метод replaceFirst() очень похож, но, как вы уже догадались, он заменит только первое совпадение. Посмотрите на следующий код:

     // Matches 'BBC' words that end with a digit.    String thePattern = "bbc\d";    // Compile regex and switch off case sensitivity.    Pattern pattern = Pattern.compile(thePattern, Pattern.CASE_INSENSITIVE);    // The target string.    String target = "I like to watch bBC1 and BbC2 - I suppose ITV is okay too";    // Get the Matcher for the target string.    Matcher matcher = pattern.matcher(target);    // Blot out all references to the BBC.    System.out.println(matcher.replaceAll("xxxx") ); 

Здесь 'вывод:

 I like to watch xxxx and xxxx - I suppose ITV is okay too 
Обратные

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

 (w)(w)(1) 

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

Методы замены объекта Matcher (и аналоги класса String) также поддерживают нотацию для выполнения обратных ссылок в строке замены. Он работает таким же образом, но вместо обратной косой черты используется знак доллара. Таким образом, matcher.replaceAll("$2") заменит все совпадения в целевой строке значением, сопоставленным второй подгруппой регулярного выражения.

Методы RegEx класса String

Как я упоминал ранее, класс Java String был обновлен, чтобы использовать преимущества регулярных выражений. В простых случаях вы можете полностью обойти использование regex API напрямую, вызвав методы с поддержкой regex непосредственно в классе String. Доступно 5 таких методов.

Вы можете использовать метод boolean matches(String regex) чтобы быстро определить, соответствует ли строка определенному шаблону. String replaceFirst(String regex, String replacement) соответствующим именем и String replaceFirst(String regex, String replacement) String replaceAll(String regex, String replacement) позволяют выполнять быстрые и грязные замены текста. И наконец, String[] split(String regEx) и String[] split(String regEx, int limit) позволяют разбивать строку на подстроки на основе регулярного выражения. Эти два последних метода по своей java.util.StringTokenizer похожи на java.util.StringTokenizer , только гораздо более мощные.

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

Надеюсь, я дал вам преимущество API regex и соблазнил тех, кто еще не открыл этот мощный инструмент, серьезно подумать. Совет: не тратьте часы драгоценного времени на разработку сложного регулярного выражения - оно может уже существовать. Есть много мест, таких как www.regexlib.com, которые делают целую кучу их свободно доступными.