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