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

April 15, 2008

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

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



Например, для того чтобы в java проверить строку на наличие определенного шаблона я должен сделать так:
  1. System.out.println ( "boy goes to school".matches("boy") );
И ... вы думаете, что вызов такого метода вернет true. Как бы не так, считается что строка regexp-а должна полностью совпасть со всей строкой.
  1. System.out.println ( "boy goes to school".matches(".*boy.*") );
Для php и некоторых других продуктов, поведение противоположное - по-умолчанию считается что ассоциация regexp-а ищется везде, в любом месте строки (здесь и далее в примерах php, большое спасибо официальному php-доку).
  1. // The "i" after the pattern delimiter indicates a case-insensitive search
  2. if (preg_match("/php/i", "PHP is the web scripting language of choice.")) {
  3.    echo "A match was found.";
  4. } 
  5. else {
  6.    echo "A match was not found.";
  7. }
Как видите, отличия еще и в том, как задаются модификаторы поиска (например, i - говорит, что искать нужно без учета регистра).

В java синтаксис указания модификаторов другой:
  1. "boy goes to school".matches("(?i).*BOY.*")
Теперь как сделать так чтобы найти все совпадения? В php это делается так:
  1. // The \\2 is an example of backreferencing. This tells pcre that
  2. // it must match the second set of parentheses in the regular expression
  3. // itself, which would be the ([\w]+) in this case. The extra backslash is 
  4. // required because the string is in double quotes.
  5. $html = "<b>bold text</b><a href=howdy.html>click me</a>";
  6.  
  7. preg_match_all("/(<([\w]+)[^>]*>)(.*)(<\/\\2>)/", $html, $matches, PREG_SET_ORDER);
  8.  
  9. foreach ($matches as $val) {
  10.    echo "matched: " . $val[0] . "\n";
  11.    echo "part 1: " . $val[1] . "\n";
  12.    echo "part 2: " . $val[3] . "\n";
  13.    echo "part 3: " . $val[4] . "\n\n";
  14. }
Результат будет таким:
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 все гораздо сложнее (ну то есть, все гораздо гибче и сделать можно более хитрый поиск и прочая и прочая ... но почти всегда такой код излишен). Например следующий код я взял со страницы Сжатие css. Небольшая самописная утилитка, он выполняет поиск в строке комментариев css и их последующее выбрасывание если какое-то условие выполнилось:
  1. Pattern p = null;
  2. Matcher m = null;
  3. try {
  4. // компилируем regexp, который ищет все комментарии без учета их длины
  5.   p = Pattern.compile("(?is)/\\*.*?\\*/");
  6.   m = p.matcher(sin);
  7.   // создаем объект Matcher, с помощью которого затем будет организован 
  8.   // цикл перебора всех найденных (подошедших под шаблон) строк-коментариев.
  9.   sb = new StringBuffer();
  10.   while (m.find()) {
  11.   // функция find ищет в исходной строке очередной подошедший для regexp-а фрагмент, 
  12.   // но как только таких совпадений больше нет, то цикл будет прекращен
  13.      try {
  14.        String gr_0 = m.group(0);
  15.        // все группы (части regexp-а заключенные в круглые скобки) могут быть доступны 
  16.        // с помощью функции group(номер_группы). 
  17.        // Есть особый номер группы – 0 – эта группа захватывает абсолютно весь текст строки,
  18.        // который проассоциировался с регулярным выражением
  19.        if (gr_0.length() - 4 <= pi)
  20.          // проверяем, что если длина этого комментария за вычетом четырех символов
  21.          //(два знака “*” и два знака “/”) все же не смогла превзойти предельную то в буфер sb помещается комментарий
  22.          m.appendReplacement(sb, gr_0);
  23.        //else
  24.          // иначе комментарий удалется
  25.          // m.appendReplacement(sb, "");
  26.      } catch (Exception e) {
  27.         e.printStackTrace();
  28.      }
  29.    }// end of -- while --
  30.    // завершаем обработку хвоста исходной строки – после последнего совпадения с regexp
  31.    m.appendTail(sb);
  32.  } catch (Exception e) {
  33.     e.printStackTrace();
  34.  }
  35.  sin = sb.toString();

Ближе к делу



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

Вот пример использования:
  1. /*
  2.         Для записи регулярного выражения используется Perl-like синтаксис, когда строка состоит из двух секций
  3.         РАЗДЕЛИТЕЛЬ_1 СЕКЦИЯ_ЧТО_ИСКАТЬ РАЗДЕЛИТЕЛЬ_2 СЕКЦИЯ_МОДИФИКАТОРЫ
  4.         разделители строятся по стандартным правилам и могут быть любыми одинаковыми символами, или парными
  5.         [] и {} и () и <>
  6.         секция модификаторов может состоять из следующих символов:
  7.          i - поиск/замена будет регистронечувствительной CASE_INSENSITIVE
  8.          d - модификатор UNIX_LINES
  9.          x - режим когда игнорируются пробелы и можно комментировать выражение COMMENTS
  10.          m - многострочный режим, в котором символы ^ и $ ассоциируются с началом и концом каждой из строк MULTILINE
  11.          s - режим "точка - это все", в котором символ новой строки ассоциирутся с символом "." DOTALL
  12.          u - включена поддержка Unicode case-folding-га UNICODE_CASE                    
  13.          */
  14.         List<String> rez = new ArrayList<String>();
  15.         List<List<String>> rez2 = new ArrayList<List<String>>();
  16.  
  17.         String pattern = "/(\\w+)-(\\w+)/idxmsu";
  18.         String inputString = "hello-petyano petka-lenka";
  19.  
  20.         // пример с поиском внутри строки всех совпадений
  21.         if (RegexpUtils.preg_match_all(pattern, inputString, rez2)) {
  22.             System.out.println("rez = " + rez);
  23.         }
  24.         // ищем одно, только первое, совпадение
  25.         if (RegexpUtils.preg_match("|(\\w+)-(\\w+)|", inputString, rez)) {
  26.             System.out.println("rez = " + rez);
  27.         }
  28.  
  29.  
  30.         System.out.println("=============================== ");
  31.  
  32.         // теперь пример простой замены
  33.         System.out.println("rez = " + RegexpUtils.preg_replace("/-(\\w+)/i", inputString, "X:$1"));
  34.  
  35.         // и пример сложной замены 
  36.         System.out.println("rez = " + RegexpUtils.preg_replace_callback("/-(\\w+)/i", inputString,
  37.                 new RegexpUtils.Replacer() {
  38.                     public String onMatch(List<String> matches) {
  39.                         return "z:" + matches.get(1);
  40.                     }
  41.                 }
  42.         ));
Вот результат работы:
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

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


  1. package testi.catsandusers;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.HashMap;
  5. import java.util.List;
  6. import java.util.regex.Matcher;
  7. import java.util.regex.Pattern;
  8.  
  9. /**
  10.  * Класс-обертка над стандартными для java средствами работы с регулярными выражениями
  11.  * Вместо классов Pattern, Matcher и циклов используются функции и подход их использования аналогичный php
  12.  */
  13. public class RegexpUtils {
  14.     /**
  15.      * Интерфейс который должен реализовать тот кто хочет выполнить обработку, замену каждого вхождения программно
  16.      */
  17.     public static interface Replacer {
  18.         /**
  19.          * Метод должен вернуть строку на которую будет выполнена замена найденного regexp-ом фрагмента
  20.          *
  21.          * @param matches список с информацией об найденном фрагменте, нулевой элемент списка 
  22.          * содержит весь текст "совпадения"
  23.          *                остальные же элементы 1,2, ... содержат значения для групп внутри регулярного выражения
  24.          * @return
  25.          */
  26.         public String onMatch(List<String> matches);
  27.     }
  28.  
  29.     /**
  30.      * Кэш, в котором хранятся скомпилированные regexp-выражения
  31.      */
  32.     private static HashMap<String, Pattern> cache = new HashMap<String, Pattern>();
  33.  
  34.     /**
  35.      * Очиска кэша скомпилированных regexp-выражений
  36.      */
  37.     public void clearCache() {
  38.         cache.clear();
  39.     }
  40.  
  41.     /**
  42.      * Выполнить поиск в строке шаблона и заменить его на новую величину вычисляемую динамически, пользователем
  43.      *
  44.      * @param pattern шаблон (regexp)
  45.      * @param input   строка, где выполнить поиск
  46.      * @param by      объект Replacer - задает значение на что выполнить замену
  47.      * @return  строка после замены
  48.      */
  49.     public static String preg_replace_callback(String pattern, String input, Replacer by) {
  50.         Pattern p = compile(pattern, false);
  51.         Matcher m = p.matcher(input);
  52.         final int gcount = m.groupCount();
  53.         StringBuffer sb = new StringBuffer();
  54.         ArrayList<String> row = new ArrayList<String>();
  55.  
  56.         while (m.find()) {
  57.             try {
  58.                 row.clear();
  59.                 for (int i = 0; i <= gcount; i++)
  60.                     row.add(m.group(i));
  61.                 m.appendReplacement(sb, by.onMatch(row));
  62.             } catch (Exception e) {
  63.                 e.printStackTrace();
  64.             }
  65.         }//end -- while --
  66.         m.appendTail(sb);
  67.         return sb.toString();
  68.     }
  69.  
  70.     /**
  71.      * Выполнить поиск в строке шаблона и заменить его на новую величину вычисляемую средствами Regexp-выражения
  72.      * @param pattern шаблон (regexp)
  73.      * @param input строка, где выполнить поиск
  74.      * @param by      строка, на которую нужно заменить найденное значение
  75.      * @return  строка после замены
  76.      */
  77.     public static String preg_replace(String pattern, String input, String by) {
  78.         Pattern p = compile(pattern, false);
  79.         Matcher m = p.matcher(input);
  80.         StringBuffer sb = new StringBuffer();
  81.         while (m.find()) {
  82.             try {
  83.                 m.appendReplacement(sb, by);
  84.             } catch (Exception e) {
  85.                 e.printStackTrace();
  86.             }
  87.         }//end -- while --
  88.         m.appendTail(sb);
  89.         return sb.toString();
  90.     }
  91.  
  92.     /**
  93.      * Проверка того ассоциирутся ли строка с шаблоном
  94.      * @param pattern  шаблон (regexp)
  95.      * @param input строка, где выполнить поиск
  96.      * @param rez Список куда будет помещена информация об совпадении:
  97.      * нулевой элемент списка содержит весь текст совпадения
  98.      * 1, 2, ... содержат значения групп
  99.      * @return булево выражение - признак того что ассоциация произошла
  100.      */
  101.     public static boolean preg_match(String pattern, String input, List <String> rez) {
  102.         Pattern p = compile(pattern, true);
  103.         Matcher m = p.matcher(input);
  104.         final int gcount = m.groupCount();
  105.         if (rez != null)
  106.             rez.clear();
  107.         if (m.matches())
  108.             for (int i = 0; i <= gcount; i++) {
  109.                 if (rez != null)
  110.                     rez.add(m.group(i));
  111.             }
  112.         return rez.size() > 0;
  113.     }
  114.  
  115.     /**
  116.      * Проверка того что в строке содержится некоторый шаблон и возвращается список со всеми найденными группами совпадений
  117.      * @param pattern  шаблон (regexp)
  118.      * @param input строка, где выполнить поиск
  119.      * @param rez список, куда будут помещены все найденные соответвия, список двухуровневый: первый уровень
  120.      * содержит перечисление объектов-списков, каждый из которых содержит информацию об 
  121.      * очередном совпадении в таком же формате как и метод preg_match 
  122.      * @return
  123.      */
  124.     public static boolean preg_match_all(String pattern, String input, List <List <String>> rez) {
  125.         Pattern p = compile(pattern, true);
  126.         Matcher m = p.matcher(input);
  127.         final int gcount = m.groupCount();
  128.         if (rez != null)
  129.             rez.clear();
  130.         while (m.find()) {
  131.             ArrayList row = new ArrayList();
  132.             for (int i = 0; i <= gcount; i++) {
  133.                 if (rez != null)
  134.                     row.add(m.group(i));
  135.             }
  136.             if (rez != null)
  137.                 rez.add(row);
  138.         }
  139.         return rez.size() > 0;
  140.     }
  141.  
  142.  
  143.     /**
  144.      * Слежебный метод выполняющий компиляцию regexp-а и сохранение его в кэш
  145.      * @param pattern текст регулярного выражения
  146.      * @param surroundBy признак того нужно ли выражение окружить .*?
  147.      * @return скомпилированный Pattern
  148.      */
  149.     private static Pattern compile(String pattern, boolean surroundBy) {
  150.         if (cache.containsKey(pattern)) return cache.get(pattern);
  151.         final String pattern_orig = pattern;
  152.  
  153.         final char firstChar = pattern.charAt(0);
  154.         char endChar = firstChar;
  155.         if (firstChar == '(') endChar = '}';
  156.         if (firstChar == '[') endChar = ']';
  157.         if (firstChar == '{') endChar = '}';
  158.         if (firstChar == '<') endChar = '>';
  159.  
  160.         int lastPos = pattern.lastIndexOf(endChar);
  161.         if (lastPos == -1)
  162.             throw new RuntimeException("Invalid pattern: " + pattern);
  163.  
  164.         char[] modifiers = pattern.substring(lastPos + 1).toCharArray();
  165.         int mod = 0;
  166.         for (int i = 0; i < modifiers.length; i++) {
  167.             char modifier = modifiers[i];
  168.             switch (modifier) {
  169.                 case 'i':
  170.                     mod |= Pattern.CASE_INSENSITIVE;
  171.                     break;
  172.                 case 'd':
  173.                     mod |= Pattern.UNIX_LINES;
  174.                     break;
  175.                 case 'x':
  176.                     mod |= Pattern.COMMENTS;
  177.                     break;
  178.                 case 'm':
  179.                     mod |= Pattern.MULTILINE;
  180.                     break;
  181.                 case 's':
  182.                     mod |= Pattern.DOTALL;
  183.                     break;
  184.                 case 'u':
  185.                     mod |= Pattern.UNICODE_CASE;
  186.                     break;
  187.             }
  188.         }
  189.         pattern = pattern.substring(1, lastPos);
  190.         if (surroundBy) {
  191.             if (pattern.charAt(0) != '^')
  192.                 pattern = ".*?" + pattern;
  193.             if (pattern.charAt(pattern.length() - 1) != '$')
  194.                 pattern = pattern + ".*?";
  195.         }
  196.  
  197.         final Pattern rezPattern = Pattern.compile(pattern, mod);
  198.         cache.put(pattern_orig, rezPattern);
  199.         return rezPattern;
  200.     }
  201. }
Собственно, написанный мною код - не панацея т.к. есть мелкие отличия которые все-равно приходится помнить, но их уже гораздо меньше и встречаются они гораздо реже.