三国人物关系词频分析与词云图:从文本到关系图谱的完整落地 简介这是一份面向Python文本分析与数据可视化学习者的三国人物关系词频分析实战资源围绕《三国演义》文本展开帮助读者掌握从原始文本到词云图与人物社交关系图的完整处理链路。压缩包共16个文件约6.73MB包含py分析脚本、txt文本与停用词表、ttf中文字体、png词云图与关系网络图、doc设计报告以及xml等工程配置覆盖代码、数据、成果与文档四类内容。项目涉及TXT文本读取、jieba分词、停用词与标点过滤、Counter词频统计、wordcloud词云生成等关键环节并延伸至人物社交关系网络的可视化呈现。已有959人学习下载适合具备Python基础、希望练习中文分词与NLP入门分析的读者参考可据此复现刘备、关羽、张飞及曹操、孙权等人物互动关系的量化结果理解文本预处理与统计可视化在实际项目中的衔接方式。1. 三国人物关系词频分析词云图从文本到关系图谱的完整落地拿到《三国演义》原文想搞清楚谁和谁互动最多、哪些人物是真正的“社交枢纽”光靠翻书肯定不现实。词频分析和词云图是两条最直接的路径词频告诉你哪些名字被反复提及词云把这种频率变成一眼可辨的视觉权重而人物关系分析则更进一步统计同一段落或同一句话里两个人名共现的次数从而推断关系的亲疏远近。这套方案适合有基础 Python 能力、想快速上手文本挖掘的开发者也适合需要做文学量化分析的研究者。整个流程不依赖复杂模型核心就是分词、统计、可视化三步但每一步都有细节决定最终图的质量。下面从数据准备开始把每个环节的参数和坑讲清楚。2. 数据准备与分词三国文本怎么切才不翻车2.1 语料获取与清洗的必做步骤《三国演义》的原文文本可以从公开古籍库获取常见格式是 txt 或 epub。拿到文本后第一件事不是直接分词而是清洗。原文里夹杂着大量非正文内容章节标题、页眉页脚、注释、标点符号混用。我一般会先用正则把“第X回”这类标题单独提取出来作为章节标记方便后续按回目做共现统计。清洗时重点处理三类噪声全角空格、换行符不统一、以及“曰”“云”这类对话标记后面紧跟的标点。如果不清洗分词工具会把“曰”和后面的人名粘在一起导致“关羽曰”被切成一个词人物名就丢了。import re def clean_text(raw_text): # 统一换行符 text raw_text.replace(\r\n, \n).replace(\r, \n) # 去掉全角空格和多余空白 text re.sub(r[ \s], , text) # 去掉括号注释如【】、内的内容 text re.sub(r[【(][^】)]*[】)], , text) # 保留中文、标点和换行去掉其他乱码 text re.sub(r[^\u4e00-\u9fa5。、“”‘’\n], , text) return text with open(sanguo.txt, r, encodingutf-8) as f: raw f.read() cleaned clean_text(raw)这段代码的逻辑是先统一格式再剔除注释和非法字符。参数上[^\u4e00-\u9fa5...]这个正则决定了保留哪些字符如果你用的文本里有生僻字或异体字需要把对应的 Unicode 范围加进去。清洗完的文本建议先打印前 500 字肉眼检查一遍确认没有把正文误删。2.2 分词工具选型与自定义词典中文分词工具常见的有 jieba、pkuseg、HanLP。做三国人物分析jieba 足够用但必须加载自定义词典。因为“诸葛亮”“司马懿”“夏侯惇”这些名字在通用词典里可能被切碎比如“诸葛”和“亮”分开“夏侯”和“惇”分开。自定义词典的格式是“词语 词频 词性”词频可以给一个较高的值确保不被切开。import jieba # 加载自定义人物词典 jieba.load_userdict(sanguo_names.txt) # 词典内容示例 # 诸葛亮 1000 nr # 司马懿 1000 nr # 夏侯惇 1000 nr # 关羽 1000 nr # 张飞 1000 nr def segment(text): words jieba.lcut(text) # 过滤掉单字和标点 words [w for w in words if len(w) 1 and not re.match(r^[。、“”‘’]$, w)] return words words segment(cleaned)这里的关键参数是len(w) 1因为单字在三国文本里大量出现但单字对人物关系分析几乎没有价值。如果你要做词云单字可以保留但做共现矩阵时一定要过滤。自定义词典需要你手动整理一份三国人物名单常见做法是从百科或公开的人物表里提取大概 200 到 300 个名字就够覆盖主要角色。2.3 词频统计与词云生成的最小命令词频统计用collections.Counter即可词云用wordcloud库。生成词云时有两个参数最影响效果font_path必须指定中文字体否则全是方块max_words控制显示多少个词一般设 100 到 200 之间太多会挤成一团。from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as plt word_counts Counter(words) # 取前 200 个高频词 top_words dict(word_counts.most_common(200)) wc WordCloud( font_pathsimhei.ttf, # 中文字体路径 width1200, height800, background_colorwhite, max_words200, colormapviridis, random_state42 ) wc.generate_from_frequencies(top_words) plt.figure(figsize(12, 8)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.savefig(sanguo_wordcloud.png, dpi200, bbox_inchestight)generate_from_frequencies直接接受字典比从文本生成更可控。random_state固定后每次生成的布局一致方便对比不同参数的效果。保存时dpi200保证打印清晰度bbox_inchestight去掉多余白边。这一步跑完你就能得到一张基础词云图但人物关系还没体现出来。3. 人物关系共现矩阵谁和谁真正“同框”3.1 共现窗口的设定与滑动逻辑人物关系的核心是共现如果两个名字在同一个窗口内出现就认为他们有关系。窗口大小是关键参数。按段落切分是最自然的做法因为《三国演义》的段落通常围绕一个事件展开。但有些段落很长比如战争描写可能同时出现十几个人物这时候窗口太大关系会变得模糊。我一般用“句子级窗口 段落级回退”先按句号、问号、感叹号切分句子如果句子太短少于 10 个字就合并到相邻句子直到满足最小长度。这样既保留了对话中的紧密关系又不会把无关人物硬凑在一起。def split_sentences(text, min_len10): # 按中文标点切分 raw_sents re.split(r[。], text) sents [] buffer for s in raw_sents: s s.strip() if not s: continue buffer s if len(buffer) min_len: sents.append(buffer) buffer if buffer: sents.append(buffer) return sents sentences split_sentences(cleaned)min_len10是经验值低于这个长度说明句子信息量不足合并后能减少噪声。切分完的句子列表就是共现统计的基本单元。3.2 共现矩阵的构建与归一化有了句子列表和人物名单就可以构建共现矩阵。矩阵的行和列都是人物值是两个人在同一句子中出现的次数。注意要去重同一句话里“关羽”出现两次只算一次共现。归一化用 Jaccard 相似度或简单共现次数除以两人各自出现总次数的几何平均前者更稳健。import numpy as np import pandas as pd # 人物名单 with open(sanguo_names.txt, r, encodingutf-8) as f: names [line.split()[0] for line in f if line.strip()] name_set set(names) co_matrix pd.DataFrame(0, indexnames, columnsnames, dtypeint) for sent in sentences: present set() for name in names: if name in sent: present.add(name) present list(present) for i in range(len(present)): for j in range(i1, len(present)): co_matrix.loc[present[i], present[j]] 1 co_matrix.loc[present[j], present[i]] 1 # 归一化除以两人出现总次数的几何平均 total co_matrix.sum(axis1) norm_matrix co_matrix.copy().astype(float) for i in names: for j in names: if i ! j and total[i] 0 and total[j] 0: norm_matrix.loc[i, j] co_matrix.loc[i, j] / np.sqrt(total[i] * total[j])这段代码的效率在 200 人规模下完全够用如果人物名单扩大到 1000 人以上需要改用稀疏矩阵。归一化后的值在 0 到 1 之间越大表示关系越紧密。total[i]是第 i 个人物出现的总句子数几何平均能抑制高频人物的权重膨胀。3.3 关系网络图与词云图的叠加呈现共现矩阵可以直接导出成网络图用networkx加matplotlib画。节点大小对应人物词频边粗细对应归一化共现强度。如果想和词云结合可以把词云作为背景网络图叠加在上面但这样容易乱。更实用的做法是并排展示左边词云看频率右边网络图看关系。import networkx as nx G nx.Graph() for name in names: G.add_node(name, sizeword_counts.get(name, 0)) threshold 0.05 # 只保留归一化共现大于阈值的边 for i in names: for j in names: if i j and norm_matrix.loc[i, j] threshold: G.add_edge(i, j, weightnorm_matrix.loc[i, j]) pos nx.spring_layout(G, k0.8, seed42) node_sizes [G.nodes[n][size] * 2 for n in G.nodes] edge_widths [G.edges[e][weight] * 5 for e in G.edges] plt.figure(figsize(14, 10)) nx.draw_networkx_nodes(G, pos, node_sizenode_sizes, node_colorskyblue, alpha0.8) nx.draw_networkx_edges(G, pos, widthedge_widths, alpha0.5) nx.draw_networkx_labels(G, pos, font_size10, font_familySimHei) plt.axis(off) plt.savefig(sanguo_network.png, dpi200, bbox_inchestight)threshold0.05是过滤弱关系的阈值调高会让图更干净但可能丢失边缘人物。spring_layout的k参数控制节点间距值越大越分散。节点大小乘以 2 是为了视觉上更明显边宽乘以 5 同理。这张图能直观看出诸葛亮、曹操、刘备这些核心节点的连接密度。4. 避坑与排查词频分析和关系图常见的五个翻车点4.1 分词把人物名切碎导致词频失真现象词云里出现“诸葛”“司马”“夏侯”单独成词而“诸葛亮”频率很低。原因jieba 默认词典没有覆盖三国人物全名或者自定义词典加载失败。解决检查sanguo_names.txt的编码是否为 UTF-8每行格式是否为“词语 词频 词性”加载后打印jieba.lcut(诸葛亮)确认输出是完整名字。如果仍然切碎把词频调到 10000 以上强制优先匹配。4.2 词云中文显示为方块现象生成的词云图里所有汉字都是方框。原因font_path指向的字体不支持中文或者路径错误。解决用绝对路径指定中文字体Windows 常用C:/Windows/Fonts/simhei.ttfLinux 用/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。确认文件存在后再跑不要用simsun.ttc这类可能缺失的字体。4.3 共现矩阵过于稀疏或过于稠密现象网络图要么只有几个孤立的点要么所有节点连成一团。原因共现窗口的min_len设置不当或者归一化阈值threshold不合理。解决先统计每句话平均包含多少个人物名如果平均超过 5 个说明窗口太大把min_len调低到 5 或按段落切分如果平均少于 1 个说明窗口太小调高到 20。阈值从 0.1 开始试观察边数变化。4.4 高频人物掩盖了次要人物的关系现象网络图里曹操、刘备、诸葛亮占据绝对中心其他人物几乎看不见。原因节点大小直接用了原始词频高频人物数值太大。解决对节点大小做对数变换np.log1p(word_counts)或者用归一化后的词频。边宽也做类似处理避免少数强边压过其他边。4.5 章节标题被当作人物名统计现象词频列表里出现“第一回”“宴桃园”这类词。原因清洗时没有去掉回目标题分词后这些词被保留。解决在清洗阶段用正则第[一二三四五六七八九十百]回匹配并删除或者在分词后过滤掉包含“回”“章”“节”的词。更彻底的做法是先把回目行单独提取不参与正文分词。5. 进阶技巧用 TF-IDF 和社区发现挖出隐藏关系基础共现矩阵只能看直接同框但三国里很多关系是间接的比如“关羽”和“曹操”通过“张辽”产生联系。这时候可以用 TF-IDF 加权和社区发现算法。TF-IDF 的思路是如果一个人物在某个章节里频繁出现但在全书里出现次数不多那他在这个章节里的权重应该更高。具体做法是把每个回目当作一篇文档计算每个人物在各回目的 TF-IDF 值然后用余弦相似度构建人物关系。社区发现用networkx的greedy_modularity_communities能自动把人物分成几个派系比如蜀汉、曹魏、东吴。from sklearn.feature_extraction.text import TfidfVectorizer from networkx.algorithms.community import greedy_modularity_communities # 按回目切分文档 chapters re.split(r第[一二三四五六七八九十百]回, cleaned) chapters [c for c in chapters if len(c) 100] # 每个人物作为一个特征词统计在各回目的出现次数 vectorizer TfidfVectorizer(vocabularynames, tokenizerlambda x: x.split(), lowercaseFalse) # 这里需要把每章文本转成空格分隔的词序列 chapter_words [ .join(segment(ch)) for ch in chapters] tfidf vectorizer.fit_transform(chapter_words) # 用 TF-IDF 矩阵的列向量计算人物相似度 from sklearn.metrics.pairwise import cosine_similarity sim cosine_similarity(tfidf.T) # 构建图并做社区发现 G2 nx.Graph() for i, name in enumerate(names): G2.add_node(name) for i in range(len(names)): for j in range(i1, len(names)): if sim[i, j] 0.3: G2.add_edge(names[i], names[j], weightsim[i, j]) communities greedy_modularity_communities(G2) for idx, comm in enumerate(communities): print(f社区 {idx1}: {, .join(comm)})vocabularynames强制只统计人物名tokenizer用空格切分是因为前面已经把词用空格连起来了。sim[i, j] 0.3是相似度阈值调低会纳入更多弱关系。社区发现的结果通常能把刘关张、曹操谋士团、孙权周瑜等分到不同组验证了算法的有效性。这套流程跑下来你得到的不仅是一张词云图而是一个可交互、可验证的人物关系分析框架。我自己的习惯是每次调整窗口大小或阈值后都把网络图存一份带时间戳的版本方便回溯对比。希望帮到你。本文还有配套的精品资源点击获取