文本预处理:让机器读得懂中文
熟悉我的朋友都知道,我做过不少从原始文档里抽知识图谱的项目。每次开始前,最花时间的往往不是搭建图谱,而是「洗数据」。说白了,你扔进一段“苹果很好吃”,机器怎么知道这说的是水果还是新发布的手机?
这就是文本预处理要解决的。中文没有天然空格,词和词之间挤在一起。所以想让后面的句法解析、命名实体识别发挥作用,预处理这步必须做扎实。嗯,这章我们就聊分词、词性标注和去停用词。这三个动作,是后续所有抽取代价的基础。
3.1 分词:第一步,也是最关键的一步
分词就是把连续汉字切分成独立的词语。你想想看,“南京市长江大桥”要是切分成“南京/市长/江大桥”,后面抽出来的知识图谱就全歪了。我个人习惯用 jieba,因为它够快,而且支持自定义词典。
基础用法很简单:
import jieba
text = "知识图谱技术正在快速发展"
words = jieba.lcut(text)
print(words)
# 输出: ['知识', '图谱', '技术', '正在', '快速', '发展']
但实际项目里文本会很复杂。比如“华为Mate60 Pro上市了”,如果不加词典,jieba很可能会把“Mate60”切成“Mate”和“60”。
解决方案?加自定义词典:
jieba.load_userdict("brand_dict.txt")
words = jieba.lcut("华为Mate60 Pro上市了")
print(words)
# 输出: ['华为', 'Mate60', 'Pro', '上市', '了']
brand_dict.txt 内容长这样:
华为Mate60 Pro 5 nz
小米14 Ultra 5 nz
iPhone 16 Pro 5 nz
每一行格式是「词语 词频 词性」。加了自定义词典后,准确率会明显提升。
小提示:如果遇到长文本,记得先删掉全角符号和多余空格。不然分词结果里会夹带一堆“ 。” “ , ”之类的垃圾。
3.2 词性标注:给每个词贴上标签
词性标注就是给每个词标记名词、动词、形容词等。为什么要做这一步?因为抽取实体关系时,你需要知道哪些词是人名,哪些是地点。
继续用 jieba:
import jieba.posseg as pseg
text = "我去北京参加人工智能大会"
words = pseg.lcut(text)
for word, flag in words:
print(f"{word}: {flag}", end=" ")
# 输出: 我: r 去: v 北京: ns 参加: v 人工智能: n 大会: n
标记含义:r 是代词,v 是动词,ns 是地名,n 是名词。你可以在后续做实体抽取时直接按词性过滤——比如只保留 ns 和 n。
我在项目中遇到过,有时同一个词在不同语境下词性完全不同。比如“把门关上”和“一把刀”中的“把”。jieba 的词性标注基于统计模型,遇到这种歧义时可能会翻车。
关键点:词性标注的错误会直接传导到下游。如果你发现实体识别结果莫名其妙,先回头检查一下词性标注是不是错了。
3.3 去除停用词:去掉噪音
停用词就是“的、了、是、在”这类高频但没啥实际语义的词。留着它们会影响词频统计和 TF-IDF 计算。为啥?因为它们出现次数太多了,会淹没真正的关键词。
一个简单的实现:
stopwords = set()
with open("stopwords_zh.txt", "r", encoding="utf-8") as f:
for line in f:
stopwords.add(line.strip())
text = "这本书是真的好看"
tokens = jieba.lcut(text)
filtered = [w for w in tokens if w not in stopwords]
print(filtered)
# 输出: ['本书', '好看'] (假设 '的' 在停用词表中)
当然,停用词表怎么选是有讲究的。我见过有人直接拿网上 2000 词的通用停用词表套上去。结果?“不”、“很”、“非常”全被滤掉了。情感分析直接崩了。
注意:对于知识图谱抽取场景,你要保留否定词(比如“不”、“没”)。因为这些词会反转三元组的关系指向。比如“他不吃苹果”去掉“不”之后,事实就反了。
所以,建议根据场景定制停用词表。比如抽取科技文献时,“提出”、“方法”、“基于”这些词其实也经常出现,但它们对语义有贡献,不应该被删掉。
3.4 三者配合:完整的预处理管线
实际项目中,不会单独用某一个步骤。我把它们串起来:
def preprocess(text, stopwords, custom_dict_path=None):
# 1. 加载自定义词典
if custom_dict_path:
jieba.load_userdict(custom_dict_path)
# 2. 分词 + 词性标注
words = pseg.lcut(text)
# 3. 过滤停用词 + 只保留名词/动词/专有名词
keep_flags = {"n", "v", "vn", "ns", "nt", "nz"}
filtered = []
for word, flag in words:
if word not in stopwords and flag in keep_flags:
filtered.append((word, flag))
return filtered
输出类似这样:
[("知识图谱", "n"), ("建立", "v"), ("自然语言", "n"), ("处理", "vn")]
这个结果就可以丢给后面的实体链接和关系抽取模块了。
经验之谈:预处理管线要写成可配置的。词性过滤规则、停用词表路径、自定义词典路径都放配置文件里。这样换一个领域,改个配置就行,不用改代码。
3.5 常用工具对比
除了 jieba,还有几个常用工具。我列举一下,方便你选型:
| 工具 | 分词 | 词性标注 | 速度 | 典型场景 |
|---|---|---|---|---|
| jieba | 好 | 好 | 快 | 快速原型、小规模 |
| HanLP | 很好 | 很好 | 中等 | 学术、复杂词性 |
| LTP (哈工大) | 很好 | 很好 | 慢 | 高精度需求 |
| PaddleNLP | 好 | 好 | 中等 | 端到端管线 |
我个人习惯,小项目用 jieba,快速;大项目用 HanLP,准确度高。如果你要处理百万级文献,建议先做预测试,看看哪个工具的 OOV(未登录词)覆盖率更符合你的语料。
好,文本预处理的三个基本步骤就这些。从分词、词性标注到去停用词,每一步都影响着知识图谱的最终质量。下次拿到原始文本,可以先按这个流程走一遍,把噪音去掉,让机器看到真正有价值的信息。