☰
古典文本NLP实践:四大名著分词、人物关系与情感分析
2026/10/22 4:58:08 网站建设 项目流程

最近把四大名著的全量文本完整跑了一轮自然语言处理,从语料清洗、分词、实体识别到人物关系网络和情感曲线,做成了一个综合程序。这篇文章把整个项目从立项到落地拆开讲:设计思路、数据处理、算法选型、踩过的坑,以及可以直接复用的代码和参数。

先说明一下这个项目定位。它是一个面向中文文本挖掘的练手级综合程序,输入是本地四大名著TXT文本,输出包括词频统计、关键词提取、人物出场次数、人物共现关系网络、章节情感倾向曲线,以及基于向量空间的相似章节检索。技术栈以 Python 为主,用了结巴分词、正则表达式、gensim 和可视化库。适合三类人参考:刚入门 NLP 但没做过完整项目的同学、想拿古典文献做数据挖掘实验的开发者,以及做文本分析却苦于找不到干净中文语料的朋友。

1. 项目全貌与设计思路

1.1 为什么选四大名著当语料

很多人做中文 NLP 练习时喜欢爬新闻或评论,但我个人更推荐先拿四大名著练手。原因有三个。

第一,文本质量干净。四大名著属于公版文本,网上有大量校对过的基础文本,不需要处理平台反爬、广告夹杂、emoji 混排等问题,能省下不少清洗时间。四大名著全量加起来大概三百多万字,体量适合跑实验,不会像全网语料那样动辄上亿字导致训练时间失控。

第二,语言层次丰富。四大名著里有大量文言、白话、诗词、对话、叙述混合的段落,比如《三国演义》半文半白,《红楼梦》口语化和雅化并存。这种混合语言对分词、实体识别、情感判断都有挑战,比跑统一的现代汉语语料更能暴露算法的短板。

第三,人物关系密集。四部书里出场人物少则几十、多则几百,人物之间又有明确的阵营、亲缘、冲突关系,非常适合验证共现图谱、社区发现这类关系的算法效果。

1.2 总体架构与模块划分

这个程序我在设计时没有搞复杂的分层架构,而是按数据流切成四个阶段。

数据层负责读入四本小说的原始文本,按章节切分并清洗杂质。处理层做分词、去停用词、词性标注和命名实体识别。分析层在这个基础上计算词频、关键词、人物关系权重、情感分值。展示层把结果渲染成表格、柱状图、关系网络图等可视化结果。

这四个阶段边界非常明确,每一层只依赖前一层产出的结构化数据。比如展示层完全不关心原始TXT长什么样,只读取分析层生成好的CSV或JSON。这样做的最大好处是,后续想单独替换某一层算法时不用动其他模块,我后来优化分词方案时,只改了处理层,分析层和展示层一行代码没动。

1.3 关键技术选型背后的取舍

分词方案我直接用了结巴的精确模式。为什么不选更复杂的 LAC 或 HanLP?原因很简单,这个项目的核心目的是观察古典文本的统计特征,不是冲击准确率榜单。结巴精确模式速度足够快,四本小说全量切词在普通笔记本上几十秒就能跑完,而琢磨复杂模型的部署和调参时间,足够我把整个流程跑三遍。

关键词提取我用 TF-IDF 和 TextRank 各做了一套。TF-IDF 能快速给出每个章节的主题词,TextRank 更适合作者想表达但词频不高的线索词。两者对照使用比单看一个指标更有价值。

可视化部分选择了支持中文比较好的 pyecharts。它生成的是 HTML 文件,不需要额外搭建服务,双击就能在浏览器里打开,交互效果比 matplotlib 静态图好不少,适合展示人物关系网络这种强交互内容。

2. 语料处理与分词工程

2.1 文本获取、清洗与章节切割

语料获取的渠道这里不建议直接爬某个读书网站,找校对过的公版TXT更省心。拿到手后第一步不是急着分词,而是统一格式。

编码问题最容易被忽略。网上下载的TXT可能是 GBK、GB18030 或 UTF-8 编码,直接读会让程序报错或者产生乱码。我的做法是用chardet先探测编码,再统一转成 UTF-8 写入中间文件。这一步做完,后面所有处理都会顺畅很多。

清洗方面,我制定了三条规则。第一,把全角字母数字转半角,避免同一个词因为全半角差异被拆成两个。第二,去掉章节号干扰,像"第一回""话说""且听下回分解"这类套话,如果不处理,会变成高频噪声词挤占词频榜。第三,删除空行和孤立标点行。这里要特别注意,不能粗暴地把所有标点删掉,因为后续情感分析还要依赖感叹号、问号这些语气标点做特征。

章节切割我依赖正则匹配"第[一二三...百千零〇]+回"这类模式。《红楼梦》《三国演义》《水浒传》用"回"做章节单位,《西游记》也是回目体,所以统一按回切分即可。每回标题作为该章节的名称字段存下来,正文单独存放。

2.2 分词调优:自定义词典与词性过滤

结巴对现代汉语支持不错,但遇到古典专有名词就露馅了。我第一次跑分词,发现"贾宝玉"经常被切成"贾/宝玉","诸葛亮"被切成"诸葛/亮","智取生辰纲"这种动宾结构更是被切得七零八落。

解决办法是构建自定义词典。我把四本书里的人名、地名、兵器名、特殊词汇整理成一个 userdict.txt,每行一个词,可以附带词频和词性。比如:

贾宝玉 1000 nr 林黛玉 800 nr 怡红院 100 ns 风月宝鉴 10 nz

这里有个细节值得说。自定义词典中的词频值不是随便写的,它表示该词被优先识别的权重。实际使用中,我建议把词频设置得比你预估的真实出现次数略高一些,这样结巴在动态规划计算最大路径时更倾向保留这个词。但也不能高得离谱,否则会把正常组合也强行粘在一起。我试过把"宝玉"设置为超高词频,结果"通灵宝玉"被切成"通灵/宝玉",反而破坏了原词。

去停用词也是一个反复迭代的过程。除了"的、了、一、是"这些通用停用词,我还加了两个有针对性的列表。第一个是叙事套话,如"话说、且说、只见、正是、未知";第二个是回目残留词,如"第一回、第二回、话说、下回"。这两个列表是通过观察前三章的词频Top50后手动筛出来的,迭代两轮之后,词频榜上基本看不到无意义的高频词了。

2.3 人名实体识别与消歧策略

实体识别这里没有直接用现成的 NER 模型,因为预训练模型对古典人名覆盖度不够,而且容易把"行者""二郎神"这类称号误判为普通名词。我的方案是词典匹配加规则校验。

在自定义词典基础上,我又整理了一份"核心人物别名表"。因为四大名著里同一个人有多个称呼,比如孙悟空又作"齐天大圣""行者""孙行者",《红楼梦》里贾宝玉的称呼也不少。做成别名映射表后,统计人物出场次数时可以把不同称呼合并到同一个人名下,否则统计结果会被严重分散。

消歧还有一个容易翻车的地方:不同人物共用同一个称呼。《三国演义》里"公"是对人物的尊称,《水浒传》里"公"是官名,这种词如果简单归入某个人物,统计会失真。我的处理方式是加白名单和黑名单,白名单是确定指向某个人物的独有称呼,比如"武圣"指向关羽;黑名单是过于通用的称谓,如"公""君""大人",不参与人物归并,只保留在原文本里。

3. 核心算法功能拆解

3.1 词频统计与关键词提取

词频统计是所有分析的地基。这块看起来简单,但要做出有用的结果,需要额外做三步处理。

第一步,统计前先过滤停用词,否则榜单前二十全被虚词占领。第二步,按词性做聚合统计,名词、动词、形容词分开看。这一步很有价值,比如《红楼梦》名词高频词能反映出大观园的日常生活场景,动词高频词能看出叙事重心。第三步,统计单位分全书级和章节级两级。全书级用来观察整体特征,章节级用来定位每回的主题词。

关键词提取我同时用了 TF-IDF 和 TextRank。TF-IDF 的关键在于它把"在本文出现多但在其他回出现少"的词筛了出来,特别适合找每回独有的线索。TextRank 则基于词共现关系计算权重,更像在找文本内部的核心概念。两套结果交叉对比,能发现很多有意思的现象。比如某一回里 TF-IDF 榜首是"埋伏",TextRank 榜首是"计策",说明这一回表面写军事行动,内核写权谋较量。

3.2 人物共现关系网络构建

人物关系网络是这个项目里我最感兴趣的部分。核心算法是共现分析。

思路是这样的:按照固定的窗口长度滑过正文,比如每个段落内或每回内,统计窗口中是否同时出现两个核心人物。如果出现,就在这两个人物的关系矩阵中加一。窗口越大,引入的噪声越多;窗口越小,又容易漏掉跨度较长的关系线。我实测下来,段落级共现比固定滑窗更符合叙事逻辑,因为一个段落通常描述一个连续场景,出现在同一段的人更可能真的产生互动。

有了共现矩阵,就可以算中心度。度中心度反映谁和最多人产生关联,介数中心度反映谁在人物网络中起"桥梁"作用。我跑完《红楼梦》的数据后发现,度中心度最高的几位和直觉基本吻合,但介数中心度能筛出一些平时存在感不强、却串联起多个圈子的关键配角。这类结果放在文学解读里也有参考价值,不只是技术自嗨。

3.3 情感分析与章节维度洞察

情感分析我采用基于情感词典的极性打分法,而不是深度模型。原因很实在:古白话的句式结构和现代语料差异较大,预训练模型的情感判断不一定可靠,而情感词典的可解释性强,哪句话被算成负向、因为哪个词触发,都能明确回溯。

实现分三步。第一,用情感词典匹配句子中的情感词,给正面词和负面词分别计分,权重可调。第二,识别否定词,比如"不、莫、休、无",如果否定词出现在情感词前面,反转情感极性。第三,处理程度副词,像"甚、极、十分、颇",会把情感分数放大或缩小。

情感分数按回聚合后,可以画出整本书的情感走势曲线。这一步很直观,比如某部书从开局到结局的情感波动,能明显看到几个低谷段,往往对应重大悲剧事件发生的回目。曲线和情节结构的对应关系,本身就是一种可讨论的发现。

4. 实操落地:环境、代码与调参细节

4.1 环境搭建与依赖清单

这个项目对硬件几乎没有要求,普通笔记本就能跑。我用的是 Python 3.9 环境,依赖包就五个:jieba、chardet、gensim、pyecharts、pandas。第一次跑建议用虚拟环境,避免污染全局包。

安装命令:

pip install jieba chardet gensim pyecharts pandas

如果网络比较慢,可以把 pip 源切换为国内镜像,这个不多说了。

4.2 数据预处理模块代码

数据读取和清洗是整个项目最繁琐的部分,但也是后面所有分析的基础。核心代码如下:

import chardet import re import jieba def detect_encoding(file_path): with open(file_path, 'rb') as f: data = f.read(50000) result = chardet.detect(data) return result['encoding'] def clean_text(text): # 全角转半角 text = text.replace(' ', ' ') # 去掉孤立空行的多余换行 text = re.sub(r'\n\s*\n', '\n', text) # 去掉回目套话(按需扩充) text = re.sub(r'话说|却说|且说|未知|正是|毕竟|下回分解', '', text) return text def split_chapters(text): pattern = re.compile(r'第[一二三四五六七八九十百千零〇两]+回') matches = list(pattern.finditer(text)) chapters = [] for i, match in enumerate(matches): title = text[match.start(): match.end() + 8] end = matches[i + 1].start() if i + 1 < len(matches) else len(text) content = text[match.end() + 8: end] chapters.append({'title': title, 'content': content}) return chapters

这段代码里split_chapters的切片边界值得注意。标题部分我是取了"第X回"后面8个字符,用来装回目名,正文从回目结束后开始取。这个长度不是固定的,有的回目特别长,所以我后来改为用下一个标题出现的位置作为回目结束点,比盲目取固定8字符要稳得多。

4.3 人物共现网络与情感分析关键实现

共现矩阵的思路是把每一回切分成段落,统计段落内人物名出现的情况。

def build_co_occurrence(chapters, person_list, window_type='paragraph'): matrix = {p: {q: 0 for q in person_list} for p in person_list} for chapter in chapters: paragraphs = re.split(r'\n', chapter['content']) for para in paragraphs: appear = {} for person in person_list: count = para.count(person) if count > 0: appear[person] = count for p in appear: for q in appear: if p != q: matrix[p][q] += 1 return matrix

情感分析我维护了三个词典:正向词典、负向词典、否定词表。打分时对每个句子先找出情感词,再向前扫描2个词范围内是否有否定词或程度副词。

def sentence_sentiment(sentence, pos_dict, neg_dict, neg_words): score = 0.0 words = list(jieba.cut(sentence)) for idx, w in enumerate(words): base = 0 if w in pos_dict: base = 1.0 elif w in neg_dict: base = -1.0 if base != 0: # 向前找否定词 for j in range(max(0, idx - 2), idx): if words[j] in neg_words: base = -base break score += base return score

实际跑出来的数据中我发现一个问题:纯按句子累加会导致长句较多、人物对话密集的回目情感绝对值偏高,短句为主的章节分数偏低。解决方案是归一化,用该回的总句数或总字数去除情感总值,这样跨章节比较才有意义。

4.4 可视化结果展示

可视化我用 pyecharts 渲染两个核心图表。人物关系网络图把共现矩阵转成节点和边,节点大小映射度中心度,边的粗细映射共现次数。章节情感折线图把每回的情感均值按顺序连成一条线,配合每回标题,能直观看到整本书的情绪起伏。

生成 HTML 后可以直接交给前端展示,也可以嵌入其他页面。这个方案对不熟悉前端的人很友好,所有交互图表都是浏览器原生支持的,不需要额外开服务。

5. 踩坑记录与优化实录

5.1 常见问题速查

这个项目踩坑的点集中在数据层和分析层,我把高频问题整理成一张速查表,方便复现时对照。

现象根本原因解决方案
乱码TXT编码不一致用chardet探测后统一 UTF-8
人名被切开词频权重不足自定义词典中提高词频值
词频榜全是虚词停用词表太简单加入叙事套话和回目残留词
同一人物统计分散别名表缺失建别名映射合并多个称呼
情感曲线谷值异常未处理否定词加入否定词反转逻辑
人物关系边过多窗口粒度过大改为段落级共现
章节切割错位回目标题长度不固定用下一个匹配位置定位

这里重点说一个容易被忽略的问题。共用称呼导致的人物统计误差,比如"小姐""夫人"这类词在几部书里都会出现,如果直接加进人物列表,会把大量无关段落错误关联到某个角色身上。我后来把这类称呼全部移出了人物词典,只保留专有名和独有称号,准确率提升明显。

5.2 性能优化与后续扩展思路

全量跑一遍四大名著,分词耗时是最大头,但用精度优先的jieba默认模式下也就一分多钟。如果想要更快,可以考虑关闭 HMM 新词发现,代价是牺牲部分未登录词的识别效果。程序里我把这个开关做成了配置项,默认开,做速度测试时关。

内存方面,四本小说全量文本加载进来不到 100MB,共现矩阵如果人物数量控制在两百人以内,内存完全无压力。但如果你把全部出场人物都加进来,矩阵会快速增长。建议设一个角色阈值,只统计出场次数达到一定标准的人物,既能控制矩阵规模,也能滤掉只出现一两次的路人角色。

后续扩展方向,我想到三个。一是加入基于 word2vec 的相似度检索,把每一回文本映射成向量,查任意一回的内容与哪几回最相似。二是引入预训练语言模型做人物对话级情感识别,会比词典方法更细腻。三是把四本书的人物关系网络合并成一个大图,做跨书角色模式的对比分析。这三个方向都建立在现有数据层之上,不需要推倒重来。

最后再分享一个我在实际项目中的体会。这类文本挖掘项目,真正耗时的地方往往不是算法本身,而是文本清洗、词典构建、结果校验这些看起来不起眼的工作。很多时候跑出来的数据有问题,问题不在模型,而在语料脏或实体映射不准。所以别急着上复杂模型,先把数据捋干净,把规则词典做扎实,后续一切分析才能站在可靠的基础上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询