2008-12-17 13 views
33

Estoy en el proceso de mover una aplicación de PHP a Java y hay un uso intensivo de expresiones regulares en el código. Me he encontrado con algo en PHP que no parecen tener un equivalente Java:Java equivalente a preg_replace_callback de PHP

preg_replace_callback() 

Para cada partido en la expresión regular, se llama a una función que se pasa el texto partido como parámetro. Como ejemplo de uso:

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

¿Cuál sería la forma ideal de hacer esto en Java?

Respuesta

22

IMPORTANTE: Como se señala en Kip en los comentarios, esta clase tiene un error de bucle infinito si la expresión regular coincidente coincide con la cadena de reemplazo. Lo dejaré como un ejercicio para que los lectores lo arreglen, si es necesario.


No conozco nada similar que esté incorporado en Java. Se podría rodar su propia sin demasiada dificultad, utilizando la clase Matcher:

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); 
     } 
    } 
} 

A continuación, llame:

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); 

Tenga en cuenta que se puede obtener toda la cadena coincidente llamando matchResults.group() o matchResults.group(0), así que no es necesario para pasar la devolución de llamada al estado actual de la cadena.

EDIT: Hace que se parezca más a la funcionalidad exacta de la función de PHP.

Aquí está el original, ya que el autor de la pregunta que le gusta:

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()); 
     } 
    } 
} 

Para este caso en particular, podría ser mejor simplemente Cola cada partido en la devolución de llamada, a continuación, después correr a través de ellos hacia atrás. Esto evitará tener que volver a correlacionar índices a medida que se modifique la cadena.

+0

De hecho, me gusta su respuesta original mejor con la cola de la cadena y los índices devuelto. Luego aplicándolos en reversa. De esta manera es más simple, pero parece hacer más trabajo, tener que volver a examinar toda la cadena para cada coincidencia. ¡Gracias por la sugerencia! – Mike

+0

He añadido la sugerencia original nuevamente. El tamaño de entrada esperado marcaría la diferencia en cuanto a si volver a escanear o poner en cola y luego reemplazar sería más efectivo. Supongo que uno también podría hacer que el método de reemplazo los ponga en cola, junto con la cadena de reemplazo ... – jdmichal

+0

Errr ... Misspoke. Obviamente, las colas siempre son más efectivas en lo que respecta al tiempo de CPU. La diferencia sería si es un problema lo suficientemente grande como para preocuparse. – jdmichal

-1

Aquí está el resultado final de lo que hice con su sugerencia. Pensé que sería bueno tener aquí en caso de que alguien tenga el mismo problema. El código resultante llamando parece:

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

toda la lista de clases sigue:

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

No usaría una variable de instancia para almacenar la devolución de llamada, sino pasarla como parámetro. Almacenarlo como una variable de instancia hace que su clase tenga un comportamiento inesperado cuando se llama desde hilos separados al mismo tiempo. (La segunda devolución de llamada obtendrá coincidencias entre la primera y la segunda). – jdmichal

51

Tratando de emular función de devolución de llamada de PHP parece una gran cantidad de trabajo cuando usted podría utilizar appendReplacement() y appendTail () en un bucle:

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

Creo que algunas clases JDK tienen poderosas funciones, pero esas características a veces se ocultan detrás de nombres de clases extrañas o nombres de métodos extraños ... Aunque la estrategia 'appendReplacement/appendTail', como se usa aquí, requiere menos código, la estrategia' callback' (La respuesta elegida de OP) es más clara, más obvia. – Stephan

+0

¿Qué pasa si necesito una cadena coincidente para obtener el reemplazo correcto? Say subjectString might contains "foo bar" pero necesito reemplazar "foo" por "Jan" y "bar" por "Goyvaerts"? – ALOToverflow

+0

Use 'foo | bar' como su expresión regular y consulte' regexMatcher.group() 'dentro del ciclo para ver qué reemplazo necesita agregar. –

0

me encontré con la respuesta de que se jdmichal bucle infinito si su cadena devuelta podría ser igualado de nuevo; a continuación hay una modificación que evita bucles infinitos de esta coincidencia.

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

No estaba del todo satisfecho con ninguna de las soluciones aquí. Yo quería una solución sin estado. Y no quería terminar en un bucle infinito si mi cadena de reemplazo coincidía con el patrón. Mientras estaba en eso, agregué soporte para un parámetro limit y un parámetro count devuelto.(Utilicé un AtomicInteger para simular el paso de un número entero por referencia.) Moví el parámetro callback al final de la lista de parámetros, para que sea más fácil definir una clase anónima.

Aquí es un ejemplo de uso:

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); 

Y aquí es mi versión de la clase 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(); 
} 

Ejemplo de uso:

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

producirá el valor de retorno:

un perro dos perros en el patio

Cuestiones relacionadas