如何优化字符串替换:四种实现方案对比与性能分析
问题背景
我们在处理商品名称时,常常需要去掉一些不需要的关键词。
例如:
原商品名:
HUAWEI Pura 70 Pro 国家补贴500元 羽砂黑 12GB+512GB 超高速风驰闪拍 华为鸿蒙智能手机
希望替换后:
HUAWEI Pura 70 Pro 羽砂黑 12GB+512GB 超高速风驰闪拍 华为鸿蒙智能手机
替换掉的关键词是:“国家补贴500元”。
你可能会说:“这很简单嘛,用 skuName.replace("国家补贴500元", "")
不就搞定了?”
确实,当关键词不多时这样完全可以。
但如果我们要处理的是几百上千个关键词,就不那么简单了!
很多项目上线后,用 String.replace
处理大量关键词会瞬间把CPU打满,程序几乎卡死,根本无法用。
其实这和“敏感词过滤”是一个道理。为了解决这个问题,就需要用更高效的算法,比如下面介绍的 Aho-Corasick 自动机算法(简称 AC 自动机)。
解决方案
我们设计了一个统一的接口 Replacer
,定义了 replaceKeywords(String text)
方法,便于对不同替换方案进行比较。
public interface Replacer {String replaceKeywords(String text);
}
下面我们介绍四种方案。
1 最简单的 String.replace
方案
这是最基础的办法,也就是把每个关键词依次替换掉。为了避免“短关键词”干扰“长关键词”,我们先把关键词按长度从长到短排序。
public class StrReplacer implements Replacer {private final List<String> keyWordList;public StrReplacer(String keyWords) {this.keyWordList = Arrays.asList(keyWords.split(";"));keyWordList.sort((a, b) -> b.length() - a.length());}@Overridepublic String replaceKeywords(String text) {String result = text;for (String keyword : keyWordList) {result = result.replace(keyword, "");}return result;}
}
这个方法适用于关键词不多的场景,简单有效。
2 使用正则表达式优化替换
其实 String.replace()
背后也是在用正则表达式。我们也可以自己构造一个正则表达式,一次性替换所有关键词。
public class PatternReplacer implements Replacer {private final Pattern pattern;public PatternReplacer(String keyWords) {List<String> keywords = Arrays.asList(keyWords.split(";"));keywords.sort((a, b) -> b.length() - a.length());String regex = keywords.stream().map(Pattern::quote).collect(Collectors.joining("|"));this.pattern = Pattern.compile(regex);}@Overridepublic String replaceKeywords(String text) {return pattern.matcher(text).replaceAll("");}
}
这个方法适用于关键词数量中等的情况,性能比循环调用 replace()
稍好。
3 使用 Aho-Corasick(AC 自动机)算法
这是一个经典的多关键词匹配算法,可以在一次遍历中找出所有匹配的关键词,效率非常高。
我们使用现成的库:
<dependency><groupId>org.ahocorasick</groupId><artifactId>ahocorasick</artifactId><version>0.6.3</version>
</dependency>
实现如下:
public class AhoCorasickReplacer implements Replacer {private final Trie trie;public AhoCorasickReplacer(String keyWords) {Trie.TrieBuilder builder = Trie.builder().ignoreOverlaps().onlyWholeWords();for (String keyword : keyWords.split(";")) {builder.addKeyword(keyword);}this.trie = builder.build();}@Overridepublic String replaceKeywords(String text) {StringBuilder result = new StringBuilder();int lastEnd = 0;for (Emit emit : trie.parseText(text)) {result.append(text, lastEnd, emit.getStart());lastEnd = emit.getEnd() + 1;}result.append(text.substring(lastEnd));return result.toString();}
}
这适用于关键词很多的高性能场景,是实际项目中非常推荐的方式。
4 自己手写 Trie 树实现替换
Trie 树(字典树)是一种结构简单的前缀树,可以快速查找前缀匹配的字符串。
图片位置(插图说明 Trie 树结构)
比如:
- root → c → a → t 代表单词 “cat”
- root → d → o → g 代表单词 “dog”
我们实现的代码只做“替换”功能,性能非常好:
public class TrieKeywordReplacer implements Replacer {private final Trie trie;public TrieKeywordReplacer(String keyWords) {trie = new Trie();for (String s : keyWords.split(";")) {trie.insert(s);}}@Overridepublic String replaceKeywords(String text) {return trie.replaceKeywords(text, "");}static class Trie {private final TrieNode root = new TrieNode();public void insert(String word) {TrieNode node = root;for (char c : word.toCharArray()) {node.children.putIfAbsent(c, new TrieNode());node = node.children.get(c);}node.isEndOfWord = true;}public String replaceKeywords(String text, String replacement) {StringBuilder result = new StringBuilder();int i = 0;while (i < text.length()) {TrieNode node = root;int j = i, lastMatch = -1;while (j < text.length() && node.children.containsKey(text.charAt(j))) {node = node.children.get(text.charAt(j));if (node.isEndOfWord) lastMatch = j;j++;}if (lastMatch >= 0) {result.append(replacement);i = lastMatch + 1;} else {result.append(text.charAt(i++));}}return result.toString();}}static class TrieNode {Map<Character, TrieNode> children = new HashMap<>();boolean isEndOfWord = false;}
}
内存占用比较
替换类 | 对象大小(单位:字节) |
---|---|
StrReplacer | 12,560 |
PatternReplacer | 21,592 |
TrieKeywordReplacer | 184,944 |
AhoCorasickReplacer | 253,896 |
可以看出,自己实现的 Trie 占用较小,AC 自动机功能更强,占用更多内存。
性能测试结果(关键词数量 400,JDK 1.8)
场景 | StrReplacer | PatternReplacer | TrieKeywordReplacer | AhoCorasickReplacer |
---|---|---|---|---|
单线程 1w 次 | 21843 ns | 28846 ns | 532 ns | 727 ns |
2线程,有1个命中关键词 | 23444 ns | 39984 ns | 680 ns | 1157 ns |
2线程,有20个命中关键词 | 252738 ns | 114740 ns | 33900 ns | 113764 ns |
2线程,无关键词匹配 | 22248 ns | 9253 ns | 397 ns | 738 ns |
总结
- 如果关键词很少,用
String.replace
最方便。 - 如果关键词较多,推荐使用 AC 自动机(如 AhoCorasick)或自己实现 Trie。
- 自己实现的 Trie 替换性能最强,因为它只做了“替换”一件事,轻量、高效。