前几天一个朋友问我:想练中文NLP,拿什么语料比较合适。我几乎没犹豫就回了一句话——去把四大名著拆了。这个项目听起来像老掉牙的语文作业,但真用 Python 跑一遍,你会在同一条链路里撞上编码清洗、分词精度、角色别名、关系网络、情感曲线这一整套典型问题。而且我这次还加了个新玩法:把编辑器里的 AI 模型当成第二双手,让它帮我写清洗函数、补全统计代码、解释陌生报错。整条链路走下来,比预想中省力不少,但也踩了不少坑。下面把完整过程和注意事项一次性摊开。
1. 为什么我先盯上四大名著:语料选择的三个硬道理
1.1 免费、规范、有章节,这是新手最好的训练场
很多人学 NLP 喜欢去爬微博评论、抓新闻标题,结果一半时间耗在反爬和字段拼接上。中文 NLP 最缺的不是模型,而是干净、稳定、可重复使用的语料。四大名著恰好满足三个条件:没有版权争议,网上随处可下载;文本质量相对较高,错字比例比UGC语料低得多;天然按“回”分块,每一回都是一个基本分析单元,非常适合做序列化统计。
从体量看,四本书合起来几百万字,读进内存之后也不过几十兆,个人电脑完全跑得动。这比动不动就几个G的网页语料友好太多。你可以放心地用词频、情感、共现矩阵这些方法来回折腾,不用担心显存和训练时间。
还有一个容易被忽略的好处:四大名著的人物体系极其庞大。光《红楼梦》有名有姓的角色就上百个,《水浒传》一百单八将更是天然的关系网络测试床。如果你想挖“人物关系图”,这套语料比任何爬来的数据都要完整。
1.2 四部书的文本形态差异,逼着你处理不同麻烦
很多人以为拿到四个 txt 文件就能一把梭,这是最大的误解。四本书的文本形态差异非常大,清洗规则根本不能套同一套。
《三国演义》属于半文半白,句子短、信息密度高,段落里经常夹杂“却说”“且听下回分解”这类套话。《红楼梦》里有大量诗词曲赋,还有极多人物对话,标点符号分布密集。《西游记》一章里常常有大段韵文描写,比如“金睛火眼”“千仞冈头”这种四字对仗。到了《水浒传》,方言和市井口语又冒出来了,称呼系统复杂到让人头疼。
这意味着你在写清洗函数时,不能只写一个正则去空格。必须针对每一本书做差异化的预处理配置,比如要不要保留韵文、要不要把人物对话抽出来、要不要过滤“话说”“却说”这类叙述性套话。我最后是把四本书分别封装成了不同的清洗管道,公用规则放在外层,单独规则写在各自函数里。这个设计比“一个函数打天下”稳得多。
1.3 项目目标:不追求炫技,先跑通一条完整NLP链路
这个项目没必要一开始就上大模型或者深度学习框架。我定的目标是:用最经典的非深度学习NLP方法,把四本书从原始文本变成三张表。
第一张表是词频统计,按回目聚合,看高频词和叙事风格的关系;第二张表是按回目的情感倾向曲线,看故事节奏变化;第三张表是人物共现关系,把四本书里核心人物的同框强度排个序,再画出关系网络。这三张表覆盖了文本清洗、中文分词、停用词过滤、情感分析、共现矩阵、图关系计算这些基础动作,足够把 NLP 入门的主干路走一遍。
整个过程我都开着编辑器里的 AI 代码补全插件,让它帮我写代码脚手架、解释报错、重构冗余逻辑。这也是我想额外验证的一件事:这些编辑器里的 AI 模型,到底能帮一个 NLP 项目省多少力,又会在哪里帮倒忙。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 读进内存之前,文本清洗已经把很多人拦住了
2.1 第一坑:txt 来源多种多样,编码和空白字符各不相同
我下载的四本书来源不同,打开第一眼就发现问题:《三国演义》是 UTF-8 带 BOM,开头有个隐藏字符;《红楼梦》是 GBK 编码;《西游记》倒是 UTF-8,但每行后面跟着一堆半角空格;《水浒传》最离谱,段落之间混着全角空格和制表符。
如果一开始就写死 open(filename, encoding='utf-8'),你会在第一本书就抛 UnicodeDecodeError。这里我的建议是做一个编码自动探测函数,把二进制数据先读进来,然后依次尝试 utf-8-sig、gbk、gb18030,哪个先解码成功用哪个。
python复制def read_text_auto(path):
raw = open(path, 'rb').read()
for enc in ('utf-8-sig', 'gb18030', 'utf-8'):
try:
return raw.decode(enc), enc
except UnicodeDecodeError:
continue
raise ValueError(f'无法解码文件: {path}')
不要用 errors='ignore' 草草忽略错误,因为你不知道中间丢掉的是无关符号还是正文里的关键汉字。解码失败宁可报错,让问题暴露在明处。
接着还要处理 BOM 和空白。我的做法是先把 \r、\t、全角空格、零宽字符等统一替换成普通空格,再压缩连续空白,最后按照标点和换行拆分段落。这里需要特别提醒:清洗文本时不要一刀切删掉所有标点。逗号、句号、问号对后续的句子切分和情感分析非常重要,如果你把标点清空了,整个文本会糊成一大段,根本没法按句滑窗。
2.2 我用编辑器AI模型写清洗函数,实测正确率并不低
这节重点说说编辑器里的 AI 模型怎么帮我干活的。我没有直接写完整清洗函数,而是把需求用自然语言描述出来,再加上一小段原文样例,让 AI 模型生成初版代码。我的 Prompt 大致是:
我有一个中文古典小说原文,需要清洗成干净的段落列表。要求如下:去掉所有空白字符和不可见符号;保留中文汉字、中文标点和英文标点;删除英文和数字;不要合并不同段落;输出为字符串列表。下面是原始文本样例:……
AI 模型很快生成了一版正则清洗函数,大方向是对的。它知道用 re.sub 匹配空白,也知道构建一个白名单正则保留中文字符和标点。但我仔细一看就发现问题:它把书名号《》写进了白名单,却漏掉了省略号和破折号,还顺带把数字删干净了,导致原文里的“一回”“二更”全部变成“回”“更”。对于一般文本这可能无所谓,但对于古籍文本,“三十回”这类数字信息是有分析价值的。
我的处理方式是:先跑一遍 AI 生成的代码,把输出原样打印前 500 个字符,肉眼检查一遍。发现问题后直接在对话里补充约束:保留中文数字和阿拉伯数字,只删除英文字母,标点只保留这组白名单。AI 模型改动得很快,但你必须养成“AI 生成的代码绝不直接复制进项目”的习惯。
2.3 回目边界识别:中文数字转阿拉伯数字的正则设计
清洗干净文本后,下一个任务是把全书拆成单个回目。这个步骤决定了后面所有按回分析的正确性。难点在于回目标题形式不统一:《三国演义》是“第一回 宴桃园豪杰三结义 斩黄巾英雄首立功”,《红楼梦》里还有“第四十回 史太君两宴大观园 金鸳鸯三宣牙牌令”这种超长标题。
我采取的策略是:先用正则找出所有“第X回”标记,再把中文数字转成阿拉伯数字作为回目的唯一索引。正则写成这样:
python复制import re
CN_NUM = '一二三四五六七八九十百零〇两'
pattern = re.compile(fr'第([{CN_NUM}]+)回')
用这种模式去遍历全文,找到所有匹配位置。接着写一个 cn2num 函数,把“一百二十”“四十”“九”这类中文数字转成 int。转化完成后再检查回目是否连续,如果发现跳号,说明文本缺章或者第一章之前还有“楔子”“引首”之类的内容,需要在后续分析时单独排除。
这一步最容易犯的错误是把“第十二回”匹配成“第十二”加上后面的“回”字,然后在转换时漏掉十位。我建议转换函数先写单元测试,至少验证“三”“二十”“九十九”“一百二十”这几种情况。不然你辛辛苦苦拆出来的回目序列,排序时会冒出一堆不连续的断点。
3. 分词和停用词:机器眼中的名著长什么样
3.1 jieba分词的基础用法与精度取舍
清洗完原文、切好回目,终于可以进入传统 NLP 的核心环节:中文分词。我用的是 jieba,它虽然不算最新技术,但在通用中文分词场景下依然是稳定可靠的默认选择。基础用法很简单:
python复制import jieba
text = "那宝玉进入大观园,见花光柳影,鸟语溪声"
words = jieba.lcut(text)
print(words)
这里有一个关键认知:分词是下游一切统计的地基。词频统计、关键词抽取、情感打分全部依赖分词结果。如果分词把“贾宝玉”切成“贾”“宝玉”,那么后面统计人物共现时会出现大量碎片,关系图谱完全失真。
我的做法不是追求分词准确率 100%,而是做两个低成本优化:加载自定义词典和启用专业名词识别。jieba 默认词典对现代汉语覆盖不错,但古籍里的很多专名它不认识。像“贾琏”“史湘云”“混世魔王”“金角大王”这类词,直接放进自定义词典,分词效果立竿见影。
python复制custom_words = ["贾宝玉", "林黛玉", "薛宝钗", "史湘云", "美猴王", "孙行者", "猪八戒", "及时雨", "智多星"]
for w in custom_words:
jieba.add_word(w)
有人喜欢给自定义词加词频和词性,我实测下来没有太大必要。如果加了错误的词频属性,反而可能让 jieba 在特定语料里强制切分,导致本来正确的切法被带偏。我一般只做 add_word,不做 suggest_freq,除非某个词被反复切错。
3.2 自定义词典和停用词表:效果差异立刻显现
光有自定义词典还不够,停用词表是决定词频统计可读性的第二个关键因素。中文文本里“的、了、是、在、我、你、他、也、都、就、又、把、被”这类虚词出现频率极高,如果不过滤,Top 词频表会被这些虚词刷屏,真正的信息词反而不见踪影。
我准备了一份针对古典小说的停用词表,除了通用虚词,还加上了“话说”“却说”“只见”“且说”“正是”“毕竟”“不知”“笑道”“心中”这类篇章标记词和对话套话。你可以根据四本书的实际词频结果动态补充。比如《红楼梦》里“笑道”出现频率极高,如果不加停用词,它永远是第一梯队高频词,但这个词对主题分析几乎没有贡献。
过滤之后,词频统计才真正开始有意义。这里我直接用 collections.Counter 统计,不需要额外引入复杂的框架:
python复制from collections import Counter
counter = Counter()
for words in chapter_words:
counter.update([w for w in words if w not in stopwords and len(w) > 1])
3.3 四部书的高频词对比表:一眼看出叙事风格差异
我把四本书各自词频前十拉出来对比,效果非常直观。在机器眼里,这四部经典呈现出了截然不同的气质:
| 书籍 | 高频词Top 5 | 给我的阅读感受 |
|---|---|---|
| 《红楼梦》 | 宝玉、黛玉、笑道、凤姐、如今 | 日常生活和对话密度极高,聚焦人际互动 |
| 《三国演义》 | 玄德、孔明、主公、丞相、军马 | 政治军事用语密集,策略叙事占主导 |
| 《西游记》 | 行者、师父、八戒、菩萨、妖怪 | 神魔角色名全覆盖,冒险闯关模式明显 |
| 《水浒传》 | 宋江、哥哥、好汉、李逵、头领 | 江湖称谓系统突出,结义关系是核心 |
这个表本身不需要多高深的技术,但它的价值在于帮我们建立一种直觉:词频统计不是冰冷的数字游戏,而是能反映一本书叙事焦点和人物关系的文本指纹。你甚至可以按回目画高频词的频率曲线,比如“宝玉”在《红楼梦》里哪几回高频出现,大概率对应宝黛感情戏的关键转折点。
3.4 按回目聚合的情感曲线:从整体走向看故事节奏
词频只是第一步,我更想知道的是:这四本书在情感色调上有没有规律性的起伏。做法是按回目切分全文,对每一回收集的句子做情感打分,再取平均值作为这一回的情感分数,最后画一条随回目序号变化的折线。
情感打分我没有依赖太重型的模型,而是用基于词典的正负情感词匹配。古籍里的情感词和现代汉语有差异,比如“欢喜”“烦恼”“凄凉”“大怒”“悲切”这些词是稳定的情感信号。我直接准备了一个小规模情感词典,词表可以从公开情感词典中剥离出常见的古今通用词,再手工补上几类古典用语。
python复制def simple_sentiment(text, pos_words, neg_words):
score = 0
for w in pos_words:
score += text.count(w)
for w in neg_words:
score -= text.count(w)
return score
这个打分非常朴素,但胜在可控。跑完四本书后我得到几条有趣的曲线:《三国演义》的整体情感曲线波动大,战事章节经常大起大落;《红楼梦》到中后段的负面情感明显持续走高,越往后越压抑;《西游记》的情感波动和降妖除魔的章节高度相关,打斗章起伏激烈;《水浒传》则在前半段整体偏正面,招安之后曲线一路下滑。
必须提醒一句:千万别把这种简单情感曲线的结果当成严格文学生结论。它只是文本表层情感词分布的反映,像反讽、隐喻、对比这些修辞手法它完全读不懂。但作为 NLP 入门项目,这个结论足以让你在和朋友聊数据时说出一句:“从词典情感打分来看,《红楼梦》后半段的负面情绪确实在稳步攀升。”
4. 人物关系靠共现矩阵:谁和谁“同框”最有说服力
4.1 人物别名合并:关系挖掘中最容易低估的一步
人物关系分析的第一步不是算矩阵,而是把同一个人在不同称呼下合并成同一个实体。这一步做不好,后面所有关系强度全是错的。
拿《西游记》举例:“孙悟空”在原文里可能被叫做“悟空”“行者”“孙行者”“美猴王”“大圣”“齐天大圣”“泼猴”“孙外公”。如果你不合并,“行者”和“孙悟空”会被当成两个人,共现矩阵里它们之间的连接强度甚至可能排到第一,但真实语义是自环。《红楼梦》里“宝玉”和“贾宝玉”“宝二爷”同理。
我的做法是维护一张别名映射表,把每个主要人物的所有称呼都规约到规范名。比如:
python复制alias_map = {
"宝玉": "贾宝玉",
"宝二爷": "贾宝玉",
"怡红公子": "贾宝玉",
"悟空": "孙悟空",
"行者": "孙悟空",
"孙行者": "孙悟空",
"美猴王": "孙悟空",
"大圣": "孙悟空",
"玄德": "刘备",
"刘玄德": "刘备",
"关公": "关羽",
"云长": "关羽",
}
规矩就是:先对文本做别名替换,再进行分词,而不是分词后再合并。先替换的好处是,连自定义词典都不用过度扩充,因为“宝玉”“行者”“大圣”这些词本来就是常见词,你只要把文本中的别名统一替换成规范名就行。
但这里有个大坑:“哥哥”这个词在水浒传里高频出现,但它既可能是“宋江”的称呼,也可能是普通的兄弟称谓。如果无脑替换,关系矩阵会严重失真。我最后选择的做法是:只用白名单里的强别名,比如“宋公明”“呼保义”“及时雨”,把“哥哥”留给后续需要上下文语义的任务。所有别名替换都必须人工审核,这一步编辑器里的 AI 模型帮不了忙,它不知道你的文学实体边界在哪。
4.2 滑窗共现:窗口大小如何影响关系强弱
实体合并之后,我按“句子”而不是“整回”划滑窗窗口。原因是整回太长,一场大战里出现的所有人物都会被强制两两连接,关系度失去辨析度。句子窗口更贴近人物互动的真实场景。
具体做法是:把每一回切分成句子,对每个句子用人物实体词典做交集匹配,找出句子里出现了哪些人物;然后对这些人名做两两组合,在共现计数器里加一。用 collections.Counter 维护 (人物A, 人物B) 的键:
python复制from itertools import combinations
co_occur = Counter()
for sentence in sent_list:
names_in_sent = [n for n in person_names if n in sentence]
for pair in combinations(set(names_in_sent), 2):
co_occur[tuple(sorted(pair))] += 1
窗口大小直接影响关系强度。我试过用 50 字窗口、100 字窗口和整个段落窗口做对比,结论是:窗口越小,关系越聚焦,但会漏掉一些“隔空对话”式的关系;窗口越大,高频出场的两个人物会被误判为关系深厚。比如《三国演义》里诸葛亮和刘备当然关系极强,但 100 字窗口下诸葛亮和所有同时期武将的共现次数都被拉高了,这种虚高会污染 Top 20 关系排序。
我最后的设置是:以句子为窗口,同时把超过 80 字的超长句子二次切分成小句。这样既保持了场景局部性,又能处理原著里动辄上百字的长句。
4.3 从矩阵到网络:中心度排序与关键人物判定
得到共现计数后,我用 networkx 构建无向图,人物是节点,共现次数是边权重。然后计算每个节点的度中心性和权重度,输出全书的 Top 20 高权重人物。这一步的价值在于:它能帮我们从数据里找出“关系枢纽人物”。
我拿《水浒传》跑出来的结果很有意思。按原始共现次数排序,前几名全是出场频率最高的人,比如宋江、李逵、鲁智深。但把边权重归一化之后,像“吴用”这种出镜率不是最高、却总是和高层人物同框的军师,关系中心度反而很突出。《三国演义》里鲁肃的排名也高于很多人想象,因为他每次出现基本都在刘备/孙权/曹操几方面的交流场景中。
这个项目环节让我重新理解了一件事:出场频率高和关系核心不是一回事。关系核心人物可能不是主角,而是那个不断把各方势力连接起来的人。这类分析用到项目里,就是编辑器的 AI 模型补全了大部分绘图代码:它用 matplotlib 画出了节点大小按中心度变化的网络图,还自动加了中文标签。但我必须承认,这个环节里它只是个“打字快一点的实习生”,真正的中心度计算逻辑还得靠我自己把关。
5. 编辑器里的AI模型实战记录:能补全,但别全信
5.1 三类高频用法:写脚手架、重构、查报错
这个项目里我把编辑器里的 AI 模型用在了三个真实场景中,分别对应 NLP 项目里最常见的三类任务。
第一类是写脚手架代码。比如我要读入四个 txt 文件、按编码自动读取、切回目、保存中间结果,这些代码重复度高、逻辑模板化,AI 模型几乎可以一次写对。我把目标说清楚,再给它一个字段列表,它能直接生成一个能跑的 Script。这类工作适合全权交给 AI,但前提是你必须能读懂生成结果。
第二类是重构现有代码。我早期用两层 for 循环遍历所有句子做共现计数,写了大概三十行,逻辑啰嗦。我让 AI 模型把它重构成 combinations + Counter 的版本,它做得很好。这类重构非常推荐,因为 AI 对“简化循环”的把握比大多数新手强。
第三类是解释报错。我遇到过一个很诡异的 TypeError:unsupported operand type(s) for +: 'int' and 'str',光看报错完全不知道问题出在哪。我把完整 traceback 和涉及的代码段一起丢给 AI,它第一时间指出是某个字段里混入了文本空值,在做加法时爆了。这种定位能力非常省时间。
5.2 AI生成代码的翻车现场:正则和业务规则是重灾区
但 AI 模型绝不是万能,这个项目里它至少翻了两次车。
第一次翻车在正则。我让它写一个“去掉英文和数字但保留中文数字”的清洗规则,它生成的正则直接把所有数字删了,连“第二回”里的“二”都没放过。我检查输出发现回目标题全没了“第”和“回”,只剩一个空壳。正则这种细节密集的代码,AI 模型特别容易在边界条件上出错,必须做输入输出样例验证。
第二次翻车在业务规则。我让它帮我做别名替换,告诉它要把“行者”替换成“孙悟空”,它直接写了一个 .replace('行者', '孙悟空') 的代码。这个代码本身没错,但它不知道一个潜在业务规则:句子“一个行者走在山路中”里的“行者”指的是路人,不是孙悟空。在古典白话文里,“行者”同时是一个普通名词,AI 模型无法理解这种歧义。
这给我的启发是:AI 生成代码的可靠程度,取决于你把业务约束描述得多清楚。你越能把隐含规则显式化,越能减少翻车概率。尤其在中文 NLP 项目中,语料本身的脏、乱、歧义,是人类先验知识的主场,机器模型很难取代。
5.3 提升AI输出质量的Prompt写法
经过这个项目,我总结出一套比较有效的 AI 代码生成 Prompt 结构:目标、输入样例、输出约束、边界条件。
先说目标,一句话说清“我要什么”。再说输入样例,贴一段真实的脏数据,让 AI 看到当前文本长什么样。接着给输出约束,比如“输出为 list[str],每个元素是一个段落,空段落丢弃”。最后补边界条件,比如“如果遇到只有标点符号的行,直接忽略”。
举个例子,我希望 AI 帮我写回目标题提取,我的 Prompt 会写成:
目标:从中文古典小说文本中提取所有回目标题。输入示例:……输出:一个由回目序号和标题组成的字典。边界条件:正文中也可能出现“第X回”,但只在行首出现且独立成行时才是回目,需要先按行判断再匹配。
这个 Prompt 的价值在于我主动把“行首且独立成行”这个隐含规则放进了约束里,AI 生成的代码就准确很多。如果你只用一句话“帮我提取回目”,它很可能把正文里的“话说第X回……”也误判进去。
我还习惯让 AI 生成代码后自己写一个极简断言函数,把真实文本里截取的样例作为测试数据跑一遍。能通过的代码我才会把它并入主流程。这个习惯让我的项目减少了很多低级错误。
6. 这套流程复盘后,我觉得你也能试一把
整个项目跑完,我最想分享的体会是:中文 NLP 入门最大的绊脚石不是模型,而是文本的脏。四本名著看着规整,实际清洗加切回目就花掉了我将近一半的时间。但正是这些脏数据,把正则、编码、去重、别名映射这些基本功全部练了一遍,这比任何现成的干净数据集都值钱。
如果你也想复刻这个项目,我建议从《红楼梦》开始。它的人物称呼体系最复杂、回目标记最清晰,适合练别名合并和情感曲线。跑通之后再加《西游记》对比处理韵文段落,最后再处理更像口语的《水浒传》。一次只加一本书,出了问题也容易定位。
编辑器里的 AI 模型在这个项目里确实帮了大忙,它让我把更多精力放在思考业务规则上,而不是复制粘贴冗余代码。但我必须提醒一句:代码可以让 AI 模型写,逻辑必须自己懂。尤其是正则表达式和数据清洗相关代码,一定要做样例验证,否则你会在统计阶段拿到一个看似合理、实则全面偏差的结果。
最后再分享一个小技巧:把所有中间结果都序列化成 CSV 或 JSON 保存,比如清洗后的章节列表、词频表、共现矩阵。这样每次调整算法,都不用重新清洗一遍几百万字的文本。做完这些,你再回去翻任何一部中文小说,都会忍不住想:“这本书的词频前十会是什么?”
