My name is Vasyl Khrystiuk‎ > ‎pub‎ > ‎java‎ > ‎core‎ > ‎

regexp in java


Теория и история.

    Регулярные выражения - это формальный язык поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов (wildcard characters). По сути это строка-образец (pattern), состоящая из символов и метасимволов и задающая правило поиска. (Вики)

    Проще говоря,   регекспы это старый движок для поиска подстрок.
    Существует два типа регулярных выражений - POSIX(появились раньше) и PCRE (перл совместимые). 

    Регекспы имеют большую гибкость но не гарантированно являются оптимальными, т.к. по определению работают полным перебором. И неправильно написанные регулярные выражение может убить программу. ( Подробней здесь: http://habrahabr.ru/blogs/regex/117177/ на основе этого: http://ru.wikipedia.org/wiki/Алгоритм_Ахо_—_Корасик, ключевые слова: NFA и минимальный DFA, Префиксное дерево) Ну и конечно же, от фанатов регекспов: http://search.cpan.org/~jhi/Regex-PreSuf-1.17/PreSuf.pm. Так же они не всегда уместны (для проверки текста по словарю более эффективны алгоритмы поиска с множественным паттерном, такие как автомат Aho-Corasick, Wu-Manber ).

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


RegExp в Java

    Есть много реализаций регекспов в яве. Все они имеют определенный уровень сложности (и не только).
Краткий список и сравнение производительности здесь: http://www.tusker.org/regex/regex_benchmark.html 

    Здесь будут рассматриваться регекспы из стандартной библиотеки Java. Эта библиотека представлена 3 классами:
            Здесь все просто: Unchecked exception thrown to indicate a syntax error in a regular-expression pattern.
        Matcher - хранит в себе обрабатываемую строку, ее состояние помнит и используемый паттерн.
        Pattern - собственно объект паттерна.

Что со всем этим можно сделать?

Найти соответствие строки паттерну

Способ 1:
Pattern.compile("a*b",Pattern.CASE_INSENSITIVE).matcher("aaab").matches();// true
ВНИМАНИЕ: только полное соответствие - от начала строки до конца, т.е. следующий пример:
Pattern.compile("a*b",Pattern.CASE_INSENSITIVE & Pattern.MULTILINE).matcher("aaabaaab").matches();// false
законно вернет false , т.к. вся строка не соответствует паттерну. 
Решение:
Pattern.compile(".*a*b.*",Pattern.CASE_INSENSITIVE).matcher("aaabaaab").matches() /* full maching */: true

Способ 2:
"dreedisgood".matches("(?i).*IS.*"); // где (?i) - флаги

Флаги

Флаги вычитываются обычной битовой маской (4*isSomeOpt()+2* isSomeOpt2()+ isSomeOpt3() )
Пример их использования пунктом выше. Список в доке по классу Pattern.

Разбитие строки на подстроки с разделителем

for (String word : Pattern.compile("a").split("asdasdasd")){
    System.out.println("splitted: " + word);
}
OUT:
splitted: 
splitted: sd
splitted: sd
splitted: sd
Как видим, перед первой буквой "а" еще есть пустая строка (длиной в 0 символов).

Еще один пример:
Pattern p3 = Pattern.compile("\\d+\\s?");
String[] words =  p3.split("java5tiger 77 java6mustang");
for (String word : words){
    System.out.println("splitted: "+word);
}
OUT:
splitted: java
splitted: tiger 
splitted: java
splitted: mustang

И еще один пример, в котором будем разбивать только на 2 группы - первую и все остальное:
String ssstr = "";
int c = 0;
for (String s  : Pattern.compile("\\d").split("va1sda1sda3sd",2)) {
    ssstr+="s["+(c++)+"]:"+s+"  ";
}
System.out.println("split with arg: "+ssstr);

OUT:
split with arg: s[0]:va  s[1]:sda1sda3sd  

Проверяет - начинается ли строка с паттерна

Pattern.compile("va.*b").matcher("va1sda1sda 3sbd").lookingAt(); //  true
Pattern.compile("a.*b").matcher("va1sda1sda 3sbd").lookingAt(); //  false
Правда никто не мешает написать нам  так:
System.out.println(Pattern.compile(".*a.*b").matcher("va1sda1sda 3sbd").lookingAt()); //true
Остается надеяться, что это работает так из за соображений производительности.

Изменение паттерна/данных

Pattern p3 = Pattern.compile("va.+?b"); // изначальный паттерн
Matcher m3 = p3.matcher("va1sda1bda 3sbd"); // матчер для него
System.out.println("Что-то нашли? "+m3.find());
System.out.println("Что именно? "+m3.group());
System.out.println(m3.toString());//дает инфу об используемом паттерне и состоянии матчера
m3.usePattern(Pattern.compile("\\d")); // новый паттерн
System.out.println(m3.toString());
m3.reset(); // сброс состояния матчера
System.out.println("Можно искать снова: "+ m3.find());
System.out.println(что там? "+m3.group());

OUT:
Что-то нашли? true
Что именно? va1sda1b
java.util.regex.Matcher[pattern=va.+?b region=0,15 lastmatch=va1sda1b]
java.util.regex.Matcher[pattern=\d region=0,15 lastmatch=]
Можно искать снова: true
И что там? 1

Если в коде выше убрать строку m3.reset(); то новый паттерн продолжит поиск с последнего места и последней строкой в выводе будет:
И что там? 3
Также в метод reset( можно передать новую строку для матчера, т.е замена m3.reset( на m3.reset("5sdf") даст:
И что там? 5


Поиск подстрок

Matcher m3 = Pattern.compile("\\d(.{1})").matcher("va1sda1dda 3jhbd"); 
while (m3.find()) { 
    // то же самое что и m3.group(0), где 0 означает полную найденную подстроку
    System.out.println("new group after find: "+ m3.group());
    // а здесь 1 означает первую выделенную группу в подстроке
    System.out.println("new subgroup in found: "+ m3.group(1));
}

OUT:
new group after find: 1s
new subgroup in found: s
new group after find: 1d
new subgroup in found: d
new group after find: 3j
new subgroup in found: j

Так же есть параметризированный вариант метода Matcher.find(int offset), который ищет по смещению.

Другой пример:
 String regex = "(\\w+)@(\\w+\\.)(\\w+)(\\.\\w+)*";
 String s ="адреса эл.почты:mymail@tut.by и rom@bsu.by";
 Pattern p2 = Pattern.compile(regex);
 Matcher m2 = p2.matcher(s);
 if(m2.find()) {
     for(int i=0; i<m2.groupCount(); i++) {
         System.out.println("all groupes: "+m2.group(i));
     }
 }
 m2.reset();
 while (m2.find()){
     System.out.println("e-mail: "+  m2.group());
 }

OUT:
all groupes: mymail@tut.by
all groupes: mymail
all groupes: tut.
all groupes: by
e-mail: mymail@tut.by
e-mail: rom@bsu.by




Замена по паттерну

Способ 1:
Простая замена буквы "i" на "1"
CharSequence inputStr =  "this is simple tex thaw i want to chancE ";
System.out.println(inputStr);
String patternStr = "i";
String replacementStr = "1";
Pattern pattern = Pattern.compile(patternStr);
Matcher matcher = pattern.matcher(inputStr);
String output = matcher.replaceAll(replacementStr);
System.out.println(output);

OUT:
this is simple tex thaw i want to chancE 
th1s 1s s1mple tex thaw 1 want to chancE 

Сложная замена с использованием групп (пробел в конце входной строки не трогать - без него не все работает как надо =) ). Обращение к группам идет через $x, где х - номер группы. Переставляет первую с последней буквой слова местами:
...
String patternStr = "\\s?(\\S?)(\\S+?)(\\S?)\\s";
String replacementStr = "$3$2$1 ";
...

OUT:
this is simple tex thaw i want to chancE 
shit si eimpls xet what i tanw ot Ehancc 

Способ 2:

Следующий пример ищет слово, которое начинается с цифр и заканчивается буквами. Найденное дополняется текстом в начале и в конце.
     CharSequence inputStr = "ab12 cd efg34 asdf 123";
     System.out.println(inputStr); // found<ab12> cd found<efg34> asdf 123
     String patternStr = "([a-zA-Z]+[0-9]+)";

     Pattern pattern = Pattern.compile(patternStr);
     Matcher matcher = pattern.matcher(inputStr);

     StringBuffer buf = new StringBuffer();
     while (matcher.find()) {
       String replaceStr = matcher.group();
       matcher.appendReplacement(buf, "found<" + replaceStr + ">");
     }
     matcher.appendTail(buf);
     
     String result = buf.toString();
     System.out.println(result);

OUT:
ab12 cd efg34 asdf 123
found<ab12> cd found<efg34> asdf 123

Еще один хороший пример. Подстановка значений в строку по ключу.
Map<String, String> props = new HashMap<String, String>();
 props.put("key1", "fox");
 props.put("key2", "dog");
 String input = "The quick brown ${key1} jumps over the lazy ${key2}.";
 System.out.println(input);
 Pattern p = Pattern.compile("\\$\\{([^}]+)\\}");
 Matcher m = p.matcher(input);
 StringBuffer sb = new StringBuffer();
 while (m.find()) {
 m.appendReplacement(sb, "");
 sb.append(props.get(m.group(1)));
 }
 m.appendTail(sb);
 System.out.println(sb.toString());

OUT:
The quick brown ${key1} jumps over the lazy ${key2}.
The quick brown fox jumps over the lazy dog.


Жадный поиск

Сначала в википедию. Там написано об чем речь. По умолчанию паттерн берет максимально возможную строку. Но это можно изменить.
Пример манипуляции этими установками:

В первом случае к первой группе относятся все возможные символы, но при этом остается минимальное количество символов для второй группы.
Во втором случае для первой группы выбирается наименьшее количество символов, т. к. используется слабое совпадение.
В третьем случае первой группе будет соответствовать вся строка, а для второй не остается ни одного символа, так как вторая группа использует слабое совпадение.
В четвертом случае строка не соответствует регулярному выражению, т. к. для двух групп выбирается наименьшее количество символов.
 
 String input = "abdcxyz";
 myMatches("([a-z]*)([a-z]+)", input);
 myMatches("([a-z]?)([a-z]+)", input);
 myMatches("([a-z]+)([a-z]*)", input);
 myMatches("([a-z]?)([a-z]?)", input);
 


public static void myMatches(String regex, String input) {
 
 Pattern pattern = Pattern.compile(regex);
 Matcher matcher = pattern.matcher(input);
 System.out.println("regex: "+regex+" :");
 if (matcher.matches()) {
 System.out.print("First group: "+ matcher.group(1)+"     ");
 System.out.println("Second group: "+ matcher.group(2));
 } else {
 System.out.println("nothing"); 
 }
 
}

regex: ([a-z]*)([a-z]+) :
First group: abdcxy     Second group: z
regex: ([a-z]?)([a-z]+) :
First group: a     Second group: bdcxyz
regex: ([a-z]+)([a-z]*) :
First group: abdcxyz     Second group: 
regex: ([a-z]?)([a-z]?) :
nothing


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


Регионы

Тут все тоже просто: можно ограничить область поиска в строке с помощью создания "региона", и установки его начала и конца:

   String text = "012 456 890 234";
    Pattern ddd = Pattern.compile("\\d{3}");
    Matcher m = ddd.matcher(text).region(3, 12);
    System.out.println("region start here: "+m.regionStart());
    System.out.println("region end here: "+m.regionEnd());
    while (m.find()) {
        System.out.printf("[%s] [%d,%d)%n",
                m.group(),
                m.start(),
                m.end()
        );
    }
    
OUT:
region start here: 3
region end here: 12
[456] [4,7)
[890] [8,11)

По умолчанию создание региона  влияет на спецсимволы начала и конца строки в паттерне(^ и $). Но и это можно изменить с помощью команды Matcher.useAnchoringBounds(true).
    String text = "012 456 890 234";
    Pattern ddd = Pattern.compile("^.*$");
    Matcher m = ddd.matcher(text).region(3, 12);
    System.out.println("region start here: "+m.regionStart());
    System.out.println("region end here: "+m.regionEnd());
    if(m.find()){
        System.out.println("find matcher text with AnchoringBounds "+m.group());
        System.out.println("check AnchoringBounds: "+ m.hasAnchoringBounds());
    }
    m.reset();
    m.useAnchoringBounds(false);
    if(m.find()){
        System.out.println("find matcher text WITHOUT  AnchoringBounds "+m.group());
        System.out.println("check AnchoringBounds: "+ m.hasAnchoringBounds());
    }
    m.reset();
    
    System.out.println("region start here: "+m.regionStart());
    System.out.println("region end here: "+m.regionEnd());


OUT:
region start here: 3
region end here: 12
find matcher text with AnchoringBounds  456 890 
check AnchoringBounds: true
find matcher text WITHOUT  AnchoringBounds 012 456 890 234
check AnchoringBounds: false
region start here: 0
region end here: 15


Экранирование

Как мы видели выше - в строках, которые например передаются на поиск или на замену могут быть символы со специальным значением. Чтоб избавиться от этого побочного эффекта, где он не нужен - строки нужно особым образом экранировать. К сожалению я не знаю как нужно экранировать, но разработчики библиотеки подумали за меня и добавили в класс Pattern этот метод:

public static String quote(String s);

Он экранирует вместо меня.


http://myregexp.com/signedJar.html  - онлайн тестер
Это все. Исходники прилагаются. Вопросы, неточности, ошибки? Пишите мне и я исправлю.

java_tag

ċ
RegExpGround.java
(11k)
Vasyl Khrystiuk,
24 Feb 2012, 19:45
Comments