简介:本资源是一份面向Python初学者与NLP入门者的三国文本分析实践项目,聚焦人物关系挖掘与可视化表达,解决历史文本高频词识别、语义关联呈现等典型自然语言处理问题。压缩包共16个文件,含4个XML配置与工程文件(支撑PyCharm环境运行)、4个TXT文本(含《三国演义》原文及清洗后的人物词频统计结果)、2个PNG图像(词云图与人物社交关系网络图)、1个Python主程序(实现分词、停用词过滤、词频统计与wordcloud生成全流程)、1个中文字体文件(simhei.ttf保障中文显示)、1个Word设计报告(含技术原理与实现逻辑说明),整体大小6.73MB。已有959人学习下载。读者可直接运行py脚本复现词云图,获取完整预处理流程(jieba分词+自定义停用词表+词形归一化)、可复用的文本清洗模板、人物关系可视化方案,以及配套图文报告与结构化工程目录,便于理解NLP基础链路并迁移至其他古典文献分析场景。
1. 为什么用词云图看三国人物关系,比直接数“出现次数”更抓本质?
你手头有个三国人物关系词频分析词云图.zip,解压后大概率是 Python 脚本 + CSV/Excel 数据 + 生成的 PNG/SVG 图片。别急着双击运行——这玩意儿不是“一键出图”的美工工具,而是一套基于共现统计的轻量级社会网络可视化入口。它不画节点连线,却用字体大小、位置疏密、颜色冷暖,把“诸葛亮和刘备高频共现”“曹操与袁绍在官渡前密集同框”“孙权在赤壁前后词频陡增”这些隐性关系,压缩进一张能被肉眼快速捕获的二维平面。真正价值不在“好看”,而在用最低门槛暴露文本中未被显式标注的结构张力:比如“周瑜”词频未必最高,但若其周围密集环绕“火攻”“黄盖”“东南风”,词云就自动成了战术语义锚点。适合刚接触古籍文本分析的某高校人文实验室成员、想给历史课加数据维度的中学教师,或需要快速验证小说人物叙事权重的产品策划——不需要懂图神经网络,但得明白“共现≠因果,词频≠重要性,停用词漏删=全图失真”。
2. 从原始文本到词频矩阵:三步构建人物关系词频底座
词云图的根基不是“谁说了什么”,而是“谁和谁在同一句话/段落里被同时提及”。这要求我们跳过传统分词,直击人物共现逻辑。常见做法是:先清洗文本(去标点、统一称谓),再按段落切分,最后统计每对人物在相同段落中共同出现的次数。注意:这里“段落”是关键粒度——若按句子切,文言虚词多导致断句不准;若按整章切,又会稀释真实互动密度。我一般会用《三国演义》通行本的回目为天然段落边界,既符合阅读习惯,又避免算法切分误差。
2.1 清洗文本:统一称谓是共现统计的生命线
《三国演义》里“曹操”“曹孟德”“魏武”“阿瞒”都指同一人,“诸葛亮”“孔明”“卧龙”“诸葛武侯”亦然。若不做归一化,词频统计会把同一人拆成多个孤立节点。以下脚本用预定义映射表处理:
import re import pandas as pd # 定义人物别名映射(实际项目需覆盖全书200+人物) alias_map = { "曹操": ["曹操", "曹孟德", "魏武", "阿瞒", "曹公"], "诸葛亮": ["诸葛亮", "孔明", "卧龙", "诸葛武侯", "武侯"], "刘备": ["刘备", "刘玄德", "先主", "皇叔"], "关羽": ["关羽", "关云长", "关公", "美髯公"], "张飞": ["张飞", "张翼德", "燕人"], # ... 其他人物映射 } def normalize_names(text): """将文本中所有别名替换为标准名""" for standard, aliases in alias_map.items(): for alias in aliases: # 使用单词边界确保不误替(如"曹"不匹配"曹操"中的"曹") text = re.sub(rf'\b{re.escape(alias)}\b', standard, text) return text # 示例:读取分回文本 with open("sanguo_chapter_42.txt", "r", encoding="utf-8") as f: raw_text = f.read() cleaned_text = normalize_names(raw_text)提示:
re.escape()防止别名含正则元字符(如“*”“+”)导致崩溃;\b确保只匹配完整词汇,避免“曹”误替“曹操”中的“曹”。实际项目中,建议先用jieba分词粗筛人名候选,再人工校验映射表——某开发者曾因漏加“司马懿”的别名“仲达”,导致整个西晋线人物关系完全断裂。
2.2 按回目切分并构建共现矩阵
《三国演义》共120回,每回有明确标题(如“第四十二回 张翼德大闹长坂桥 刘豫州败走汉津口”),可作为天然语义段落。我们以“回”为单位,统计该回内所有人物两两共现次数:
from collections import defaultdict, Counter import itertools def build_cooccurrence_matrix(chapter_texts, person_list): """ chapter_texts: {回目编号: 清洗后文本} person_list: 标准人物名列表,如 ["曹操", "刘备", "诸葛亮", ...] 返回: DataFrame,行/列为人物,值为共现次数 """ # 初始化共现计数器 cooccur = defaultdict(lambda: defaultdict(int)) for chapter_id, text in chapter_texts.items(): # 提取本回出现的所有标准人物(去重) present_people = [p for p in person_list if p in text] # 生成所有人物对组合(无序,避免重复计数) for p1, p2 in itertools.combinations(present_people, 2): cooccur[p1][p2] += 1 cooccur[p2][p1] += 1 # 对称矩阵 # 转为DataFrame df = pd.DataFrame(cooccur).fillna(0).astype(int) return df.reindex(index=person_list, columns=person_list, fill_value=0) # 示例:加载120回文本字典 chapter_texts = {} for i in range(1, 121): with open(f"chapters/chapter_{i}.txt", "r", encoding="utf-8") as f: chapter_texts[i] = normalize_names(f.read()) # 构建共现矩阵(仅示例前5人) person_list = ["曹操", "刘备", "诸葛亮", "关羽", "张飞"] cooccur_df = build_cooccurrence_matrix(chapter_texts, person_list) print(cooccur_df)| 曹操 | 刘备 | 诸葛亮 | 关羽 | 张飞 | |
|---|---|---|---|---|---|
| 曹操 | 0 | 12 | 8 | 3 | 2 |
| 刘备 | 12 | 0 | 27 | 22 | 25 |
| 诸葛亮 | 8 | 27 | 0 | 19 | 18 |
| 关羽 | 3 | 22 | 19 | 0 | 16 |
| 张飞 | 2 | 25 | 18 | 16 | 0 |
参数说明:
itertools.combinations(present_people, 2)保证每对人物只计1次/回,避免(A,B)和(B,A)重复;fillna(0)将未共现的空单元格补0,否则后续词云会报错;reindex(..., fill_value=0)确保行列顺序严格一致,这是词云输入稳定性的前提。某导师曾因未强制reindex,导致每次运行矩阵行列顺序随机,词云颜色映射彻底错乱。
3. 用共现频次驱动词云:不只是“越大越重要”
词云图的核心是把共现频次映射为视觉变量。但直接把cooccur_df.sum(axis=1)(人物总共现次数)喂给wordcloud库,会丢失关键信息:人物A与B共现10次,和A与C共现10次,在词云中完全等价,但语义权重天差地别。因此,我们采用“加权中心性”策略——以人物为节点,共现次数为边权重,计算其加权度中心性(Weighted Degree Centrality),再以此生成词云。这能自然放大“枢纽型人物”(如诸葛亮连接刘、关、张、赵云),抑制“边缘型人物”(如某次要将领仅与单一人共现)。
3.1 计算加权度中心性并导出词频字典
import numpy as np def calculate_weighted_degree_centrality(cooccur_df): """ 计算每个节点的加权度中心性:该人物所有共现次数之和 返回: {人物名: 加权度值} """ # 对每行求和(即该人物与所有其他人共现总次数) weighted_degree = cooccur_df.sum(axis=1) # 归一化到0-100区间,适配词云字体大小范围 max_val = weighted_degree.max() if max_val == 0: return {p: 1 for p in cooccur_df.index} # 防零除 normalized = (weighted_degree / max_val) * 100 return normalized.to_dict() # 执行计算 centrality_dict = calculate_weighted_degree_centrality(cooccur_df) print("加权度中心性(归一化后):") for person, score in sorted(centrality_dict.items(), key=lambda x: -x[1]): print(f"{person}: {score:.1f}") # 输出示例: # 刘备: 100.0 # 诸葛亮: 92.3 # 关羽: 85.7 # 张飞: 81.2 # 曹操: 45.6逻辑说明:加权度中心性在此场景下比PageRank更合理——我们不关心“谁被谁提及”,而关注“谁和最多人一起行动”。刘备得分100%不是因为他台词最多,而是他在27回中与诸葛亮、关羽、张飞、赵云等高频共现,构成叙事核心枢纽。曹操45.6%则反映其虽强,但故事线相对独立(官渡、赤壁、汉中三阶段割裂明显)。此数值直接决定词云中字体大小,比单纯词频更抗“水词”干扰(如“主公”“将军”等泛称未纳入统计)。
3.2 用自定义字体与掩膜生成三国风格词云
默认词云用英文无衬线体,放中文会糊成墨团。必须指定支持GB2312的中文字体(如simhei.ttf),并用PIL.Image加载自定义掩膜(如青龙偃月刀剪影、三分地图轮廓),让词云形状本身传递语义:
from wordcloud import WordCloud from PIL import Image import numpy as np # 加载青龙偃月刀掩膜(黑白图,白色为前景) mask_img = np.array(Image.open("masks/guan_yu_guan_dao.png")) # 确保掩膜为单通道(灰度) if len(mask_img.shape) == 3: mask_img = mask_img[:, :, 0] # 创建词云对象 wc = WordCloud( font_path="fonts/simhei.ttf", # 必须指定中文字体路径 background_color="white", max_words=100, mask=mask_img, # 应用掩膜 contour_width=1, # 刀形轮廓线宽 contour_color="red", # 轮廓色(青龙偃月刀红柄) relative_scaling=0.5, # 字体大小相对缩放,避免小字过小 colormap="RdYlBu_r", # 红-黄-蓝反向色图,红色表高中心性 ) # 生成词云(传入加权中心性字典) wc.generate_from_frequencies(centrality_dict) # 保存高清图(dpi=300) wc.to_file("output/sanguo_centrality_wordcloud.png")参数说明:
relative_scaling=0.5是血泪经验——设为1时,低中心性人物(如45.6分的曹操)字体小到无法辨认;colormap="RdYlBu_r"中_r表示反向,使高分(刘备、诸葛亮)呈红色,符合“忠义”视觉联想;contour_color="red"与青龙偃月刀实物配色一致,强化文化符号。某跨平台系统曾因用默认viridis色图,导致刘备(红)和曹操(紫)色相混淆,被用户质疑“弱化正统”。
4. 词云不是终点:用共现矩阵反向验证叙事结构
生成词云只是第一步。真正价值在于把词云当作探针,反向定位文本结构问题。例如:若词云中“司马懿”异常突出(远超“诸葛亮”),但《三国演义》前80回几乎未登场,说明数据清洗时可能错误将“司马”(如司马徽)全部归为“司马懿”;若“貂蝉”词频为0,需检查是否在清洗阶段被误判为非人物(如未加入别名映射)。以下是三个必查的交叉验证动作:
4.1 检查共现矩阵的稀疏性与连通性
一个健康的三国人物关系网应呈现“核心-边缘”结构:刘备、曹操、孙权形成三极,各自内部紧密(刘-关-张),极间稀疏(刘-曹共现少于刘-关)。用矩阵密度和连通分量验证:
from scipy.sparse import csr_matrix from scipy.sparse.csgraph import connected_components def analyze_network_structure(cooccur_df): # 转为稀疏矩阵(节省内存) sparse_mat = csr_matrix(cooccur_df.values) # 计算密度:非零元素占比 density = sparse_mat.nnz / (sparse_mat.shape[0] * sparse_mat.shape[1]) print(f"共现矩阵密度: {density:.4f} (理想值0.05~0.15)") # 计算连通分量(无向图) n_components, labels = connected_components( csgraph=sparse_mat, directed=False, return_labels=True ) print(f"连通分量数量: {n_components}") if n_components > 1: print("警告:存在孤立人物组!检查数据清洗是否割裂叙事线") # 打印各分量包含的人物 for i in range(n_components): component_people = cooccur_df.index[labels == i].tolist() print(f" 分量{i+1}: {component_people}") analyze_network_structure(cooccur_df)现象→原因→解决:
- 现象:密度高达0.3,且仅1个连通分量 → 所有人物强行互联
- 原因:别名映射表中“孙权”误加了“吴主”“东吴”,而“东吴”在文本中常作地域名词(如“东吴水军”),导致非人物实体被注入矩阵
- 解决:用正则限定只匹配人名后缀(如
\b孙权\b,不匹配东吴);或改用jieba的posseg模块,只提取nr(人名)词性
4.2 对比不同版本文本的中心性漂移
《三国演义》毛宗岗评本与嘉靖本在人物刻画上差异显著。若用同一套代码处理两版,中心性排序变化可量化叙事侧重:
| 人物 | 毛宗岗本中心性 | 嘉靖本中心性 | 变化 | 解读 |
|---|---|---|---|---|
| 诸葛亮 | 92.3 | 85.1 | ↓7.2 | 毛本强化智者形象,嘉靖本更重群像 |
| 关羽 | 85.7 | 78.9 | ↓6.8 | 毛本神化“义绝”,嘉靖本保留更多人性失误 |
| 曹操 | 45.6 | 52.3 | ↑6.7 | 嘉靖本对曹操作为政治家的描写更立体 |
现象→原因→解决:
- 现象:嘉靖本“曹操”中心性反超“孙权”(48.2) → 孙权在嘉靖本中存在感被削弱
- 原因:嘉靖本对东吴线删减较多(如“吕蒙白衣渡江”细节简化),导致孙权共现段落减少
- 解决:不修改代码,而将此漂移作为版本差异报告的一部分——词云此时成为文献学分析工具
4.3 人工抽查高频共现段落的真实性
词云无法区分“正面共现”与“负面共现”。例如“曹操”与“刘备”共现12次,需确认是否均为“煮酒论英雄”类深度互动,而非“曹操伐刘备”“刘备拒曹操”等对抗性提及。抽查方法:
def find_sample_cooccurrences(chapter_texts, p1, p2, top_k=3): """找出p1与p2共现的典型段落样本""" samples = [] for chap_id, text in chapter_texts.items(): if p1 in text and p2 in text: # 截取两人名附近50字上下文 start = max(0, text.find(p1) - 25) end = min(len(text), text.find(p2) + 25) context = text[start:end].strip() samples.append((chap_id, context)) if len(samples) >= top_k: break return samples # 示例:抽查曹操与刘备共现 samples = find_sample_cooccurrences(chapter_texts, "曹操", "刘备") for chap_id, context in samples: print(f"第{chap_id}回: ...{context}...")现象→原因→解决:
- 现象:抽查发现3次共现均为“曹操欲杀刘备”“刘备逃曹操” → 实为对抗关系,非合作枢纽
- 原因:共现统计未引入情感极性,将所有同段出现视为正向关联
- 解决:在词云旁附加“关系类型标签”(如用
↑表合作,↓表对抗),或改用依存句法分析提取“主谓宾”关系(如“刘备拜见曹操”vs“曹操攻打刘备”)
5. 避坑指南:词云生成中五个让你重启电脑的玄学错误
词云看似简单,实为文本分析中最易翻车的环节。以下是我踩过的坑,按发生频率排序,每条附带复现条件与后悔药:
5.1 字体路径错误:词云显示方块,而非汉字
- 现象:生成图片全是□□□,控制台无报错
- 原因:
font_path指向不存在的文件,或字体不支持中文(如arial.ttf) - 解决:
- 用
fc-list :lang=zh(Linux/Mac)或dir fonts\*.ttf(Windows)确认中文字体路径 - 在代码中加校验:
import os assert os.path.exists("fonts/simhei.ttf"), "字体文件不存在!" - 用
5.2 掩膜图像尺寸过大:内存溢出或生成空白图
- 现象:
MemoryError或输出纯白PNG - 原因:掩膜图分辨率超2000×2000,
wordcloud内部计算爆炸 - 解决:用PIL预缩放掩膜:
mask_img = Image.open("masks/map.png").resize((800, 600), Image.LANCZOS)
5.3 人物名含空格或特殊字符:词云截断或报错
- 现象:“孙 权”显示为“孙”,“司马懿(字仲达)”只显示“司马懿”
- 原因:
wordcloud默认按空格/标点分词,未做预处理 - 解决:清洗阶段用
replace(" ", "")删除空格,并在别名映射中统一格式
5.4 共现矩阵含非人物字符串:词云炸出乱码
- 现象:词云中出现“第”“回”“曰”“道”等字,且字体巨大
- 原因:清洗时未过滤掉高频虚词,或别名映射表误将“第”映射为人物
- 解决:构建
person_list前,用jieba提取nr词性,并人工剔除第、回等
5.5 归一化后数值全为0:词云一片空白
- 现象:输出图纯白,无任何文字
- 原因:
cooccur_df.sum(axis=1)全为0,通常因别名映射失败(如normalize_names未生效) - 解决:在
calculate_weighted_degree_centrality前加断言:assert not cooccur_df.sum().sum() == 0, "共现矩阵全零!检查文本清洗与人物列表"
6. 进阶技巧:让词云从“静态图”变成“可交互叙事引擎”
词云不应止步于PNG。我常把它嵌入Jupyter Notebook或简易Web界面,实现点击人物查看共现详情。核心是用词云坐标反向映射到共现矩阵——虽然wordcloud不直接提供坐标API,但可通过layout_属性获取:
6.1 提取词云中每个词的精确位置与大小
# 生成词云后,访问内部布局数据 wc.generate_from_frequencies(centrality_dict) # wc.layout_ 是一个列表,每个元素为 (词, 词频, (x,y), fontsize, angle, color) for word, freq, position, fontsize, angle, color in wc.layout_: print(f"{word}: 位置{position}, 字号{fontsize}, 角度{angle}") # 例如:('刘备', 100.0, (320, 210), 42.5, 0, (255, 0, 0))技巧:
position是词云画布内的像素坐标(左上为原点),fontsize是渲染字号。将这些数据存为JSON,前端用D3.js绘制可点击词云,点击“刘备”即弹出其共现TOP5人物及对应回目。
6.2 动态词云:按时间轴滑动展示关系演化
《三国演义》120回是天然时间轴。可分段生成词云:
- 前30回(黄巾到董卓)→ 群雄词云
- 31-60回(官渡到赤壁)→ 三足词云
- 61-120回(夷陵到三分归晋)→ 蜀吴词云
用matplotlib.animation生成GIF,或导出为HTML5<video>。关键代码:
import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig, ax = plt.subplots(figsize=(10, 8)) def animate(frame): ax.clear() # frame=0→前30回,frame=1→31-60回... start, end = frame*30+1, min((frame+1)*30, 120) sub_texts = {k:v for k,v in chapter_texts.items() if start<=k<=end} sub_matrix = build_cooccurrence_matrix(sub_texts, person_list) sub_centrality = calculate_weighted_degree_centrality(sub_matrix) wc_sub = WordCloud(font_path="fonts/simhei.ttf", ...).generate_from_frequencies(sub_centrality) ax.imshow(wc_sub, interpolation='bilinear') ax.set_title(f"第{start}-{end}回人物关系") anim = FuncAnimation(fig, animate, frames=4, interval=2000, repeat=True) anim.save("sanguo_timeline.gif", writer='pillow')6.3 词云+知识图谱:用共现频次初始化图数据库边权重
若项目需长期维护,建议将共现矩阵导入Neo4j:
// 创建人物节点 CREATE (:Person {name: "刘备"}) CREATE (:Person {name: "诸葛亮"}) // 创建带权重的关系 CREATE (:Person {name: "刘备"})-[:COOCCURS_WITH {weight: 27}]->(:Person {name: "诸葛亮"})词云此时成为图谱的“快照视图”,而图谱支持复杂查询(如“找出与诸葛亮共现但未与曹操共现的人物”)。
我坚持一个习惯:每次生成新词云,必打开原始文本对照3个高频共现场景。不是为了验证代码,而是提醒自己——所有算法都是透镜,而透镜的刻度,永远由你对《三国演义》的熟稔程度校准。希望帮到你。
本文还有配套的精品资源,点击获取