我们的模块性能之一在很大程度上依赖于我们如何替换字符串中的子字符串。
我们形成一个替换地图"它可以包含超过3500个字符串对,然后我们将StringUtils.replaceEach(text, searchList, replacementList)
应用于大字符串(几个MB)。
键和值都是唯一的,并且在大多数情况下具有相同的字符长度(但它不是我们可以依赖的东西)。
我的任务是否比StringUtils.replaceEach()
更复杂?对replaceEach()
解决的简单替换问题可能有点过分,但在我的"重"情况下。
答案 0 :(得分:5)
您可以使用正则表达式引擎,有效地将键与输入字符串匹配,并替换它们。
首先,使用交替运算符连接所有键,如下所示:
var keys = "keyA|keyB|keyC";
接下来,编译模式:
Pattern pattern = Pattern.compile("(" + keys + ")")
根据输入文本创建匹配器:
Matcher matcher= pattern.matcher(text);
现在,在循环中应用正则表达式,找到文本中的所有键,并使用 appendReplacement (这是一个& #34;内联"字符串替换方法),用相应的值替换所有:
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb,dictionary.get(matcher.group(0)));
}
matcher.appendTail(sb);
你走了。
请注意,这可能看起来有些天真,但实际上regexp引擎已经针对手头的任务进行了大量优化,并且Java regexp实现也允许“内联”#34;替换,一切都很好。
我做了一个小基准测试,通过应用 /usr/share/X11/rgb.txt 中定义的颜色名称列表(~200种不同颜色名称)对& #34;犯罪和惩罚" 由Fyodor Dostoyevsky,我从Project Gutenberg下载(大小约1MB),使用上述技术, 它解决了
比StringUtils.replaceEach快12倍 - 900毫秒vs 10700毫秒
用于后者(不计算模式编译时间)。
P.S。如果您的密钥可能包含字符,对于正则表达式不安全,例如。^ $(),则应在将它们添加到模式之前使用 Pattern.quote()。
旁注:
此方法将按顺序替换 keys ,它们出现在模式列表中,例如" a => 1 | b => 2 | aa => 3" 应用于"欢迎来到bazaar" 将导致"欢迎来到b1z11r" ,而不是"欢迎来到b1z3r" ,如果你想要最长的匹配,你应该按字典顺序对键进行排序,然后再将它们添加到模式中(即" b | aa | a" )。它也适用于您原始的StringUtils.replaceEach()方法。
更新
上述方法应该适用于问题,如原始问题所述,即替换地图的大小与输入数据大小相比(相对)较小。
如果您有一个很长的字典,应用于短文本, 由 StringUtils.replaceEach()完成的线性搜索可能比它快。
我做了一个额外的基准测试,通过应用 10000 随机选择的单词(+4个字符长)的词典来说明:
cat /usr/share/dict/words | grep -E "^.{4,}$" | shuf | head -10000
反对: 1024,2048,4096,8192,16384,32768,65536,131072,262144 和 524288 字符的长片摘录来自同一个"犯罪与惩罚"文本。
结果如下:
text Ta(ms) Tb(ms) Ta/Tb(speed up)
---------------------------------------
1024 99 240 0.4125
2048 43 294 0.1462585
4096 113 721 0.1567267
8192 128 1329 0.0963130
16384 320 2230 0.1434977
32768 2052 3708 0.5533981
65536 6811 6650 1.0242106
131072 32422 12663 2.5603728
262144 150655 23011 6.5470862
524288 614634 29874 20.574211
注意模式字符串长度为 135537 字节(所有10000个键连接在一起)
答案 1 :(得分:0)
首先 - 如果您正在谈论优化,请发布您的分析结果。它是关于应该优化什么的唯一可靠信息来源(参见the Third Rule of Optimization)。
如果您确定字符串操作确实占用了大部分时间,那么请记住两件事。
首先,Java字符串是不可变的。每次调用replace方法时,都会创建一个新字符串,在您的情况下很可能意味着需要大量内存分配。多年来Java已经变得更好了,如果你可以跳过它,那么就去做吧。我已经检查过,StringUtils.replaceEach
确实使用了缓冲区,并且应该具有相对内存效率。此外,特别是使用第二个笔记中的自定义搜索算法,您可以实现自定义解决方案以进行替换。自定义解决方案可能包括创建自己的char缓冲区以进行有效替换,使用StringBuilder
/ StringBuffer
进行替换(您必须跟踪替换的长度,因为调用{{1}在.toString()
上的每次搜索之前,与手动替换字符串一样低效。)
其次,search algorithm本身就是其中之一。我不知道Apache的StringBuffer
使用了哪个,但Java的默认实现并不是最优的。您可以使用separate library for searching。
答案 2 :(得分:0)
html, body {
width:100%;
height:100%;
margin:0;
padding:0;
}
正在使用StringUtils
算法(对于要替换的每个单词,请在输入中进行替换)。当O(n * m)
(要替换的单词数)很小时,这实际上是m
(输入的大小)。
然而,对于"大"要检查的替换次数,您可能最好处理每个输入字,这将在O(n)
时间内完成。
O(n)
在单词边界上拆分不仅可以保留空白(通过不消耗输入),而且可以使代码变得相当简单。
答案 3 :(得分:0)
处理这种情况的最佳方法:将源字符串预编译为代码。 扫描每个源字符串以获取替换密钥;将字符串分解为一系列代码片段,其中包含将键结果插入流中的功能。例如:以下源字符串:
The quick $brown $fox jumped over the $lazy dog.
变为
public StringBuilder quickBrown(Map<String, String> dict) {
StringBuilder sb = new StringBuilder();
sb.append("The quick ");
sb.append(dict.getOrElse("$brown", "brown"));
sb.append(" ");
sb.append(dict.getOrElse("$fox", "fox"));
sb.append(" jumped over the ");
sb.append(dict.getOrElse("$lazy", "lazy");
sb.append(" dog.");
return sb;
}
然后使用要替换的映射字典调用与特定字符串对应的方法。
请注意,通过“scan”和“translate”,我的意思是使用程序生成Java代码,然后根据需要动态加载已编译的类文件。
答案 4 :(得分:0)
此算法的缓慢部分是查找所有匹配项。如果以智能方式完成(即,在临时字符缓冲区中,仅将每个字符最多移位一次),则替换是直截了当的。
所以你的问题真的简化为“多字符串搜索”,这已经是一个研究得很好的问题了。你可以找到方法in this question的一个很好的总结 - 但是一行摘要是“grep做得很好”。
Zeppelin已经为此展示了一个合理的循环 - theano.scan()
行为确保您不会不必要地转移事物(这会将此降低到appendReplacement
)。
答案 5 :(得分:0)
虽然@zeppelin提出的appendReplacement解决方案在“最重的数据”上出乎意料地快得多,但结果却是一张带有更大地图的噩梦。
到目前为止,最好的解决方案是我们所拥有的(StringUtils.replaceEach)和建议内容的组合:
protected BackReplacer createBackReplacer(Map<ReplacementKey, String> replacementMap) {
if (replacementMap.isEmpty()) {
return new BackReplacer() {
@Override
public String backReplace(String str) {
return str;
}
};
}
if (replacementMap.size() > MAX_SIZE_FOR_REGEX) {
final String[] searchStrings = new String[replacementMap.size()];
final String[] replacementStrings = new String[replacementMap.size()];
int counter = 0;
for (Map.Entry<ReplacementKey, String> replacementEntry : replacementMap.entrySet()) {
searchStrings[counter] = replacementEntry.getValue();
replacementStrings[counter] = replacementEntry.getKey().getValue();
counter++;
}
return new BackReplacer() {
@Override
public String backReplace(String str) {
return StringUtils.replaceEach(str, searchStrings, replacementStrings);
}
};
}
final Map<String, String> replacements = new HashMap<>();
StringBuilder patternBuilder = new StringBuilder();
patternBuilder.append('(');
for (Map.Entry<ReplacementKey, String> entry : replacementMap.entrySet()) {
replacements.put(entry.getValue(), entry.getKey().getValue());
patternBuilder.append(entry.getValue()).append('|');
}
patternBuilder.setLength(patternBuilder.length() - 1);
patternBuilder.append(')');
final Pattern pattern = Pattern.compile(patternBuilder.toString());
return new BackReplacer() {
@Override
public String backReplace(String str) {
if (str.isEmpty()) {
return str;
}
StringBuffer sb = new StringBuffer(str.length());
Matcher matcher = pattern.matcher(str);
while (matcher.find()) {
matcher.appendReplacement(sb, replacements.get(matcher.group(0)));
}
matcher.appendTail(sb);
return sb.toString();
}
};
}
StringUtils算法(MAX_SIZE_FOR_REGEX = 0):
type = TIMER,name = *。run,count = 8127,min = 4.239809,max = 4235197.925261, mean = 645.736554,stddev = 47197.97968925558,duration_unit =毫秒
appendReplace算法(MAX_SIZE_FOR_REGEX = 1000000):
type = TIMER,name = * .run,count = 8155,min = 4.374516,max = 7806145.439165999, mean = 1145.757953,stddev = 86668.38562815856,duration_unit =毫秒
混合解决方案(MAX_SIZE_FOR_REGEX = 5000):
type = TIMER,name = *。run,count = 8155,min = 3.5862789999999998,max = 376242.25076799997, mean = 389.68986564688714,stddev = 11733.9997814448,duration_unit =毫秒
我们的数据:
type=HISTOGRAM, name=initialValueLength, count=569549, min=0, max=6352327, mean=6268.940661478599, stddev=198123.040651236, median=12.0, p75=16.0, p95=32.0, p98=854.0, p99=1014.5600000000013, p999=6168541.008000023
type=HISTOGRAM, name=replacementMap.size, count=8155, min=0, max=65008, mean=73.46108949416342, stddev=2027.471388983965, median=4.0, p75=7.0, p95=27.549999999999955, p98=55.41999999999996, p99=210.10000000000036, p999=63138.68900000023
此更改将StringUtils.replaceEach中的时间花费减少了一半,并且在我们的模块中提供了25%的性能提升,这主要是IO限制。