2008-12-17 15 views
33

Sono in procinto di spostare un'applicazione da PHP a Java e vi è un uso intenso delle espressioni regolari nel codice. Ho eseguito in qualcosa in PHP che non sembrano avere un java equivalente:Java equivalente al preg_replace_callback di PHP

preg_replace_callback() 

Per ogni soddisfazione della regex, si chiama una funzione che viene passato il testo partita come parametro. Ad esempio l'utilizzo:

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

Quale sarebbe il modo ideale per farlo in Java?

risposta

22

IMPORTANTE: Come sottolineato da Kip nei commenti, questa classe ha un bug ciclo infinito se l'espressione regolare corrispondenza corrisponda sulla stringa di sostituzione. Lo lascerò come esercizio ai lettori per sistemarlo, se necessario.


Non so nulla di simile che sia incorporato in Java. Si potrebbe rotolare il proprio, senza troppe difficoltà, utilizzando la classe 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); 
     } 
    } 
} 

Quindi chiamare:

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

noti che è possibile ottenere l'intera stringa corrispondente chiamando matchResults.group() o matchResults.group(0), quindi non è necessario passare la richiamata allo stato attuale della stringa.

MODIFICA: Sembra che assomigli più all'esatta funzionalità della funzione PHP.

Ecco l'originale, dal momento che il richiedente è piaciuto:

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

Per questo particolare caso d'uso, potrebbe essere meglio fare la fila semplicemente ogni partita nella richiamata, poi dopo correre attraverso di loro indietro. Ciò impedirà di rimappare gli indici man mano che la stringa viene modificata.

+0

Io in realtà come la vostra risposta originale meglio con la coda la stringa e gli indici restituita. Quindi applicandole al contrario. In questo modo è più semplice, ma sembra fare più lavoro, dovendo ripetere la scansione dell'intera stringa per ogni partita. Grazie per il suggerimento! – Mike

+0

Ho aggiunto di nuovo il suggerimento originale. Le dimensioni di input previste farebbero la differenza se la scansione o la messa in coda e la sostituzione sarebbero state più efficaci. Suppongo che si possa anche avere il metodo di sostituzione in coda, insieme alla stringa di sostituzione ... – jdmichal

+0

Errr ... Misspoke. Ovviamente l'accodamento è sempre più efficace in termini di tempo della CPU. La differenza sarebbe se si tratta di un problema abbastanza grande di cui preoccuparsi. – jdmichal

-1

Ecco il risultato finale di ciò che ho fatto con il tuo suggerimento. Ho pensato che sarebbe stato bello avere qui, nel caso qualcuno avesse lo stesso problema. Il codice risultante chiamante appare come:

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

L'intero elenco classe segue:

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

Non vorrei utilizzare una variabile di istanza per memorizzare la richiamata, ma piuttosto passarla come parametro. La sua memorizzazione come variabile di istanza fa sì che la classe abbia un comportamento imprevisto quando viene chiamata da thread separati nello stesso momento. (Il secondo callback otterrà le corrispondenze dal primo al secondo). – jdmichal

51

cercando di emulare funzione di callback di PHP sembra un sacco di lavoro, quando si potrebbe utilizzare appendReplacement() e appendTail () in un ciclo:

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

Penso che alcune classi JDK abbiano potenti funzionalità, ma a volte queste funzionalità sono nascoste dietro nomi di classi strani o nomi di metodi strani ... Anche se la strategia 'appendReplacement/appendTail', come qui usata, richiede meno codice, la strategia' callback' (La risposta scelta dall'OP) è più chiara, più ovvia! – Stephan

+0

Cosa succede se ho bisogno di una stringa abbinata per ottenere la giusta sostituzione? Supponiamo che subjectString contenga "foo bar", ma devo sostituire "foo" di "Jan" e "bar" di "Goyvaerts"? – ALOToverflow

+0

Usa 'foo | bar' come espressione regolare e cerca' regexMatcher.group() 'all'interno del ciclo per vedere quale sostituzione devi aggiungere. –

0

ho trovato risposta che di jdmichal sarebbe ciclo infinito se il tuo stringa restituita potrebbe essere abbinato di nuovo; di seguito è una modifica che impedisce loop infiniti da questa corrispondenza.

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

Non ero abbastanza soddisfatto con nessuna delle soluzioni qui. Volevo una soluzione senza stato. E non volevo finire in un ciclo infinito se la mia stringa sostitutiva corrispondeva al modello. Mentre ero a esso ho aggiunto il supporto per un parametro limit e un parametro restituito count.(Ho usato un AtomicInteger per simulare il passaggio di un intero per riferimento.) Ho spostato il parametro callback alla fine dell'elenco dei parametri, per semplificare la definizione di una classe anonima.

Ecco un esempio di utilizzo:

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

Ed ecco la mia versione di classe 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(); 
} 

Esempio di utilizzo:

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

produrrà il valore di ritorno:

un cane due cani nel cortile