Regexp-ы для java точь в точь как для php

Сегодня я столкнулся с необходимость написать несколько регулярных выражений для java. Надо сказать, что последнее время я часто переключаюсь между java и php, так что держать в голове два стиля использования regexp-ов становится все труднее: допускаю мелкие ошибки и опечатки. Отличия в regexp-ах не в самом синтаксисе (он обычный, те же самый \w, \d, классы символов и их модификаторы). Отличия в мелочах и эти мелочи мне не нравятся:

Сравнение java-подхода к regexp-ам и подхода php

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

System.out.println ( "boy goes to school".matches("boy") );

И ... вы думаете, что вызов такого метода вернет true. Как бы не так, считается что строка regexp-а должна полностью совпасть со всей строкой.

System.out.println ( "boy goes to school".matches(".*boy.*") );

Для php и некоторых других продуктов, поведение противоположное - по-умолчанию считается что ассоциация regexp-а ищется везде, в любом месте строки (здесь и далее в примерах php, большое спасибо официальному php-доку).

<?
// The "i" after the pattern delimiter indicates a case-insensitive search
if (preg_match("/php/i", "PHP is the web scripting language of choice.")) {
   echo "A match was found.";
} 
else {
   echo "A match was not found.";
}
?>

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

"boy goes to school".matches("(?i).*BOY.*")

Теперь как сделать так чтобы найти все совпадения? В php это делается так:

<?
// The \\2 is an example of backreferencing. This tells pcre that
// it must match the second set of parentheses in the regular expression
// itself, which would be the ([\w]+) in this case. The extra backslash is 
// required because the string is in double quotes.
$html = "<b>bold text</b><a href=howdy.html>click me</a>";
 
preg_match_all("/(<([\w]+)[^>]*>)(.*)(<\/\\2>)/", $html, $matches, PREG_SET_ORDER);
 
foreach ($matches as $val) {
   echo "matched: " . $val[0] . "\n";
   echo "part 1: " . $val[1] . "\n";
   echo "part 2: " . $val[3] . "\n";
   echo "part 3: " . $val[4] . "\n\n";
}
?>

Результат будет таким:

matched: <b>bold text</b>
part 1: <b>
part 2: bold text
part 3: </b>
 
matched: <a href=howdy.html>click me</a>
part 1: <a href=howdy.html>
part 2: click me
part 3: </a>

Для java все гораздо сложнее (ну то есть, все гораздо гибче и сделать можно более хитрый поиск и прочая и прочая ... но почти всегда такой код излишен). Например следующий код я взял со страницы Proj csscompactor, он выполняет поиск в строке комментариев css и их последующее выбрасывание если какое-то условие выполнилось:

Pattern p = null;
Matcher m =
null;
try {
// компилируем regexp, который ищет все комментарии без учета их длины
  
p = Pattern.compile("(?is)/\\*.*?\\*/");
   m = p.matcher
(sin);
  
// создаем объект Matcher, с помощью которого затем будет организован
   // цикл перебора всех найденных (подошедших под шаблон) строк-коментариев.
  
sb = new StringBuffer();
  
while (m.find()) {
     
// функция find ищет в исходной строке очередной подошедший для regexp-а
      // фрагмент,
      // но как только таких совпадений больше нет, то цикл будет прекращен
     
try {
        
String gr_0 = m.group(0);
        
// все группы (части regexp-а заключенные в круглые скобки) могут
         // быть доступны
         // с помощью функции group(номер_группы).
         // Есть особый номер группы – 0 – эта группа захватывает абсолютно
         // весь текст строки,
         // который проассоциировался с регулярным выражением
        
if (gr_0.length() - 4 <= pi)
           
// проверяем, что если длина этого комментария за вычетом
            // четырех символов
            // (два знака “*” и два знака “/”) все же не смогла превзойти
            // предельную то в буфер sb помещается комментарий
           
m.appendReplacement(sb, gr_0);
        
// else
         // иначе комментарий удалется
         // m.appendReplacement(sb, "");
     
} catch (Exception e) {
        
e.printStackTrace();
     
}
   }
// end of -- while --
   // завершаем обработку хвоста исходной строки – после последнего совпадения
   // с regexp
  
m.appendTail(sb);
} catch (Exception e) {
  
e.printStackTrace();
}
sin = sb.toString();

Ближе к делу

Одним словом, в java сделать можно больше чем в php но всегда большим количеством кода. Так что я решил написать небольшой класс-утилитку которая бы позволяла вызывать regexp-ы сходным образом, как для php:

Вот пример использования:

/*
* Для записи регулярного выражения используется Perl-like синтаксис, когда
* строка состоит из двух секций РАЗДЕЛИТЕЛЬ_1 СЕКЦИЯ_ЧТО_ИСКАТЬ РАЗДЕЛИТЕЛЬ_2
* СЕКЦИЯ_МОДИФИКАТОРЫ разделители строятся по стандартным правилам и могут быть
* любыми одинаковыми символами, или парными [] и {} и () и <> секция
* модификаторов может состоять из следующих символов: i - поиск/замена будет
* регистронечувствительной CASE_INSENSITIVE d - модификатор UNIX_LINES x -
* режим когда игнорируются пробелы и можно комментировать выражение COMMENTS m -
* многострочный режим, в котором символы ^ и $ ассоциируются с началом и концом
* каждой из строк MULTILINE s - режим "точка - это все", в котором символ новой
* строки ассоциирутся с символом "." DOTALL u - включена поддержка Unicode
* case-folding-га UNICODE_CASE
*/
List<String> rez = new ArrayList<String>();
List<List<String>> rez2 =
new ArrayList<List<String>>();

String pattern =
"/(\\w+)-(\\w+)/idxmsu";
String inputString =
"hello-petyano petka-lenka";

// пример с поиском внутри строки всех совпадений
if (RegexpUtils.preg_match_all(pattern, inputString, rez2)) {
  
System.out.println("rez = " + rez);
}
// ищем одно, только первое, совпадение
if (RegexpUtils.preg_match("|(\\w+)-(\\w+)|", inputString, rez)) {
  
System.out.println("rez = " + rez);
}


System.out.println
("=============================== ");

// теперь пример простой замены
System.out.println("rez = " + RegexpUtils.preg_replace("/-(\\w+)/i", inputString, "X:$1"));

// и пример сложной замены
System.out.println("rez = " + RegexpUtils.preg_replace_callback("/-(\\w+)/i", inputString,
     
new RegexpUtils.Replacer() {
  
public String onMatch(List<String> matches) {
     
return "z:" + matches.get(1);
  
}
}
))
;

Вот результат работы:

rez = [[hello-petyano, hello, petyano], [ petka-lenka, petka, lenka]]
rez = [hello-petyano petka-lenka, hello, petyano]
=============================== 
rez = helloX:petyano petkaX:lenka
rez = helloz:petyano petkaz:lenka

А вот пример исходных кодов моей библиотечки

package testi.catsandusers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Класс-обертка над стандартными для java средствами работы с регулярными
* выражениями Вместо классов Pattern, Matcher и циклов используются функции и
* подход их использования аналогичный php
*/
public class RegexpUtils {
  
/**
    * Интерфейс который должен реализовать тот кто хочет выполнить обработку,
    * замену каждого вхождения программно
    */
  
public static interface Replacer {
     
/**
       * Метод должен вернуть строку на которую будет выполнена замена
       * найденного regexp-ом фрагмента
       *
       *
@param matches
       *            список с информацией об найденном фрагменте, нулевой
       *            элемент списка содержит весь текст "совпадения" остальные
       *            же элементы 1,2, ... содержат значения для групп внутри
       *            регулярного выражения
       *
@return
      
*/
     
public String onMatch(List<String> matches);
  
}
  

  
/**
    * Кэш, в котором хранятся скомпилированные regexp-выражения
    */
  
private static HashMap<String, Pattern> cache = new HashMap<String, Pattern>();
  
  
/**
    * Очиска кэша скомпилированных regexp-выражений
    */
  
public void clearCache() {
     
cache.clear();
  
}
  

  
/**
    * Выполнить поиск в строке шаблона и заменить его на новую величину
    * вычисляемую динамически, пользователем
    *
    *
@param pattern
    *            шаблон (regexp)
    *
@param input
    *            строка, где выполнить поиск
    *
@param by
    *            объект Replacer - задает значение на что выполнить замену
    *
@return строка после замены
    */
  
public static String preg_replace_callback(String pattern, String input, Replacer by) {
     
Pattern p = compile(pattern, false);
      Matcher m = p.matcher
(input);
     
final int gcount = m.groupCount();
      StringBuffer sb =
new StringBuffer();
      ArrayList<String> row =
new ArrayList<String>();
     
     
while (m.find()) {
        
try {
           
row.clear();
           
for (int i = 0; i <= gcount; i++)
              
row.add(m.group(i));
            m.appendReplacement
(sb, by.onMatch(row));
        
} catch (Exception e) {
           
e.printStackTrace();
        
}
      }
// end -- while --
     
m.appendTail(sb);
     
return sb.toString();
  
}
  

  
/**
    * Выполнить поиск в строке шаблона и заменить его на новую величину
    * вычисляемую средствами Regexp-выражения
    *
    *
@param pattern
    *            шаблон (regexp)
    *
@param input
    *            строка, где выполнить поиск
    *
@param by
    *            строка, на которую нужно заменить найденное значение
    *
@return строка после замены
    */
  
public static String preg_replace(String pattern, String input, String by) {
     
Pattern p = compile(pattern, false);
      Matcher m = p.matcher
(input);
      StringBuffer sb =
new StringBuffer();
     
while (m.find()) {
        
try {
           
m.appendReplacement(sb, by);
        
} catch (Exception e) {
           
e.printStackTrace();
        
}
      }
// end -- while --
     
m.appendTail(sb);
     
return sb.toString();
  
}
  

  
/**
    * Проверка того ассоциирутся ли строка с шаблоном
    *
    *
@param pattern
    *            шаблон (regexp)
    *
@param input
    *            строка, где выполнить поиск
    *
@param rez
    *            Список куда будет помещена информация об совпадении: нулевой
    *            элемент списка содержит весь текст совпадения 1, 2, ...
    *            содержат значения групп
    *
@return булево выражение - признак того что ассоциация произошла
    */
  
public static boolean preg_match(String pattern, String input, List <String> rez) {
     
Pattern p = compile(pattern, true);
      Matcher m = p.matcher
(input);
     
final int gcount = m.groupCount();
     
if (rez != null)
        
rez.clear();
     
if (m.matches())
        
for (int i = 0; i <= gcount; i++) {
           
if (rez != null)
              
rez.add(m.group(i));
        
}
     
return rez.size() > 0;
  
}
  

  
/**
    * Проверка того что в строке содержится некоторый шаблон и возвращается
    * список со всеми найденными группами совпадений
    *
    *
@param pattern
    *            шаблон (regexp)
    *
@param input
    *            строка, где выполнить поиск
    *
@param rez
    *            список, куда будут помещены все найденные соответвия, список
    *            двухуровневый: первый уровень содержит перечисление
    *            объектов-списков, каждый из которых содержит информацию об
    *            очередном совпадении в таком же формате как и метод preg_match
    *
@return
   
*/
  
public static boolean preg_match_all(String pattern, String input, List <List <String>> rez) {
     
Pattern p = compile(pattern, true);
      Matcher m = p.matcher
(input);
     
final int gcount = m.groupCount();
     
if (rez != null)
        
rez.clear();
     
while (m.find()) {
        
ArrayList row = new ArrayList();
        
for (int i = 0; i <= gcount; i++) {
           
if (rez != null)
              
row.add(m.group(i));
        
}
        
if (rez != null)
           
rez.add(row);
     
}
     
return rez.size() > 0;
  
}
  

  
  
/**
    * Слежебный метод выполняющий компиляцию regexp-а и сохранение его в кэш
    *
    *
@param pattern
    *            текст регулярного выражения
    *
@param surroundBy
    *            признак того нужно ли выражение окружить .*?
    *
@return скомпилированный Pattern
    */
  
private static Pattern compile(String pattern, boolean surroundBy) {
     
if (cache.containsKey(pattern)) return cache.get(pattern);
     
final String pattern_orig = pattern;
     
     
final char firstChar = pattern.charAt(0);
     
char endChar = firstChar;
     
if (firstChar == '(') endChar = '}';
     
if (firstChar == '[') endChar = ']';
     
if (firstChar == '{') endChar = '}';
     
if (firstChar == '<') endChar = '>';
     
     
int lastPos = pattern.lastIndexOf(endChar);
     
if (lastPos == -1)
        
throw new RuntimeException("Invalid pattern: " + pattern);
     
     
char[] modifiers = pattern.substring(lastPos + 1).toCharArray();
     
int mod = 0;
     
for (int i = 0; i < modifiers.length; i++) {
        
char modifier = modifiers[i];
        
switch (modifier) {
        
case 'i':
            mod |= Pattern.CASE_INSENSITIVE;
           
break;
        
case 'd':
            mod |= Pattern.UNIX_LINES;
           
break;
        
case 'x':
            mod |= Pattern.COMMENTS;
           
break;
        
case 'm':
            mod |= Pattern.MULTILINE;
           
break;
        
case 's':
            mod |= Pattern.DOTALL;
           
break;
        
case 'u':
            mod |= Pattern.UNICODE_CASE;
           
break;
        
}
      }
     
pattern = pattern.substring(1, lastPos);
     
if (surroundBy) {
        
if (pattern.charAt(0) != '^')
           
pattern = ".*?" + pattern;
        
if (pattern.charAt(pattern.length() - 1) != '$')
           
pattern = pattern + ".*?";
     
}
     

     
final Pattern rezPattern = Pattern.compile(pattern, mod);
      cache.put
(pattern_orig, rezPattern);
     
return rezPattern;
  
}
}

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

Источник — http://black-zorro.com/mediawiki/Regexp-ы_для_java_точь_в_точь_как_для_php

Теги: php regexp