文本预处理:分词技术、去停用词与文本清洗

文本预处理,说白了就是给原始文本「洗澡」。你从网上爬下来的数据,或者用户输入的内容,往往带着各种噪音——乱码、标点、无意义的语气词。如果不处理干净,后面的 Embedding 模型就会学到一堆垃圾特征。

我个人习惯把预处理分成三个步骤:清洗 → 分词 → 去停用词。顺序不能乱,先去掉脏东西再切词,否则分词器会被标点和特殊符号干扰。

核心观点:预处理的质量直接决定 Embedding 的上限。模型再强,也救不了脏数据。

文本预处理核心流程 原始文本 ① 文本清洗 去HTML/特殊字符 ② 分词 Jieba / spaCy ③ 去停用词 过滤无意义词 干净 序列 输入 Embedding 模型 💡 顺序不可颠倒:先清洗 → 再分词 → 最后去停用词,否则分词结果会包含大量噪音 清洗后的文本分词准确率可提升 15%-30%

2.1 文本清洗与标准化

清洗是第一步,也是最容易被忽略的一步。我记得刚入行时接过一个项目,数据是从 PDF 里 OCR 出来的,里面全是乱码和多余换行。我当时直接扔进分词器,结果出来的词全是「\n\n\n」、「•」、「■」这种垃圾。后来花了两天重新清洗,模型效果直接翻倍。

清洗的标准流程,我一般这么干:

  1. 统一大小写 — 英文全部转小写,避免「Apple」和「apple」被当成两个词
  2. 去除 HTML 标签 — 用正则或 BeautifulSoup 把 <div>、<p> 之类剥干净
  3. 处理特殊字符 — 保留中文、英文、数字,其他符号看场景决定是否保留
  4. 去除多余空白 — 多个空格、换行、制表符统一成一个空格
  5. 编码统一 — 全部转成 UTF-8,避免乱码

我的小技巧:清洗时先做一次「肉眼检查」。随机抽 100 条数据打印出来看看,能发现很多正则覆盖不到的边缘情况。

举个实际例子。下面这段代码是我常用的清洗函数:

import re

def clean_text(text):
    # 1. 统一小写
    text = text.lower()
    # 2. 去除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    # 3. 只保留中文、英文、数字、常用标点
    text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.\,\!\?]', '', text)
    # 4. 合并多余空白
    text = re.sub(r'\s+', ' ', text).strip()
    return text

你可能会问:为什么保留标点?因为有些场景下,问号和感叹号是有语义的。比如「你确定?」和「你确定。」表达的情绪完全不同。具体保留哪些符号,取决于你的下游任务。

2.2 分词技术:Jieba vs spaCy

分词是中文 NLP 的「卡脖子」环节。英文天然有空格分隔,中文没有。所以选对分词工具很重要。

我主要用两个工具:Jieba 和 spaCy。它们各有千秋,我列个表对比一下:

维度 Jieba spaCy
分词模式 精确模式、全模式、搜索引擎模式 基于统计的 pipeline
自定义词典 支持,简单易用 支持,但需要训练或规则
速度 极快(纯 Python 实现) 中等(Cython 底层)
词性标注 内置,但精度一般 内置,精度高
依赖解析 不支持 支持
适用场景 快速原型、大规模数据 需要语法分析的场景

Jieba 实战

Jieba 是我用得最多的工具,因为它快、轻量、上手简单。对于 Embedding 管道来说,大部分场景用 Jieba 就够了。

import jieba

text = "我在搭建一个文本Embedding生产管道"
# 精确模式(默认)
words = jieba.lcut(text)
print(words)
# 输出:['我', '在', '搭建', '一个', '文本', 'Embedding', '生产', '管道']

# 添加自定义词典
jieba.add_word('生产管道')
words = jieba.lcut(text)
print(words)
# 输出:['我', '在', '搭建', '一个', '文本', 'Embedding', '生产管道']

注意看,加了自定义词典后,「生产管道」被正确识别为一个词。这在专业领域特别有用。我在做医疗文本项目时,就靠自定义词典把「冠状动脉粥样硬化」这种长词完整保留下来。

避坑指南:我曾经在电商评论数据上直接用 Jieba 默认词典,结果「不辣」被切成「不」和「辣」,情感分析直接翻车。后来加了否定词词典才解决。所以,领域词典一定要加。

spaCy 实战

spaCy 更适合需要语法分析的场景。比如你想知道「苹果」是公司名还是水果,spaCy 的依存分析能帮你判断。

import spacy

# 加载中文模型
nlp = spacy.load("zh_core_web_sm")
doc = nlp("苹果发布了新款iPhone")

for  in doc:
    print(f"{.text}: {.pos_}, 依赖: {.dep_}")
# 输出示例:
# 苹果: PROPN, 依赖: nsubj
# 发布: VERB, 依赖: ROOT
# 了: PART, 依赖: aux
# 新款: NOUN, 依赖: compound
# iPhone: PROPN, 依赖: dobj

spaCy 的词性标注和依赖解析精度很高,但模型文件比较大(几百 MB),部署时要注意。我个人习惯:线上服务用 Jieba,离线分析用 spaCy。

2.3 去除停用词

停用词就是那些高频但没啥实际意义的词,比如「的」、「了」、「在」、「是」。去掉它们能减少噪音,让 Embedding 更聚焦在关键内容上。

但这里有个坑:停用词表不是通用的。我见过有人直接拿网上的通用停用词表去处理金融新闻,结果把「跌」、「涨」这种关键词也过滤掉了。为什么?因为通用表里把单字词全删了。

我的做法是:

  1. 先用通用停用词表(比如哈工大停用词表)做第一轮过滤
  2. 然后根据你的数据,手动调整:去掉那些不该被过滤的词
  3. 最后跑一遍统计,看看被过滤的词里有没有高频关键词
# 加载停用词表
stop_words = set()
with open('stopwords.txt', 'r', encoding='utf-8') as f:
    for line in f:
        stop_words.add(line.strip())

# 过滤停用词
tokens = ['我', '在', '搭建', '一个', '文本', 'Embedding', '生产', '管道']
filtered = [w for w in tokens if w not in stop_words]
print(filtered)
# 输出:['搭建', '文本', 'Embedding', '生产', '管道']

经验之谈:停用词表建议保留否定词(不、没、别)和程度副词(很、非常、极其)。这些词对情感和语义有重要影响,去掉会丢失信息。

2.4 完整预处理管道示例

把上面三步串起来,就是一个完整的预处理管道。我一般封装成一个类,方便复用:

class TextPreprocessor:
    def __init__(self, stopwords_path=None, user_dict=None):
        self.stopwords = set()
        if stopwords_path:
            with open(stopwords_path, 'r', encoding='utf-8') as f:
                self.stopwords = set(line.strip() for line in f)
        if user_dict:
            jieba.load_userdict(user_dict)

    def clean(self, text):
        text = text.lower()
        text = re.sub(r'<[^>]+>', '', text)
        text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.\,\!\?]', '', text)
        text = re.sub(r'\s+', ' ', text).strip()
        return text

    def tokenize(self, text):
        return jieba.lcut(text)

    def remove_stopwords(self, tokens):
        return [t for t in tokens if t not in self.stopwords and len(t.strip()) > 0]

    def process(self, text):
        cleaned = self.clean(text)
        tokens = self.tokenize(cleaned)
        return self.remove_stopwords(tokens)

# 使用
pp = TextPreprocessor(stopwords_path='stopwords.txt')
result = pp.process("我今天在搭建一个文本Embedding生产管道!")
print(result)
# 输出:['今天', '搭建', '文本', 'Embedding', '生产', '管道']

你看,经过预处理后,文本从 15 个字变成了 6 个核心词。这些词就是 Embedding 模型的输入。干净、聚焦、没有噪音。

总结一下:文本预处理不是可有可无的「前戏」,而是决定 Embedding 质量的基石。清洗要彻底,分词要准确,停用词要定制。这三步做好了,后面的工作会轻松很多。