1. 概述
在本教程中,我们将介绍几种在 Java 中从字符串中移除停用词(stopwords)的方法。这个操作在很多实际场景中非常有用,例如我们希望从用户评论、论坛发帖等内容中过滤掉一些无意义或禁止使用的词汇。
我们会分别使用手动遍历、Collection.removeAll()
和正则表达式来实现这一功能,并通过 JMH 进行性能对比。
2. 加载停用词列表
首先,我们需要从文本文件中加载停用词列表。
我们假设有一个名为 english_stopwords.txt
的文件,其中包含了我们希望过滤掉的单词,比如 I, he, she, the 等。
我们可以通过 Files.readAllLines()
方法将这些停用词加载到一个 List<String>
中:
@BeforeClass
public static void loadStopwords() throws IOException {
stopwords = Files.readAllLines(Paths.get("english_stopwords.txt"));
}
3. 手动遍历移除停用词
第一种方法很简单粗暴:我们对字符串中的每个单词进行遍历,并检查是否为停用词,如果不是则保留。
@Test
public void whenRemoveStopwordsManually_thenSuccess() {
String original = "The quick brown fox jumps over the lazy dog";
String target = "quick brown fox jumps lazy dog";
String[] allWords = original.toLowerCase().split(" ");
StringBuilder builder = new StringBuilder();
for(String word : allWords) {
if(!stopwords.contains(word)) {
builder.append(word);
builder.append(' ');
}
}
String result = builder.toString().trim();
assertEquals(result, target);
}
✅ 优点:逻辑清晰,易于理解
❌ 缺点:效率不高,每次都要调用 contains()
,时间复杂度是 O(n*m)
4. 使用 Collection.removeAll()
第二种方法使用 Java 集合类的 removeAll()
方法,一次性移除所有匹配的停用词,不需要手动遍历。
@Test
public void whenRemoveStopwordsUsingRemoveAll_thenSuccess() {
ArrayList<String> allWords =
Stream.of(original.toLowerCase().split(" "))
.collect(Collectors.toCollection(ArrayList<String>::new));
allWords.removeAll(stopwords);
String result = allWords.stream().collect(Collectors.joining(" "));
assertEquals(result, target);
}
我们先将字符串按空格分割成单词列表,然后转换为 ArrayList
,最后调用 removeAll()
方法进行过滤。
✅ 优点:代码简洁,语义清晰
✅ 缺点:需要额外创建集合,内存占用略高
5. 使用正则表达式
第三种方法使用正则表达式,将整个停用词列表转换为一个正则模式,然后通过字符串替换来移除所有匹配的词。
@Test
public void whenRemoveStopwordsUsingRegex_thenSuccess() {
String stopwordsRegex = stopwords.stream()
.collect(Collectors.joining("|", "\\b(", ")\\b\\s?"));
String result = original.toLowerCase().replaceAll(stopwordsRegex, "");
assertEquals(result, target);
}
我们使用 Stream
将停用词拼接成类似 \\b(word1|word2|...)\\b\\s?
的正则表达式,其中:
\b
表示单词边界,防止匹配到单词中间的部分(例如避免将 "he" 从 "heat" 中误删)\s?
表示匹配可选的空格,这样在删除停用词后不会留下多余的空格
✅ 优点:一行代码搞定,写法非常简洁
❌ 缺点:正则性能较差,尤其在停用词较多时明显拖慢处理速度
6. 性能对比
为了找出哪种方法性能最好,我们使用 JMH(Java Microbenchmark Harness)进行基准测试。
我们使用一个较大的文本文件 shakespeare-hamlet.txt
作为测试数据源,并分别对三种方法进行测试。
6.1 测试准备
@Setup
public void setup() throws IOException {
data = new String(Files.readAllBytes(Paths.get("shakespeare-hamlet.txt")));
data = data.toLowerCase();
stopwords = Files.readAllLines(Paths.get("english_stopwords.txt"));
stopwordsRegex = stopwords.stream()
.collect(Collectors.joining("|", "\\b(", ")\\b\\s?"));
}
6.2 测试方法
✅ 手动遍历
@Benchmark
public String removeManually() {
String[] allWords = data.split(" ");
StringBuilder builder = new StringBuilder();
for(String word : allWords) {
if(!stopwords.contains(word)) {
builder.append(word);
builder.append(' ');
}
}
return builder.toString().trim();
}
✅ removeAll 方法
@Benchmark
public String removeAll() {
ArrayList<String> allWords =
Stream.of(data.split(" "))
.collect(Collectors.toCollection(ArrayList<String>::new));
allWords.removeAll(stopwords);
return allWords.stream().collect(Collectors.joining(" "));
}
✅ 正则表达式方法
@Benchmark
public String replaceRegex() {
return data.replaceAll(stopwordsRegex, "");
}
6.3 测试结果
Benchmark Mode Cnt Score Error Units
removeAll avgt 60 7.782 ± 0.076 ms/op
removeManually avgt 60 8.186 ± 0.348 ms/op
replaceRegex avgt 60 42.035 ± 1.098 ms/op
⚠️ 结论:removeAll()
是最快的,而使用正则表达式则是最慢的。
7. 总结
我们介绍了三种从 Java 字符串中移除停用词的方法:
方法 | 优点 | 缺点 |
---|---|---|
✅ 手动遍历 | 逻辑清晰 | 性能较差 |
✅ removeAll() |
简洁高效 | 需要构建集合 |
✅ 正则表达式 | 一行搞定 | 性能最差,尤其在大数据量时明显拖慢 |
根据性能测试结果,使用 removeAll()
是最优选择,既简洁又高效。
完整源码可以在 GitHub 上找到。