GrabDuck

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

:

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

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

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


import java.util.regex.Matcher; 
import java.util.regex.Pattern; 
  
public class UserNameCheck { 
     
    public static void main(String[] args){ 
        System.out.println("Cool check:"); 
         
        System.out.println(checkWithRegExp("_@BEST")); 
        System.out.println(checkWithRegExp("vovan")); 
        System.out.println(checkWithRegExp("vo")); 
        System.out.println(checkWithRegExp("Z@OZA")); 
         
        System.out.println("\nDumb check:"); 
         
        System.out.println(dumbCheck("_@BEST")); 
        System.out.println(dumbCheck("vovan")); 
        System.out.println(dumbCheck("vo")); 
        System.out.println(dumbCheck("Z@OZA")); 
    } 
     
    public static boolean checkWithRegExp(String userNameString){ 
        Pattern p = Pattern.compile("^[a-z0-9_-]{3,15}$"); 
        Matcher m = p.matcher(userNameString); 
        return m.matches(); 
    } 
     
    public static boolean dumbCheck(String userNameString){ 
         
        char[] symbols = userNameString.toCharArray(); 
        if(symbols.length < 3 || symbols.length > 15 ) return false; 
         
        String validationString = "abcdefghijklmnopqrstuvwxyz0123456789_"; 
         
        for(char c : symbols){ 
            if(validationString.indexOf(c)==-1) return false; 
        } 
         
        return true; 
    } 
}
Результат:

Cool check:

false

true

false

false

Dumb check:

false

true

false

false

Как видим, в нашей программе есть два метода для проверки имени пользователя на валидность. Первый метод checkWithRegExp(String userNameString) использует регулярное выражение для проверки валидности, а второй dumbCheck(String userNameString) делает проверку "Вручную". 

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

Регуля́рные выраже́ния (англ. regular expressions) — формальный язык поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов (символов-джокеров, англ. wildcard characters). По сути это строка-образец (англ. pattern, по-русски её часто называют «шаблоном», «маской»), состоящая из символов и метасимволов и задающая правило поиска.

Регулярные выражения используются в большом количестве языков программирования.

В Java тоже есть пакет, который позволяет работать с ними - java.util.regex.

"Регулярные выражения откроют перед вами возможности, о которых вы, возможно, даже не подозревали. Ежедневно я неоднократно использую их для решения всевозможных задач – и простых, и сложных(и если бы не регулярные выражения, многие простые задачи оказались бы довольно сложными).

Конечно, эффектные примеры, открывающие путь к решению серьезных проблем, наглядно демонстрируют достоинства регулярных выражений. Менее очевиден тот факт, что регулярные выражения используются в повседневной работе для решения «неинтересных» задач – «неинтересных» в том смысле, что программисты вряд ли станут обсуждать их с коллегами в курилке, но без решения этих задач вы не сможете нормально работать.

Приведу простой пример. Однажды мне потребовалось проверить множество файлов (точнее 70 с лишним файлов с текстом этой книги) и убедиться в том, что в каждом файле строка ‘SetSize’ встречается ровно столько же раз, как и строка ‘ResetSize’. Задача усложнялась тем, что регистр символов при подсчете не учитывался (т. е. строки ‘setSIZE’ и ‘SetSize’ считаются эквивалентными). Конечно, просматривать 32 000 строк текста вручную было бы, по меньшей мере, неразумно. Даже использование стандартных команд поиска в редакторе потребует воистину титанических усилий, учитывая количество файлов и возможные различия в регистре символов.

На помощь приходят регулярные выражения! Я ввожу всего одну короткую команду, которая проверяет все файлы и выдает всю необходимую информацию. Общие затраты времени – секунд 15 на ввод команды и еще 2 секунды на проверку данных. Потрясающе!" Из книги "Регулярные выражения. Дж. Фридл.".

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

Вот примеры основных метасимволов:

  ^     - (крышка, цирркумфлекс) начало проверяемой строки

  $     - (доллар) конец проверяемой строки

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

  |     -  означает «или». Подвыражения, объединенные этим способом, называются альтернативами (alternatives)

  ?     - (знак вопроса) означает, что предшествующий ему символ является необязательным

  +     -  обозначает «один или несколько экземпляров непосредственно предшествующего элемента

  *     –  любое количество экземпляров элемента (в том числе и нулевое)

  \\d   –  цифровой символ

  \\D   –  не цифровой символ

  \\s   –  пробельный символ

  \\S   –  не пробельный символ

  \\w   –  буквенный или цифровой символ или знак подчёркивания

  \\W   –  любой символ, кроме буквенного или цифрового символа или знака подчёркивания

Давайте рассмотрим несколько примеров с некоторыми из этих метасимволов.
Следующий метод проверит строку на содержание в ней слова BACON и только! Никаких пробелов и других символов. Про классы Pattern и Matcher мы ещё поговорим. Метод matches() проверяет строку на соответствие регулярному выражению.

 public static boolean test(String testString){ 
        Pattern p = Pattern.compile("^BACON$"); 
        Matcher m = p.matcher(testString); 
        return m.matches(); 
}
Здесь ^BACON$ = Начало строки + BACON + конец строки.

        System.out.println(test("BACON"));      //true 
        System.out.println(test("  BACON"));    //false 
        System.out.println(test("BACON  "));    //false 
        System.out.println(test("^BACON$"));    //false 
        System.out.println(test("bacon"));      //false
В комментарии справа написан результат выполнения. Надеюсь тут все ясно.
Идем дальше. Напишем простой метод проверки того что строка заканчивается на .com или .ru или .ua. Этакий очень примитивный валидатор ссылки.

  public static boolean test(String testString){ 
        Pattern p = Pattern.compile(".+\\.(com|ua|ru)"); 
        Matcher m = p.matcher(testString); 
        return m.matches();
результат:

   System.out.println(test("trololo.com"));     //true 
        System.out.println(test("trololo.ua "));     //false 
        System.out.println(test("trololo.ua"));      //true 
        System.out.println(test("trololo/ua"));      //false 
        System.out.println(test("i love java because it is cool.ru"));      //true 
        System.out.println(test("BACON.ru"));        //true 
        System.out.println(test("BACON.de"));        //false
Разберем строку ".+\\.(com|ua|ru)" более детально:
+ - означает, что сначала может идти любое количество любых символов (от одного)
\\. - экранирование точки. Таким образом мы указываем, что идет именно точка а не любой символ.
(com|ua|ru) - тут все просто: либо com, либо ua, либо ru. А что было бы, если не было бы скобок ? (регулярное выражение рассматривалось бы как  ".+\\.com" или "ua" или "ru" - совсем не то, что нужно :) ).
С остальными символами поэкспериментируйте сами.

Иногда возникает необходимость описать несколько вариантов ОДНОГО СИМВОЛА. Например, допустим на необходимо найти в тексте слово "Таиланд" и заменить на Украина. Но вот в чем беда: некоторые пишут слово через й "Тайланд". А вдруг кто-то написал слово с маленькой буквы "таиланд" ?

Конечно внимательные читатели могут сказать :"В чем проблема ? мы уже знаем метасивол ИЛИ !". И привести такие варианты регулярных выражений :

"Таиланд|Тайланд|таиланд|тайланд"

"(Т|т)а(и|й)ланд"

И это действительно будет работать. НО механизм регулярных выражений предлагает более изящный способ определения допустимых символов. 

Это символьный класс - он определяет перечень символов которые могут быть (или НЕ могут) на месте данного символа. 

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

К примеру, символьный класс [aeiou] соответствует любой гласной букве в нижнем регистре(Это будет только одна буква из перечня). 


Перепишем наш пример про Таиланд с использованием символьных классов:

public class Rexep {

    public static final String TEXT = "Мне очень нравится Тайланд. Таиланд это то место куда бы я поехал. тайланд - мечты сбываются!";

    public static void main(String[] args){

    System.out.println(TEXT.replaceAll("[Тт]а[ий]ланд", "Украина"));

     }

}
Скомпилируйте этот код и посмотрите результат.

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

Внутри символьных классов есть свои собственные метасимволы:

^ - логическое НЕ. Например [^ABC] - не A или B или C, но все остальные символы подходят.

- -(дефис) интервал символов; так, выражение <H[1-6]> эквивалентно <H[123456]> 


Снова пример:

    public static boolean test(String testString){ 
        Pattern p = Pattern.compile("^[a-z]+"); 
        Matcher m = p.matcher(testString); 
        return m.matches(); 
    }
При выполнении метода:

   System.out.println(test("pizza"));   //true 
        System.out.println(test("@pizza"));  //false 
        System.out.println(test("pizza3"));  //false
"^[a-z]+" = начало строки + любой символ в пределах a-z (abcdef...z) любое количество раз (от одного).

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

Пример:


public static void main(String[] args){ 
        
        Pattern p = Pattern.compile("(якороль).+(\\1)"); 
        Matcher m = p.matcher("регулярные выражения это круто регулярные выражения это круто регулярные выражения это круто якороль Я СЕГОДНЯ ЕЛ БАНАНЫ якороль регулярные выражения это круто" ); 
        if(m.find()){ 
            System.out.println(m.group()); 
        } 
}
Результатом будет:

якороль Я СЕГОДНЯ ЕЛ БАНАНЫ якороль

На месте первой группы (якороль) могло содержаться более сложное выражение, тогда обратная ссылка \\1 значительно сократила бы размер регулярного выражения.

Группы нумеруются слева направо, начиная с 1. Каждая открывающая скобка увеличивает номер группы:

(  (  )  )(  (   )  )

^  ^      ^  ^

1  2      3  4

Нулевая группа совпадает со всей найденной под последовательностью. 

Регулярные выражения предоставляют инструменты позволяющие указать сколько раз может повторятся один или несколько символов. С некоторыми мы уже встречались:

+     - Одно или более

*     - Ноль или более

?     - Ноль или одно

{n}   - Ровно n раз

{m,n} - От m до n включительно

{m,}  - Не менее m

{,n}  - Не более n

Теперь мы можем полностью понять регулярное выражение с самого первого примера: "^[a-z0-9_-]{3,15}$" .

Разберем её по кусочкам:

^ - начало строки

[a-z0-9_-] - символ который может быть маленькой латинской буквой или цифрой или символом подчеркивания.

{3,15} - предыдущий объект(смотри выше) может повторяться от 3х до 15раз.

Рассмотрим регулярное выражение для проверки ip адреса на валидность.

import java.util.regex.Matcher; 
import java.util.regex.Pattern; 
  
public class IPAddressValidator{ 
  
    private Pattern pattern; 
    private Matcher matcher; 
  
    private static final String IPADDRESS_PATTERN =  
"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + 
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + 
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + 
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"; 
  
    public IPAddressValidator(){ 
 pattern = Pattern.compile(IPADDRESS_PATTERN); 
    } 
  
   /** 
    * Validate ip address with regular expression 
    * @param ip ip address for validation 
    * @return true valid ip address, false invalid ip address 
    */ 
    public boolean validate(final String ip){   
 matcher = pattern.matcher(ip); 
 return matcher.matches();         
    } 
}

^                             #начало строки

 (                            #  начало группы #1

   [01]?\\d\\d?           #    возможно 3 цифры, первая 0 или 1 которой

                               #    может не быть вообще, вторая любая цифра, третья

                               #  любая цифра которой может не быть вообще

    |                          #    ИЛИ

   2[0-4]\\d                #    начинается с 2, за которым 

                               #    идет число в пределах 0-4 и потом любое число 

    |                         #    ИЛИ

   25[0-5]                 #    начинается с 25, за которым 

                              #    идет число в пределах 0-5

 )                            #  конец группы

  \.                          #  потом точка

....                          # потом то же самое ещё 3 раза

$                            #конец строки

Не так сложно как кажется на первый взгляд! Главное это практика. Пишите свои регулярные выражения, разбирайте чужие. Если вы поймете и возьмете на вооружение регулярные выражения, вы ещё на шаг приблизитесь к профи! :)

Пакет, который позволяет работать с регулярными выражениями - java.util.regex.

В библиотеке регулярных выражений имеется три основных класса: Pattern, Matcher и PatternSyntaxException.  (еще есть классы ASCII, MatchResult, UnicodeProp)

1. Class Pattern - Регулярное выражение, которое Вы записываете в строке, должно сначала быть скомпилированным в объект данного класса. После компиляции объект этого класса может быть использован для создания объекта Matcher. 

В классе Pattern объявлены следующие методы:

Pattern compile(String regex) – возвращает Pattern, который соответствует regex.

Matcher matcher(CharSequence input) – возвращает Matcher, 

с помощью которого можно находить соответствия в строке input.

http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html

2. Class Matcher

Объект Matcher анализирует строку, начиная с 0, и ищет соответствие шаблону.

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

 boolean matches() просто указывает, соответствует ли вся входная последовательность шаблону.

 int start() указывает значение индекса в строке, где начинается соответствующая шаблону строка.

 int end() указывает значение индекса в строке, где заканчивается соответствующая шаблону строка плюс единица.

 String group() - возвращает найденную строку

 String group(int group) - если у Вас в регулярном выражении были группы, то можно вывести только кусочек строки соответствующей определенной группе.

http://docs.oracle.com/javase/1.5.0/docs/api/java/util/regex/Matcher.html


А теперь давайте посмотрим некоторые методы класса String

public boolean matches(String regex) { 
        return Pattern.matches(regex, this); 
}

Метод, заменяющий первое найденное соответствие

public String replaceFirst(String regex, String replacement) { 
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement); 
}

Метод, заменяющий все найденные соответствия:

public String replaceAll(String regex, String replacement) { 
        return Pattern.compile(regex).matcher(this).replaceAll(replacement); 
}

Заменяет все найденные последовательности символов(массивы) target на replacement

public String replace(CharSequence target, CharSequence replacement) { 
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher( 
                this).replaceAll(Matcher.quoteReplacement(replacement.toString())); 
}

Как видим, даже класс String во всю использует регулярные выражения.

Существует два принципиально разных типа механизмов регулярных выражений: недетерминированный конечный автомат (НКА) , детерминированный конечный автомат (ДКА), а так же существует гибридный вариант.

Java использует механизм НКА.

Рассмотрим алгоритм НКА (Из книги "Регулярные выражения. Дж. Фридл."), который может использоваться механизмом для поиска совпадения выражения to(nite|knight|night) в тексте ‘…tonight…’. Механизм просматривает регулярное выражение по одному компоненту, начиная с t, и проверяет, совпадает ли компонент с «текущим текстом». В случае совпадения проверяется следующий компонент. Процедура повторяется до тех пор, пока не будет найдено совпадение для всех компонентов регулярного выражения; 

в этом случае мы считаем, что найдено общее совпадение. В примере to(nite|knight|night) первым компонентом является литерал t. Проверка завершается неудачей до тех пор, пока в целевом тексте не будет обнаружен символ ‘t’. Когда это произойдет, o сравнивается со следующим символом, и в случае совпадения управление будет передано следующему компоненту. В данном случае «следующим компонентом» является конструкция выбора (nite|knight|night), которая означает «либо nite, либо knight, либо night». Столкнувшись с тремя альтернативами, механизм просто по очереди перебирает их.

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

Проверка первой альтернативы, nite, подчиняется тому же принципу последовательного сравнения компонентов: «Сначала проверить n, потом i, затем t и наконец e». Если проверка завершается неудачей (как

в нашем примере), механизм переходит к другой альтернативе и т. д. до тех пор, пока не будет найдено совпадение или не будут исчерпаны все варианты (тогда механизм сообщает о неудаче). Управление передается

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

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

Спасибо за внимание!

Для закрепления материала рекомендую пройти тесты:
Тест знаний Java - Основы
Тест знаний Java - Средний уровень