Java эквивалент PHP preg_replace_callback для preg_replace_callback
-
22-08-2019 - |
Вопрос
Я нахожусь в процессе переноса приложения с PHP на Java, и в коде активно используются регулярные выражения.Я наткнулся на что-то в PHP, что, похоже, не имеет эквивалента java:
preg_replace_callback()
Для каждого совпадения в регулярном выражении он вызывает функцию, которой передается текст совпадения в качестве параметра.В качестве примера использования:
$articleText = preg_replace_callback("/\[thumb(\d+)\]/",'thumbReplace', $articleText);
# ...
function thumbReplace($matches) {
global $photos;
return "<img src=\"thumbs/" . $photos[$matches[1]] . "\">";
}
Каков был бы идеальный способ сделать это на Java?
Решение
ВАЖНЫЙ:Как указано в Кип в комментариях этот класс имеет ошибку бесконечного цикла, если совпадающее регулярное выражение совпадает с заменяющей строкой.Я оставлю это в качестве упражнения для читателей, чтобы они исправили это, если необходимо.
Я не знаю ничего подобного, что было бы встроено в Java.Вы могли бы создать свой собственный без особых трудностей, используя класс 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);
}
}
}
Тогда звони:
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)
, поэтому нет необходимости передавать при обратном вызове текущее состояние строки.
Редактировать: Сделал это более похожим на точную функциональность 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());
}
}
}
Для этого конкретного варианта использования, возможно, было бы лучше просто поставить каждое совпадение в очередь обратного вызова, а затем выполнить их в обратном порядке.Это предотвратит необходимость переназначения индексов по мере изменения строки.
Другие советы
Попытка эмулировать функцию обратного вызова 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);
Я не был вполне удовлетворен ни одним из предложенных здесь решений.Мне нужно было решение без гражданства.И я не хотел оказаться в бесконечном цикле, если моя строка замены случайно совпадет с шаблоном.Пока я занимался этим, я добавил поддержку для 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();
}
}
Я обнаружил, что ответ 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;
}
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")
выдаст возвращаемое значение:
одна собака, две собаки во дворе
Вот окончательный результат того, что я сделал с вашим предложением.Я подумал, что было бы неплохо иметь его здесь на случай, если у кого-то возникнет такая же проблема.Результирующий вызывающий код выглядит следующим образом:
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);
}
}