古典文本NLP实践:四大名著分词、人物关系与情感分析
最近把四大名著的全量文本完整跑了一轮自然语言处理从语料清洗、分词、实体识别到人物关系网络和情感曲线做成了一个综合程序。这篇文章把整个项目从立项到落地拆开讲设计思路、数据处理、算法选型、踩过的坑以及可以直接复用的代码和参数。先说明一下这个项目定位。它是一个面向中文文本挖掘的练手级综合程序输入是本地四大名著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_typeparagraph): 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 的相似度检索把每一回文本映射成向量查任意一回的内容与哪几回最相似。二是引入预训练语言模型做人物对话级情感识别会比词典方法更细腻。三是把四本书的人物关系网络合并成一个大图做跨书角色模式的对比分析。这三个方向都建立在现有数据层之上不需要推倒重来。最后再分享一个我在实际项目中的体会。这类文本挖掘项目真正耗时的地方往往不是算法本身而是文本清洗、词典构建、结果校验这些看起来不起眼的工作。很多时候跑出来的数据有问题问题不在模型而在语料脏或实体映射不准。所以别急着上复杂模型先把数据捋干净把规则词典做扎实后续一切分析才能站在可靠的基础上。