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 上找到。


原始标题:Removing Stopwords from a String in Java