3、文本预处理基础:分词、去停用词、词干提取与词形还原、文本清洗实战

文本预处理,说白了就是给原始文本「洗个澡」。我做了这么多年 NLP 项目,可以负责任地告诉你:预处理做得好,模型成功一半。很多同学一上来就调模型参数,结果数据里全是噪声,那再好的模型也白搭。

今天咱们就把预处理这四板斧——分词、去停用词、词干提取与词形还原、文本清洗——挨个讲透。我会结合实战经验,把那些坑都给你指出来。

3.1 文本清洗:先把垃圾倒掉

文本清洗是第一步,也是最容易被忽视的一步。我见过太多人拿到数据直接分词,结果模型学了一堆乱码和 HTML 标签。

常见的清洗内容包括:

  • 去除 HTML 标签:爬虫拿到的数据常有 <div><p> 等标签
  • 去除特殊符号:@、#、$、% 等,除非你的任务需要保留
  • 统一大小写:英文文本全部转小写
  • 去除多余空白:多个空格、换行符压缩成一个
  • 处理编码问题:乱码字符、Unicode 控制字符

核心原则:清洗要「适度」。过度清洗会丢失语义信息。比如「iPhone 12」中的数字和大小写,在情感分析里可能很重要。

来看一段 Python 清洗代码:

import re
import unicodedata

def clean_text(text):
    # 去除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 去除 URL
    text = re.sub(r'http\S+|www\S+', '', text)
    # 统一转小写
    text = text.lower()
    # 只保留字母、数字、空格和基本标点
    text = re.sub(r'[^a-zA-Z0-9\s\.\,\!\?]', '', text)
    # 压缩多余空白
    text = re.sub(r'\s+', ' ', text).strip()
    return text

我的经验:清洗规则最好做成可配置的。不同任务需要保留的东西不一样。我曾经做过一个产品评论分析,发现「#」号在美妆评论里代表色号,去掉后模型准确率掉了 5 个点。

3.2 分词:把句子切成词

分词就是把连续的文本切成一个个有意义的单元。中文和英文的切法完全不同。

3.2.1 英文分词

英文天然有空格分隔,看起来简单,但坑也不少:

  • 缩写词:don't 要拆成 do 和 n't 吗?
  • 连字符:state-of-the-art 是一个词还是三个?
  • 专有名词:New York 不能拆成 New 和 York

我一般用 NLTK 或 spaCy 的分词器,它们内置了这些规则:

from nltk.tokenize import word_tokenize

text = "I don't like state-of-the-art methods."
tokens = word_tokenize(text)
print(tokens)
# 输出:['I', 'do', "n't", 'like', 'state-of-the-art', 'methods', '.']

3.2.2 中文分词

中文分词就麻烦多了。没有空格,全靠算法猜。常用的工具有 jieba、HanLP、pkuseg 等。

我个人习惯用 jieba,轻量又好用:

import jieba

text = "我爱自然语言处理"
# 精确模式
tokens = jieba.lcut(text)
print(tokens)
# 输出:['我', '爱', '自然语言处理']

# 添加自定义词典
jieba.add_word('自然语言处理')
tokens = jieba.lcut(text)
print(tokens)
# 输出:['我', '爱', '自然语言处理']

注意:中文分词没有「完美」的答案。同一个句子,不同分词器结果可能不同。我建议根据你的领域数据,准备一份自定义词典,能显著提升分词准确率。

3.3 去停用词:扔掉没用的词

停用词就是那些高频但没啥实际意义的词,比如「的」「了」「是」「a」「the」等。去掉它们可以减少噪声,降低特征维度。

但这里有个坑——停用词不是绝对的。比如在情感分析中,「不」这个字很重要:「我不喜欢」和「我喜欢」意思完全相反。如果你把「不」也去掉,那就完蛋了。

我一般会准备两份停用词表:

任务类型 停用词策略 示例
文本分类 去除高频虚词 的、了、是、在、有
情感分析 保留否定词和程度词 不、很、非常、太
关键词提取 严格去除 所有虚词、代词、介词
机器翻译 基本不去停用词 保留所有词

实战代码:

def remove_stopwords(tokens, stopwords_set):
    return [token for token in tokens if token not in stopwords_set]

# 加载停用词表
with open('stopwords_zh.txt', 'r', encoding='utf-8') as f:
    stopwords = set([line.strip() for line in f])

# 使用
tokens = ['我', '爱', '自然语言处理', '的', '研究']
filtered = remove_stopwords(tokens, stopwords)
print(filtered)
# 输出:['我', '爱', '自然语言处理', '研究']

避坑指南:我曾经做过一个电商评论情感分析,把「不」字加进了停用词表,结果模型把「不好」和「好」当成一类,准确率直接腰斩。从那以后,我每次都会先检查停用词表里有没有否定词。

3.4 词干提取与词形还原:把词变回原形

这两个操作都是为了把不同形态的词统一起来。比如「running」「ran」「runs」都指向「run」。

3.4.1 词干提取

词干提取比较粗暴,直接砍掉词缀。速度快,但结果可能不是合法单词。

from nltk.stem import PorterStemmer

stemmer = PorterStemmer()
words = ['running', 'ran', 'runs', 'better', 'studies']
for w in words:
    print(f"{w} -> {stemmer.stem(w)}")
# 输出:
# running -> run
# ran -> ran  (没变,因为ran不是规则变化)
# runs -> run
# better -> better  (没变)
# studies -> studi  (砍过头了)

3.4.2 词形还原

词形还原更智能,它会根据词性把词还原成词典里的原形。比如「better」还原成「good」,「ran」还原成「run」。

from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet

lemmatizer = WordNetLemmatizer()

# 需要指定词性,否则默认是名词
words = [('running', 'v'), ('ran', 'v'), ('better', 'a'), ('studies', 'n')]
for w, pos in words:
    print(f"{w} -> {lemmatizer.lemmatize(w, pos)}")
# 输出:
# running -> run
# ran -> run
# better -> good
# studies -> study

怎么选?

  • 词干提取:速度快,适合大规模数据、信息检索场景
  • 词形还原:准确率高,适合情感分析、文本分类等需要语义理解的任务

我个人在文本分类中更倾向词形还原,虽然慢一点,但效果更好。

3.5 实战:完整的预处理流程

好了,理论讲完了,咱们来写一个完整的预处理流水线。这是我做项目时常用的模板:

import re
import jieba
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords

class TextPreprocessor:
    def __init__(self, lang='zh'):
        self.lang = lang
        self.lemmatizer = WordNetLemmatizer()
        # 加载停用词
        if lang == 'zh':
            with open('stopwords_zh.txt', 'r') as f:
                self.stopwords = set([l.strip() for l in f])
        else:
            self.stopwords = set(stopwords.words('english'))
    
    def clean(self, text):
        # 清洗
        text = re.sub(r'<[^>]+>', '', text)
        text = re.sub(r'http\S+', '', text)
        text = text.lower()
        text = re.sub(r'[^a-zA-Z0-9\u4e00-\u9fff\s]', '', text)
        text = re.sub(r'\s+', ' ', text).strip()
        return text
    
    def tokenize(self, text):
        if self.lang == 'zh':
            return jieba.lcut(text)
        else:
            return text.split()
    
    def lemmatize(self, token):
        # 简单处理,实际应用中需要词性标注
        return self.lemmatizer.lemmatize(token)
    
    def process(self, text):
        text = self.clean(text)
        tokens = self.tokenize(text)
        # 去停用词 + 词形还原
        result = []
        for token in tokens:
            if token not in self.stopwords and len(token) > 1:
                if self.lang == 'en':
                    token = self.lemmatize(token)
                result.append(token)
        return result

# 使用示例
preprocessor = TextPreprocessor(lang='zh')
text = "我真的很喜欢这个产品,它太棒了!"
print(preprocessor.process(text))
# 输出:['真的', '喜欢', '这个', '产品', '太棒了']

重要提醒:预处理流程的顺序很重要。我建议:先清洗 → 再分词 → 去停用词 → 最后词形还原。如果先分词再清洗,可能会把词切碎,导致清洗不干净。

3.6 常见问题与避坑指南

做预处理这么多年,我踩过的坑不少,分享几个最常见的:

  • 编码问题:中文文本经常遇到 GBK 和 UTF-8 混用。我建议统一用 UTF-8,遇到乱码直接丢弃或转码。
  • 数字处理:电话号码、日期、价格要不要保留?看任务。如果是情感分析,价格数字可能有用;如果是新闻分类,数字意义不大。
  • 表情符号:社交媒体文本里表情符号很有用。😊 代表正面,😡 代表负面。我一般会保留或者转成文字标签。
  • 拼写错误:英文文本里常见拼写错误。可以用 PySpellChecker 纠正,但要注意速度。

我的习惯:每次做新项目,我都会先随机抽 100 条数据,手动检查预处理结果。看看有没有不该去掉的被去掉了,有没有该去掉的没去掉。这一步花不了 10 分钟,但能避免很多后期问题。

嗯,文本预处理这块就讲到这里。说白了,预处理没有银弹,关键是根据你的数据和任务灵活调整。下一章咱们会讲特征工程,到时候这些预处理的结果就要派上大用场了。

公众号:蓝海资料掘金营,微信deep3321