ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python实现武侠小说人物关系网络分析:从爬虫到可视化

Python实现武侠小说人物关系网络分析:从爬虫到可视化 在整理《陆小凤传奇》这部经典武侠作品相关资料时很多读者关注的是剧情本身但对开发者来说这部小说还有一个更有意思的打开方式把它当作中文文本分析、爬虫实战和人物关系挖掘的语料。本文不讨论影视改编和演员信息只围绕公开的小说文本讲清楚如何用 Python 完成从网页抓取、文本清洗、中文分词到人物关系网络构建的完整流程。这套方案适用于很多同类场景比如你想分析《楚留香传奇》的角色关系或者想统计某部网络小说的高频词甚至想搭建一个简单的“小说人物知识图谱”都可以复用下面的代码思路。1. 项目背景与核心概念1.1 为什么要做“小说文本分析”武侠小说有一个非常适合程序处理的特点人物多、人名固定、章节结构清晰。以《陆小凤传奇》为例主角团包含陆小凤、花满楼、西门吹雪、叶孤城、司空摘星等还有大量配角分布在各个单元故事中。如果靠人工去统计“谁和谁在哪些章节同时出现”工作量会非常大而且容易出现遗漏。文本分析可以把这个过程自动化用爬虫拿到全文文本。用分词工具把人名识别出来。按章节统计人物出现次数。用“共现关系”判断人物之间的关系强度。用网络图把关系可视化。这个思路不仅适用于武侠小说也能用在新闻人物关系分析、上市公司高管网络分析、历史人物关系挖掘等方向。1.2 核心概念说明先梳理几个关键术语方便后续代码理解。术语含义在本文中的作用爬虫自动获取网页数据的程序获取小说章节正文文本清洗去除无关字符、统一格式把网页正文整理成干净的小说文本中文分词把连续汉字切分成词语识别“陆小凤”“花满楼”等人名共现关系两个实体在同一个窗口内同时出现衡量人物之间是否有关联关系网络用节点和边表示实体关系可视化人物关系结构1.3 项目整体流程整个项目可以分为以下步骤后面会逐个实现获取小说目录页提取章节链接。逐章抓取正文保存为本地文本文件。对全文进行清洗按章节切分。使用 jieba 分词结合自定义人名词典完成人名识别。统计人物出场次数分析核心角色。基于滑动窗口统计人物共现关系生成关系边。使用 networkx 绘制人物关系网络图。2. 环境准备与版本说明2.1 运行环境本文示例使用 Python 3.9 环境在 Windows / macOS / Linux 上都可以运行。建议使用虚拟环境隔离依赖避免和系统 Python 环境冲突。python -m venv venv source venv/bin/activate # Linux / macOS # venv\Scripts\activate # Windows2.2 依赖库安装项目需要以下 Python 库requests发送 HTTP 请求获取网页内容。beautifulsoup4解析 HTML提取正文。lxmlBeautifulSoup 的解析器性能更好。jieba中文分词工具。networkx构建和绘制关系网络。matplotlib配合 networkx 输出关系图。安装命令pip install requests beautifulsoup4 lxml jieba networkx matplotlib需要注意的是matplotlib在部分 Linux 服务器上可能需要额外安装中文字体否则图片中的中文会显示成方框后面会专门讲解决方案。2.3 项目目录结构建议按下面的结构组织项目lu_xiao_feng_analysis/ ├── data/ │ ├── raw/ │ │ └── lu_xiao_feng.txt # 爬取到的原始全文 │ ├── characters.txt # 自定义人名词典 │ └── stopwords.txt # 停用词表 ├── output/ │ ├── character_count.csv # 人物出场统计 │ ├── relation.csv # 人物共现关系 │ └── relation_graph.png # 关系网络图 ├── spider.py # 爬虫脚本 └── analysis.py # 文本分析脚本这里的目录结构不是强制要求但建议把“原始数据”和“计算结果”分开方便后续重新分析。3. 核心原理拆解3.1 网页抓取原理爬虫的本质是模拟浏览器向服务器发起 HTTP 请求拿到 HTML 响应后再从中提取你需要的内容。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders) print(resp.status_code) print(resp.encoding)这里最重要的是两点User-Agent不能省略很多网站会拦截没有浏览器标识的请求。resp.encoding需要根据网页实际情况设置常见的有utf-8、gbk、gb2312否则可能出现中文乱码。3.2 正文提取策略拿到 HTML 之后需要定位正文所在的标签。不同网站的页面结构不同一般有两种方式使用 BeautifulSoup 的find/select方法通过标签名、class、id 定位。使用正则表达式针对固定格式的 HTML 做提取。推荐优先使用 BeautifulSoup因为正则表达式对 HTML 结构变化非常敏感一旦页面改版就容易失效。from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) title soup.find(h1).get_text() content soup.find(div, idcontent).get_text()需要注意get_text()会把标签内部的所有文本拼接起来但不同的网站正文标签属性不一样必须根据实际页面调整选择器。3.3 中文分词与人名识别jieba 是 Python 中最常用的中文分词库使用方式很简单import jieba words jieba.lcut(陆小凤和花满楼一起走进了小楼) print(words) # [陆小凤, 和, 花满楼, 一起, 走进, 了, 小楼]但 jieba 自带词典对古龙小说中的人名覆盖并不完整像“司空摘星”“沙曼”“上官飞燕”这些名字有时候会被切碎。解决办法是加载自定义词典jieba.load_userdict(data/characters.txt)自定义词典的格式很简单每行一个词可以附带词频和词性陆小凤 100 nr 花满楼 100 nr 西门吹雪 100 nr 司空摘星 100 nr其中nr表示“人名”词性这样 jieba 在分词时会更倾向于把这些词识别为人名。3.4 人物共现关系判断两个人物是否有关系常用“共现”方法如果两个人物在同一个窗口比如同一段话、同一章节内同时出现就认为这两个人物存在关联。窗口大小会影响结果窗口太小只能捕捉近距离互动但会漏掉章节级别的关系。窗口太大会把同章节内没有直接互动的人也关联起来产生很多噪声。本文采用“章节内段落共现”的方式设定滑动窗口为 50 个词两个人物出现在同一个窗口内就记录一次共现。def build_co_occurrence(words, window_size50): relations {} for i in range(len(words)): for j in range(i 1, min(i window_size, len(words))): w1, w2 words[i], words[j] if w1 in name_set and w2 in name_set: key tuple(sorted([w1, w2])) relations[key] relations.get(key, 0) 1 return relations这里的name_set是人名集合这样只统计两个都是人名的情况忽略普通词语。3.5 网络图可视化networkx 是 Python 中常用的复杂网络分析库可以构建节点和边并调用 matplotlib 绘制图像。import networkx as nx G nx.Graph() G.add_edge(陆小凤, 花满楼, weight50) G.add_edge(陆小凤, 西门吹雪, weight30) pos nx.spring_layout(G, seed42) nx.draw(G, pos, with_labelsTrue)边上的weight可以映射成边的粗细权重越大表示共现次数越多人物关系越紧密。4. 完整实战案例下面进入完整实现。为了让代码可以直接复制运行所有脚本都按“函数”拆分每个函数负责一个独立任务。4.1 创建项目目录先创建项目目录和基础文件mkdir -p lu_xiao_feng_analysis/{data/raw,output} cd lu_xiao_feng_analysis然后创建data/characters.txt文件内容如下可以按需扩展陆小凤 100 nr 花满楼 100 nr 西门吹雪 100 nr 叶孤城 100 nr 司空摘星 100 nr 沙曼 100 nr 上官飞燕 100 nr 孙秀青 100 nr 老实和尚 100 nr 朱停 100 nr4.2 编写爬虫脚本 spider.pyspider.py负责抓取小说正文。这里以通用的“章节列表页 正文页”结构为例实际使用时需要根据目标网站调整选择器。import time import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def get_html(url): 获取网页 HTML并做基本编码处理 resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding resp.raise_for_status() return resp.text def parse_chapter_list(list_url): 从目录页解析章节标题和链接。 注意不同网站结构不同需要根据实际页面调整选择器。 html get_html(list_url) soup BeautifulSoup(html, lxml) chapters [] for a in soup.select(div.listmain a): title a.get_text().strip() href a.get(href) if title and href: chapters.append({title: title, url: href}) return chapters def parse_chapter_content(chapter_url): 解析单个章节页返回标题和正文文本 html get_html(chapter_url) soup BeautifulSoup(html, lxml) title soup.find(h1).get_text().strip() content_div soup.find(div, idcontent) if content_div is None: return title, content content_div.get_text() # 常见清洗 content content.replace(笔趣阁, ) content content.replace(。。, 。) content content.replace( , ) return title, content def save_all_chapters(list_url, output_path): 抓取所有章节并保存到单个文本文件 chapters parse_chapter_list(list_url) with open(output_path, w, encodingutf-8) as f: for idx, chapter in enumerate(chapters, 1): title, content parse_chapter_content(chapter[url]) if not content: print(f[跳过] {title} 内容为空) continue f.write(f{title}\n) f.write(content \n\n) print(f[{idx}/{len(chapters)}] 已保存: {title}) time.sleep(1) # 控制请求频率 if __name__ __main__: # 请替换为你实际访问的目录页 URL list_url https://example.com/lu_xiao_feng/ save_all_chapters(list_url, data/raw/lu_xiao_feng.txt)这段代码有以下几个关键点resp.apparent_encoding会根据页面内容自动推测编码比手动指定更省心。time.sleep(1)控制请求速度避免对目标网站造成压力。正文清洗部分目前只做了最简单的处理后续还可以继续优化。4.3 编写文本清洗函数爬下来的原始文本可能包含大量空白字符、特殊符号和重复内容需要先清洗。import re def clean_text(text): 清洗小说文本统一标点去除多余空白 # 替换全角空格和常见空白 text text.replace(\u3000, ) text text.replace(\xa0, ) # 多个换行合并为 2 个 text re.sub(r\n{3,}, \n\n, text) # 去掉特殊符号 text re.sub(r[【】\[\]], , text) return text.strip()这里主要处理三类问题全角空格小说网站经常用\u3000做段落缩进。连续换行批量抓取时可能出现多个空行。特殊符号部分网站会在正文中插入广告或乱码符号。4.4 章节切分函数为了后续统计“每章人物出场次数”需要把全文按章节切分。切分的依据是章节标题常见格式是“第一章 xxx”或“第1章 xxx”。import re CHAPTER_PATTERN re.compile(r^第[0-9一二三四五六七八九十百千0-9]章\s*.*$) def split_chapters(text): 按章节标题把全文切分成列表每个元素是一章 lines text.split(\n) chapters [] current_title None current_content [] for line in lines: if CHAPTER_PATTERN.match(line.strip()): if current_title is not None: chapters.append({title: current_title, content: \n.join(current_content)}) current_title line.strip() current_content [] else: current_content.append(line) if current_title is not None: chapters.append({title: current_title, content: \n.join(current_content)}) return chapters如果《陆小凤传奇》的章节标题格式不是“第 x 章”比如是“楔子”“第一章 陆小凤”这种需要调整CHAPTER_PATTERN正则表达式。正则表达式里包含了中英文数字和“章”字基本能覆盖常见格式。4.5 编写分析脚本 analysis.pyanalysis.py是核心分析脚本完成分词、统计、共现关系构建和可视化。import csv import re import jieba from collections import Counter # 加载自定义词典 jieba.load_userdict(data/characters.txt) def load_names(path): 从词典文件加载人名集合 names set() with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line: names.add(line.split()[0]) return names def tokenize(text): 调用 jieba 分词过滤长度小于 2 的词 words jieba.lcut(text) return [w.strip() for w in words if len(w.strip()) 2] def count_characters(chapters, names): 统计每个人物在全文中的出现次数 counter Counter() for chapter in chapters: content chapter[content] words tokenize(content) for word in words: if word in names: counter[word] 1 return counter def build_co_occurrence(chapters, names, window_size50): 构建人物共现关系返回边列表 relations Counter() for chapter in chapters: words tokenize(chapter[content]) for i in range(len(words)): if words[i] not in names: continue for j in range(i 1, min(i window_size, len(words))): if words[j] not in names: continue pair tuple(sorted([words[i], words[j]])) relations[pair] 1 return relations def save_relations(relations, output_path, min_count5): 保存共现关系到 CSV同时过滤低频边 with open(output_path, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([source, target, weight]) for (source, target), weight in relations.items(): if weight min_count: writer.writerow([source, target, weight]) def main(): # 读取原始文本 with open(data/raw/lu_xiao_feng.txt, r, encodingutf-8) as f: raw_text f.read() # 清洗和切分 raw_text clean_text(raw_text) chapters split_chapters(raw_text) print(f共切分到 {len(chapters)} 章) # 加载人名 names load_names(data/characters.txt) print(f加载 {len(names)} 个人名) # 人物出现次数统计 character_count count_characters(chapters, names) top character_count.most_common(20) print(\n出场次数 Top 20) for name, count in top: print(f{name}: {count}) # 保存统计结果 with open(output/character_count.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([name, count]) for name, count in character_count.most_common(): writer.writerow([name, count]) # 共现关系 relations build_co_occurrence(chapters, names) save_relations(relations, output/relation.csv, min_count5) print(f\n共生成了 {len(relations)} 对共现关系已过滤低频边) if __name__ __main__: main()这段脚本把分析链路串起来了执行顺序是清洗 → 切分 → 加载人名 → 统计 → 构建共现 → 保存 CSV。4.6 绘制人物关系网络图为了让关系数据更直观再写一个可视化脚本draw_graph.py。import csv import matplotlib.pyplot as plt import networkx as nx from collections import defaultdict plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False def load_relations(path, min_weight10): 从 CSV 加载关系数据 edges [] with open(path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: w int(row[weight]) if w min_weight: edges.append((row[source], row[target], w)) return edges def draw_graph(path, output_pathoutput/relation_graph.png): edges load_relations(path, min_weight10) G nx.Graph() for source, target, weight in edges: G.add_edge(source, target, weightweight) # 节点大小按连接度 degree dict(G.degree()) node_size [v * 200 for v in degree.values()] # 边粗细按权重 edge_width [G[u][v][weight] / 10 for u, v in G.edges()] pos nx.spring_layout(G, k0.6, seed42) plt.figure(figsize(16, 12)) nx.draw_networkx_edges(G, pos, widthedge_width, alpha0.5, edge_colorgray) nx.draw_networkx_nodes(G, pos, node_sizenode_size, node_color#4C72B0, alpha0.8) nx.draw_networkx_labels(G, pos, font_size10, font_familySimHei) plt.title(《陆小凤传奇》人物关系网络图, fontsize16) plt.axis(off) plt.tight_layout() plt.savefig(output_path, dpi300) plt.show() if __name__ __main__: draw_graph(output/relation.csv)这里的min_weight10是过滤阈值低于 10 次共现的关系不画出来避免图像过于杂乱。如果人物关系太少可以把阈值调低如果边太多密密麻麻就调高阈值。4.7 运行与验证按顺序执行两个脚本python spider.py python analysis.py python draw_graph.py预期会看到类似下面的输出共切分到 56 章 加载 15 个人名 出场次数 Top 20 陆小凤: 856 花满楼: 412 西门吹雪: 298 ... 共生成了 1200 对共现关系已过滤低频边同时output目录下会生成character_count.csv人名与出现次数。relation.csv人物共现关系表。relation_graph.png关系网络图。4.8 结果说明从人物出场次数可以看出谁是全书的核心角色这通常是第一直觉的验证。从共现关系可以看出人物之间的亲疏程度如果“陆小凤”和“花满楼”的共现次数远高于其他配对说明这两个角色在剧情中有大量交集。这只是一个基础分析如果希望进一步细化可以继续做按章节统计人物出现次数观察主角在剧情不同阶段的戏份变化。统计人物首次出现的章节推断角色登场顺序。用社区发现算法如 louvain自动划分人物派系。5. 常见问题与排查思路在运行上面的代码时可能会遇到一些问题这里整理了一份排查清单。问题现象常见原因解决思路爬取正文为空HTML 页面结构变化选择器失效打开目标网页检查正文标签调整select或find参数请求返回 403目标网站有反爬策略补全User-Agent或添加Cookie适当增加请求间隔中文乱码页面编码不是 UTF-8使用resp.apparent_encoding或手动设置resp.encoding gbkjieba 把人名切碎词典中缺少该人物在characters.txt中添加该名字并重新加载共现关系边太多窗口过大或阈值过低缩小窗口大小提高min_count过滤阈值matplotlib 图片中文显示为方块系统中文字体缺失修改plt.rcParams[font.sans-serif]为系统已安装的中文字体章节切分失败标题格式不符合正则先用命令行查看章节标题格式再调整CHAPTER_PATTERN5.1 爬取内容为空这是爬虫开发中最常见的问题。网站改版、正文使用了动态加载、正文内容被反爬混淆都可能导致find(div, idcontent)返回空值。解决步骤浏览器打开目标页面按 F12 打开开发者工具。找到正文对应的 HTML 标签。对比代码中的选择器和实际标签。修正后重新运行。5.2 jieba 分词不准确jieba 是基于统计词频的分词工具对古龙小说里的大量专有名词支持有限。比如“司空摘星”可能被切成“司空/摘星”“叶孤城”可能被切成“叶/孤城”。解决方法是维护一份项目专属的人名词典并在每次运行前加载jieba.load_userdict(data/characters.txt)词典文件更新后需要重新运行脚本不需要重装 jieba。5.3 共现关系噪声大如果使用“整章共现”统计那么同章节出现的任何两个人物都会产生关联哪怕他们没有任何直接互动。这会带来大量噪声。解决方案有两种缩小窗口从整章改为段落级别或者设定滑动窗口为 30~50 个词。提高阈值只保留权重较高的边比如min_count从 5 提高到 20。6. 最佳实践与工程建议6.1 爬虫的合法合规这一点需要特别强调。爬虫只是技术手段使用时必须注意只抓取公开信息不抓取需要登录才能访问的付费内容。遵守目标网站的robots.txt协议。控制请求频率建议至少间隔 1 秒。抓取内容仅用于个人学习研究不用于商业用途。如果目标网站明确禁止爬虫应当停止抓取改用其他合法数据来源。6.2 数据与代码分离建议将原始文本、清洗后文本、词典、分析结果分别存放在不同目录。这样即使后续修改分析逻辑也不需要重新爬取数据。data/raw/ # 原始网页抓取结果 data/processed/ # 清洗后的文本 data/characters.txt # 人名词典 output/ # 统计结果和可视化图片6.3 词典持续维护小说文本分析的效果很大程度取决于词典质量。运行完一次统计后可以检查 Top 100 高频词中是否还有被切碎的人名如果发现“花满/楼”这种结果就把完整人名加入词典。也可以从统计结果中导出高频词汇再去人工筛选。6.4 性能优化点如果分析全书几百万字的文本build_co_occurrence的双层循环可能比较慢。优化方向先统计出现人名在句子中的位置只对含有人名的词做配对。使用多进程处理不同章节。如果数据量极大可以改用 NumPy 矩阵运算或者使用数据库存储中间结果。6.5 日志和异常处理正式项目中不要直接把print当作日志。可以引入logging模块记录爬取进度和异常信息import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logging.info(开始抓取章节%s, title) logging.error(抓取失败%s, url, exc_infoTrue)6.6 关于数据版权《陆小凤传奇》属于古龙作品著作权保护期内的作品不能随意传播和商用。如果只想做技术练习建议使用已进入公共领域的古籍或者使用开源中文语料库。本文代码演示的是分析方法不鼓励对受版权保护的作品进行大规模抓取和传播。7. 总结与学习路线这篇文章从需求出发完整实现了一个“武侠小说文本分析”项目使用requests和BeautifulSoup完成网页抓取与正文提取。使用jieba结合自定义词典完成中文分词和人名识别。使用滑动窗口统计人物共现关系输出 CSV。使用networkx和matplotlib绘制人物关系网络图。这套方法并不局限于《陆小凤传奇》。你可以把characters.txt换成《楚留香传奇》的人物表也可以把爬虫目标换成其他公开语料核心分析逻辑基本不需要改动。下一步可以尝试的方向用 Scrapy 重构爬虫支持增量抓取和断点续爬。将人物关系数据导入 Neo4j 图数据库实现关系查询。结合情感分析判断人物之间的互动是正向还是负向。使用社区发现算法自动划分人物派系。把同一作者的几部小说合并分析观察人物 IP 的跨作品联动。如果跑通了完整流程你会发现文本分析项目最花时间的往往不是写代码而是清洗数据和维护词典。只要把这两个环节做好后面的统计与可视化都是水到渠成的事。
返回列表