简介:这是一份面向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', encoding='utf-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_path='simhei.ttf', # 中文字体路径 width=1200, height=800, background_color='white', max_words=200, colormap='viridis', random_state=42 ) wc.generate_from_frequencies(top_words) plt.figure(figsize=(12, 8)) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig('sanguo_wordcloud.png', dpi=200, bbox_inches='tight')generate_from_frequencies直接接受字典,比从文本生成更可控。random_state固定后每次生成的布局一致,方便对比不同参数的效果。保存时dpi=200保证打印清晰度,bbox_inches='tight'去掉多余白边。这一步跑完你就能得到一张基础词云图,但人物关系还没体现出来。
3. 人物关系共现矩阵:谁和谁真正“同框”
3.1 共现窗口的设定与滑动逻辑
人物关系的核心是共现:如果两个名字在同一个窗口内出现,就认为他们有关系。窗口大小是关键参数。按段落切分是最自然的做法,因为《三国演义》的段落通常围绕一个事件展开。但有些段落很长,比如战争描写,可能同时出现十几个人物,这时候窗口太大,关系会变得模糊。我一般用“句子级窗口 + 段落级回退”:先按句号、问号、感叹号切分句子,如果句子太短(少于 10 个字),就合并到相邻句子,直到满足最小长度。这样既保留了对话中的紧密关系,又不会把无关人物硬凑在一起。
def split_sentences(text, min_len=10): # 按中文标点切分 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_len=10是经验值,低于这个长度说明句子信息量不足,合并后能减少噪声。切分完的句子列表就是共现统计的基本单元。
3.2 共现矩阵的构建与归一化
有了句子列表和人物名单,就可以构建共现矩阵。矩阵的行和列都是人物,值是两个人在同一句子中出现的次数。注意要去重:同一句话里“关羽”出现两次只算一次共现。归一化用 Jaccard 相似度或简单共现次数除以两人各自出现总次数的几何平均,前者更稳健。
import numpy as np import pandas as pd # 人物名单 with open('sanguo_names.txt', 'r', encoding='utf-8') as f: names = [line.split()[0] for line in f if line.strip()] name_set = set(names) co_matrix = pd.DataFrame(0, index=names, columns=names, dtype=int) 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(i+1, len(present)): co_matrix.loc[present[i], present[j]] += 1 co_matrix.loc[present[j], present[i]] += 1 # 归一化:除以两人出现总次数的几何平均 total = co_matrix.sum(axis=1) 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, size=word_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, weight=norm_matrix.loc[i, j]) pos = nx.spring_layout(G, k=0.8, seed=42) 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_size=node_sizes, node_color='skyblue', alpha=0.8) nx.draw_networkx_edges(G, pos, width=edge_widths, alpha=0.5) nx.draw_networkx_labels(G, pos, font_size=10, font_family='SimHei') plt.axis('off') plt.savefig('sanguo_network.png', dpi=200, bbox_inches='tight')threshold=0.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.ttf,Linux 用/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(vocabulary=names, tokenizer=lambda x: x.split(), lowercase=False) # 这里需要把每章文本转成空格分隔的词序列 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(i+1, len(names)): if sim[i, j] > 0.3: G2.add_edge(names[i], names[j], weight=sim[i, j]) communities = greedy_modularity_communities(G2) for idx, comm in enumerate(communities): print(f'社区 {idx+1}: {", ".join(comm)}')vocabulary=names强制只统计人物名,tokenizer用空格切分是因为前面已经把词用空格连起来了。sim[i, j] > 0.3是相似度阈值,调低会纳入更多弱关系。社区发现的结果通常能把刘关张、曹操谋士团、孙权周瑜等分到不同组,验证了算法的有效性。这套流程跑下来,你得到的不仅是一张词云图,而是一个可交互、可验证的人物关系分析框架。我自己的习惯是每次调整窗口大小或阈值后,都把网络图存一份带时间戳的版本,方便回溯对比。希望帮到你。
本文还有配套的精品资源,点击获取