2008-12-17 2 views
33

Я в процессе перемещения приложения с PHP на Java, и в коде используется интенсивное использование регулярных выражений. Я перебежать что-то в PHP, который, кажется, не имеют Java эквивалент:Java-эквивалент PHP preg_replace_callback

preg_replace_callback() 

Для каждого матча в регулярном выражении, она вызывает функцию, которая передается текст матча в качестве параметра. В качестве примера используется:

$articleText = preg_replace_callback("/\[thumb(\d+)\]/",'thumbReplace', $articleText); 
# ... 
function thumbReplace($matches) { 
    global $photos; 
    return "<img src=\"thumbs/" . $photos[$matches[1]] . "\">"; 
} 

Что было бы идеальным способом сделать это на Java?

ответ

22

ВАЖНО: Как было отмечено Kip в комментариях, этот класс имеет бесконечную ошибку петли, если соответствующий регулярное выражение соответствует на строку замены. Я оставлю это упражнением для читателей, чтобы исправить его, если это необходимо.


Я не знаю ничего подобного, что встроено в Java. Вы можете свернуть свой собственный без особых трудностей, с помощью класса Сличитель:

import java.util.regex.*; 

public class CallbackMatcher 
{ 
    public static interface Callback 
    { 
     public String foundMatch(MatchResult matchResult); 
    } 

    private final Pattern pattern; 

    public CallbackMatcher(String regex) 
    { 
     this.pattern = Pattern.compile(regex); 
    } 

    public String replaceMatches(String string, Callback callback) 
    { 
     final Matcher matcher = this.pattern.matcher(string); 
     while(matcher.find()) 
     { 
      final MatchResult matchResult = matcher.toMatchResult(); 
      final String replacement = callback.foundMatch(matchResult); 
      string = string.substring(0, matchResult.start()) + 
        replacement + string.substring(matchResult.end()); 
      matcher.reset(string); 
     } 
    } 
} 

Тогда звоните:

final CallbackMatcher.Callback callback = new CallbackMatcher.Callback() { 
    public String foundMatch(MatchResult matchResult) 
    { 
     return "<img src=\"thumbs/" + matchResults.group(1) + "\"/>"; 
    } 
}; 

final CallbackMatcher callbackMatcher = new CallbackMatcher("/\[thumb(\d+)\]/"); 
callbackMatcher.replaceMatches(articleText, callback); 

Обратите внимание, что вы можете получить всю найденную строку с помощью вызова matchResults.group() или matchResults.group(0), так что это не необходимо передать обратный вызов текущему состоянию строки.

EDIT: Это больше похоже на точную функциональность функции PHP.

Вот оригинал, так как просящие понравилась:

public class CallbackMatcher 
{ 
    public static interface Callback 
    { 
     public void foundMatch(MatchResult matchResult); 
    } 

    private final Pattern pattern; 

    public CallbackMatcher(String regex) 
    { 
     this.pattern = Pattern.compile(regex); 
    } 

    public String findMatches(String string, Callback callback) 
    { 
     final Matcher matcher = this.pattern.matcher(string); 
     while(matcher.find()) 
     { 
      callback.foundMatch(matcher.toMatchResult()); 
     } 
    } 
} 

Для этого конкретного случая использования, это может быть лучше просто очереди каждого матча в обратном вызове, то после запуска через них в обратном направлении. Это предотвратит необходимость переназначения индексов при изменении строки.

+0

Я на самом деле нравится ваш первоначальный ответ лучше с очередями возвращенную строку и индексы. Затем применяя их в обратном порядке. Этот способ проще, но, похоже, делает больше работы, требуя повторной проверки всей строки для каждого совпадения. Спасибо за предложение! – Mike

+0

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

+0

Errr ... Misspoke. Очевидно, что очередность всегда более эффективна в отношении времени процессора. Разница в том, будет ли это достаточно большой проблемой, о которой нужно беспокоиться. – jdmichal

-1

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

content = ReplaceCallback.find(content, regex, new ReplaceCallback.Callback() { 
    public String matches(MatchResult match) { 
     // Do something special not normally allowed in regex's... 
     return "newstring" 
    } 
}); 

Весь класс перечисление следующим образом:

import java.util.regex.MatchResult; 
import java.util.regex.Pattern; 
import java.util.regex.Matcher; 
import java.util.Stack; 

/** 
* <p> 
* Class that provides a method for doing regular expression string replacement by passing the matched string to 
* a function that operates on the string. The result of the operation is then used to replace the original match. 
* </p> 
* <p>Example:</p> 
* <pre> 
* ReplaceCallback.find("string to search on", "/regular(expression/", new ReplaceCallback.Callback() { 
*  public String matches(MatchResult match) { 
*   // query db or whatever... 
*   return match.group().replaceAll("2nd level replacement", "blah blah"); 
*  } 
* }); 
* </pre> 
* <p> 
* This, in effect, allows for a second level of string regex processing. 
* </p> 
* 
*/ 
public class ReplaceCallback { 
    public static interface Callback { 
     public String matches(MatchResult match); 
    } 

    private final Pattern pattern; 
    private Callback callback; 

    private class Result { 
     int start; 
     int end; 
     String replace; 
    } 

    /** 
    * You probably don't need this. {@see find(String, String, Callback)} 
    * @param regex  The string regex to use 
    * @param callback An instance of Callback to execute on matches 
    */ 
    public ReplaceCallback(String regex, final Callback callback) { 
     this.pattern = Pattern.compile(regex); 
     this.callback = callback; 
    } 

    public String execute(String string) { 
     final Matcher matcher = this.pattern.matcher(string); 
     Stack<Result> results = new Stack<Result>(); 
     while(matcher.find()) { 
      final MatchResult matchResult = matcher.toMatchResult(); 
      Result r = new Result(); 
      r.replace = callback.matches(matchResult); 
      if(r.replace == null) 
       continue; 
      r.start = matchResult.start(); 
      r.end = matchResult.end(); 
      results.push(r); 
     } 
     // Improve this with a stringbuilder... 
     while(!results.empty()) { 
      Result r = results.pop(); 
      string = string.substring(0, r.start) + r.replace + string.substring(r.end); 
     } 
     return string; 
    } 

    /** 
    * If you wish to reuse the regex multiple times with different callbacks or search strings, you can create a 
    * ReplaceCallback directly and use this method to perform the search and replace. 
    * 
    * @param string The string we are searching through 
    * @param callback A callback instance that will be applied to the regex match results. 
    * @return The modified search string. 
    */ 
    public String execute(String string, final Callback callback) { 
     this.callback = callback; 
     return execute(string); 
    } 

    /** 
    * Use this static method to perform your regex search. 
    * @param search The string we are searching through 
    * @param regex  The regex to apply to the string 
    * @param callback A callback instance that will be applied to the regex match results. 
    * @return The modified search string. 
    */ 
    public static String find(String search, String regex, Callback callback) { 
     ReplaceCallback rc = new ReplaceCallback(regex, callback); 
     return rc.execute(search); 
    } 
} 
+0

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

51

Пытаясь подражать функции обратного вызова PHP кажется очень много работы, когда вы могли бы просто использовать appendReplacement() и appendTail () в цикле:

StringBuffer resultString = new StringBuffer(); 
Pattern regex = Pattern.compile("regex"); 
Matcher regexMatcher = regex.matcher(subjectString); 
while (regexMatcher.find()) { 
    // You can vary the replacement text for each match on-the-fly 
    regexMatcher.appendReplacement(resultString, "replacement"); 
} 
regexMatcher.appendTail(resultString); 
+3

Я думаю, что некоторые классы JDK имеют мощные функции, но эти функции иногда скрываются за странными именами классов или странными именами методов ... Хотя стратегия 'appendReplacement/appendTail', используемая здесь, требует меньше кода, стратегии callback (Выбранный ответ OP) яснее, более очевидным! – Stephan

+0

Что делать, если мне нужно совместить строку, чтобы получить правильную замену? Скажем, subjectString может содержать «foo bar», но мне нужно заменить «foo» на «Jan» и «bar» на «Goyvaerts»? – ALOToverflow

+0

Используйте 'foo | bar' в качестве вашего регулярного выражения и запрос' regexMatcher.group() 'внутри цикла, чтобы увидеть, какую замену вам нужно добавить. –

0

Я нашел ответ, что jdmichal был бы бесконечный цикл, если ваша возвращаемая строка может быть сопоставлен снова; ниже - это модификация, которая предотвращает бесконечные петли от этого соответствия.

public String replaceMatches(String string, Callback callback) { 
    String result = ""; 
    final Matcher matcher = this.pattern.matcher(string); 
    int lastMatch = 0; 
    while(matcher.find()) 
    { 
     final MatchResult matchResult = matcher.toMatchResult(); 
     final String replacement = callback.foundMatch(matchResult); 
     result += string.substring(lastMatch, matchResult.start()) + 
      replacement; 
     lastMatch = matchResult.end(); 
    } 
    if (lastMatch < string.length()) 
     result += string.substring(lastMatch); 
    return result; 
} 
3

Я не был полностью удовлетворен любым решением здесь. Мне нужно решение без гражданства. И я не хотел заканчиваться бесконечным циклом, если бы моя строка замены соответствовала шаблону. Пока я был у него, я добавил поддержку параметра limit и возвращаемого параметра count.(Я использовал AtomicInteger для имитации передачи целого по ссылке.) Я переместил параметр callback в конец списка параметров, чтобы упростить определение анонимного класса.

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

final Map<String,String> props = new HashMap<String,String>(); 
props.put("MY_NAME", "Kip"); 
props.put("DEPT", "R&D"); 
props.put("BOSS", "Dave"); 

String subjectString = "Hi my name is ${MY_NAME} and I work in ${DEPT} for ${BOSS}"; 
String sRegex = "\\$\\{([A-Za-z0-9_]+)\\}"; 

String replacement = ReplaceCallback.replace(sRegex, subjectString, new ReplaceCallback.Callback() { 
    public String matchFound(MatchResult match) { 
    String group1 = match.group(1); 
    if(group1 != null && props.containsKey(group1)) 
     return props.get(group1); 
    return match.group(); 
    } 
}); 

System.out.println("replacement: " + replacement); 

А вот моя версия класса ReplaceCallback:

import java.util.concurrent.atomic.AtomicInteger; 
import java.util.regex.*; 

public class ReplaceCallback 
{ 
    public static interface Callback { 
    /** 
    * This function is called when a match is made. The string which was matched 
    * can be obtained via match.group(), and the individual groupings via 
    * match.group(n). 
    */ 
    public String matchFound(MatchResult match); 
    } 

    /** 
    * Replaces with callback, with no limit to the number of replacements. 
    * Probably what you want most of the time. 
    */ 
    public static String replace(String pattern, String subject, Callback callback) 
    { 
    return replace(pattern, subject, -1, null, callback); 
    } 

    public static String replace(String pattern, String subject, int limit, Callback callback) 
    { 
    return replace(pattern, subject, limit, null, callback); 
    } 

    /** 
    * @param regex The regular expression pattern to search on. 
    * @param subject The string to be replaced. 
    * @param limit The maximum number of replacements to make. A negative value 
    *     indicates replace all. 
    * @param count If this is not null, it will be set to the number of 
    *     replacements made. 
    * @param callback Callback function 
    */ 
    public static String replace(String regex, String subject, int limit, 
      AtomicInteger count, Callback callback) 
    { 
    StringBuffer sb = new StringBuffer(); 
    Matcher matcher = Pattern.compile(regex).matcher(subject); 
    int i; 
    for(i = 0; (limit < 0 || i < limit) && matcher.find(); i++) 
    { 
     String replacement = callback.matchFound(matcher.toMatchResult()); 
     replacement = Matcher.quoteReplacement(replacement); //probably what you want... 
     matcher.appendReplacement(sb, replacement); 
    } 
    matcher.appendTail(sb); 

    if(count != null) 
     count.set(i); 
    return sb.toString(); 
    } 
} 
0
public static String replace(Pattern pattern, Function<MatchResult, String> callback, CharSequence subject) { 
    Matcher m = pattern.matcher(subject); 
    StringBuffer sb = new StringBuffer(); 
    while (m.find()) { 
     m.appendReplacement(sb, callback.apply(m.toMatchResult())); 
    } 
    m.appendTail(sb); 
    return sb.toString(); 
} 

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

replace(Pattern.compile("cat"), mr -> "dog", "one cat two cats in the yard") 

произведет возвращаемое значение:

одна собака две собаки во дворе