文本预处理:分词技术、去停用词与文本清洗
文本预处理,说白了就是给原始文本「洗澡」。你从网上爬下来的数据,或者用户输入的内容,往往带着各种噪音——乱码、标点、无意义的语气词。如果不处理干净,后面的 Embedding 模型就会学到一堆垃圾特征。
我个人习惯把预处理分成三个步骤:清洗 → 分词 → 去停用词。顺序不能乱,先去掉脏东西再切词,否则分词器会被标点和特殊符号干扰。
核心观点:预处理的质量直接决定 Embedding 的上限。模型再强,也救不了脏数据。
2.1 文本清洗与标准化
清洗是第一步,也是最容易被忽略的一步。我记得刚入行时接过一个项目,数据是从 PDF 里 OCR 出来的,里面全是乱码和多余换行。我当时直接扔进分词器,结果出来的词全是「\n\n\n」、「•」、「■」这种垃圾。后来花了两天重新清洗,模型效果直接翻倍。
清洗的标准流程,我一般这么干:
- 统一大小写 — 英文全部转小写,避免「Apple」和「apple」被当成两个词
- 去除 HTML 标签 — 用正则或 BeautifulSoup 把 <div>、<p> 之类剥干净
- 处理特殊字符 — 保留中文、英文、数字,其他符号看场景决定是否保留
- 去除多余空白 — 多个空格、换行、制表符统一成一个空格
- 编码统一 — 全部转成 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 更聚焦在关键内容上。
但这里有个坑:停用词表不是通用的。我见过有人直接拿网上的通用停用词表去处理金融新闻,结果把「跌」、「涨」这种关键词也过滤掉了。为什么?因为通用表里把单字词全删了。
我的做法是:
- 先用通用停用词表(比如哈工大停用词表)做第一轮过滤
- 然后根据你的数据,手动调整:去掉那些不该被过滤的词
- 最后跑一遍统计,看看被过滤的词里有没有高频关键词
# 加载停用词表
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 质量的基石。清洗要彻底,分词要准确,停用词要定制。这三步做好了,后面的工作会轻松很多。