Python文本分析实战:从《哈利波特》到TextRank摘要与人物网络

发布时间:2026/10/24 2:38:43
Python文本分析实战:从《哈利波特》到TextRank摘要与人物网络 做文本分析这些年我经常被问到一个略显偷懒的问题七本《哈利波特》、几百万字如果只给你六句话你会怎么概括更让人好奇的是这套讲魔法学校的小说为什么能跨语言、跨年龄、跨代际地火这么多年。网上的书评大多停在感性层面说童年滤镜世界观宏大但很少有人把它拆成可量化、可复现的证据。这篇文章不打算写读后感而是换个角度用一套 Python 文本分析流水线把整部作品切成章节、统计词频、抽取摘要、构建人物共现网络、画出情感曲线最后用数据去支撑那六句话也去解释为什么好看。全文包含完整的可复制代码、目录结构和排错清单适合有一定 Python 基础、想入门文本分析或自然语言处理实战的读者也适合只想拿一套现成脚本来分析自己手里文本的同学。1. 背景与核心概念1.1 从六句话读完到一次真实的文本挖掘任务用六句话读完一本书听起来像标题党但在技术上它对应的是一个非常经典的任务——自动文本摘要Text Summarization。摘要任务的目标是把一篇长文档压缩成一段短文本同时尽量保留原文的核心信息。工业界用得最多的地方是新闻聚合、会议纪要、法律文书检索和客服工单归类而总结一本小说只是它最讨喜的一个应用场景。真正动手之后你会发现难点不在调用某个库而在前面那几步文本怎么清洗、章节怎么切分、中文怎么分词、停用词怎么处理、句子相似度怎么定义。这些环节任何一个处理不当最后抽出来的六句话就会变成六句废话比如连续抽到三段环境描写。所以本文的重点不是给你一个summarize(text)的黑盒函数而是把每一步拆开讲清楚让你知道参数为什么这么设、结果不好时该改哪里。顺带说一句用代码分析小说还有一个隐性收益你会被迫用证据说话。你觉得某个人物重要是因为你记得他而共现网络告诉你他重要是因为他和主角在同一段文字里出现了几百次。这两种重要经常不一致而这种不一致本身往往就是解读作品的入口。1.2 抽取式摘要与生成式摘要先分清再动手文本摘要大致分两派。**抽取式摘要Extractive**是从原文中挑出若干句子原样拼成摘要**生成式摘要Abstractive**是理解原文后用自己的话重写类似人类写读后感。前者实现简单、结果不会失真、可解释性强缺点是句子之间可能不连贯后者读起来自然但需要序列到序列模型训练成本高还容易编造原文没有的内容。本文采用的是抽取式路线具体算法是TextRank。它 borrowed 自 Google 的 PageRank 思想把每个句子看成一个节点句子之间的相似度看成边然后迭代计算每个句子的重要性得分最后取分数最高的若干句。这个算法不需要训练、不需要标注数据几十行代码就能实现非常适合作为文本分析的第一课。需要提醒的是抽取式摘要出来的是候选六句读起来一定是生硬的。真实的做法是让算法给出候选让人来润色。第 7 节给出的那六句话就是在算法输出的候选句子和关键词排行基础上重新组织的。这也是我在实际项目里一贯的建议——摘要系统先做辅助人别急着做替代人。1.3 文本分析能回答为什么风靡全球吗严格来说不能。一部作品流行是出版、影视、翻译、营销、时代情绪共同作用的结果文本本身只是其中一个变量。但文本分析能回答一个更聚焦的问题这部作品在文本层面具备哪些容易被大量读者接受的特征。比如高频词里朋友学校家害怕选择这类词占比如何说明它的情感锚点在哪人物共现网络是三主角紧耦合 大量配角环绕还是单主角辐射前者更容易让读者找到代入对象情感曲线是不是呈现每章小起伏、每卷大低谷再回升的节奏这种节奏对连载阅读极其友好关键词是否集中在少数几个意象上意象越集中读者越容易记住和转述。这些都是可以用代码量化、可以用图表呈现的。本文的分析结论只基于文本统计不涉及任何超出语料的推断。这一点在写作和技术报告里都很重要数据能支撑什么就只说什么。1.4 本文的技术路线与产出物整条流水线可以概括成五步用 ASCII 图表示大概是这样原始 txt 语料 │ ├─[1] 清洗 按章切分 → chapters: List[str] │ ├─[2] 分词 停用词过滤 → 词频 / TF-IDF 关键词 │ ├─[3] 分句 相似度矩阵 → TextRank → 候选六句 │ ├─[4] 段落窗口共现 → networkx 人物关系图 → 中心度排名 │ └─[5] 词典情感打分 → 逐章情感曲线 ↓ output/*.csv *.png产出物包括keywords.csv关键词及权重、summary.txt候选摘要句、network.png人物关系图、centrality.csv人物中心度排名、sentiment.png情感曲线。这五个文件足以支撑一篇有数据支撑的分析文章也足以作为一个文本分析项目的模板反复复用。2. 环境准备与版本说明2.1 运行环境与依赖清单本文示例使用 Python 编写建议 Python 3.9 及以上版本如果你的环境是 3.8只要 numpy 能正常安装代码同样可以跑通。操作系统不限Windows、macOS、Linux 均可代码里涉及的路径操作全部使用pathlib不存在 Windows 反斜杠转义问题。第三方库一共四个作用分别是jieba负责中文分词与关键词提取numpy负责相似度矩阵和向量运算networkx负责图结构与中心度计算matplotlib负责出图。这里不锁定具体版本号因为这几个库的公开 API 在近几个大版本里都保持稳定锁死版本反而容易在不同环境下装不上。安装时以当前稳定版为准如果遇到接口变动按官方文档调整即可。# 文件路径hp_analysis/requirements.txt jieba numpy networkx matplotlib如果你的语料是英文原版可以把jieba换成nltk或spacy做分词和句子切分后面的 TextRank、共现网络、情感打分逻辑完全不用改因为这三部分只依赖词列表和句子列表这两个抽象结构。这也是我建议的写法把文本处理层和算法层解耦换语言只需要改一个文件。2.2 安装命令与依赖验证安装命令很直接如果下载速度慢可以临时指定镜像源# 文件路径命令行执行 python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install -r requirements.txt # 如需指定镜像源 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后建议做一次最小验证确认三个关键库都能正常导入并且 jieba 能切出中文词# 文件路径hp_analysis/check_env.py import jieba import numpy as np import networkx as nx import matplotlib print(numpy:, np.__version__) print(networkx:, nx.__version__) print(matplotlib:, matplotlib.__version__) print(jieba 分词测试:, /.join(jieba.lcut(霍格沃茨的城堡在夜里亮起了灯)))预期输出会打印出三个库的版本号最后一行是类似霍格沃茨/的/城堡/在/夜里/亮起/了/灯的分词结果。如果这一步就报错说明环境没装好先解决环境问题再往下走不要带着环境问题去调试算法那样排查成本会翻倍。2.3 项目目录结构目录按配置 / 数据 / 模块 / 输出四层划分方便后面把脚本改成流水线hp_analysis/ ├── requirements.txt ├── config.py # 路径、人物名单、停用词文件位置 ├── stopwords.txt # 中文停用词表一行一个 ├── corpus/ # 放你的 txt 语料一个文件一部 │ └── book01.txt ├── load_text.py # 读取语料 按章切分 ├── keywords.py # 词频与 TF-IDF 关键词 ├── summarizer.py # 分句 TextRank 摘要 ├── network.py # 人物共现网络 ├── sentiment.py # 词典情感打分 ├── main.py # 串起整条流水线 └── output/ # 自动生成的结果目录这个结构看起来比一个 2000 行的脚本麻烦但好处非常明显当你想换一种摘要算法时只需要替换summarizer.py当你想加一个新的分析维度时只需要新增一个模块并在main.py里挂一行。文本分析项目最大的坑从来不是算法难而是三个月后回来看不懂自己写的脚本。2.4 语料准备与合规边界这一节必须单独说。本文的全部示例都以你合法持有的电子文本为前提请使用自己购买或已获授权的电子书、公版文本或者自己手打的片段做测试。不要把未授权的全文打包上传仓库、不要在大段输出中还原原文、不要把分析结果公开成替代原著的阅读材料。技术分析和内容分发是两件事边界要划清楚。在具体操作上建议只保留用于统计的中间结果词频、句子索引、分数原文只在本地读取一次。如果你的文章需要展示语料片段控制在一两句、且用于说明算法效果即可。这样做既合规也更专业。另外提醒一点语料编码统一存成 UTF-8避免读取时出现乱码。如果拿到的是 GBK 编码的文件可以用编辑器转码或者在读取时显式指定encodinggbk。乱码不会让程序崩但会让分词结果全部变成单字最终关键词表里全是的了非常隐蔽。3. 核心原理与关键 API 拆解3.1 分句摘要任务的第一步摘要的基本单位是句子所以第一件事是把长文本切成句子。中文分句看起来简单实际上坑不少省略号后面算不算句子引号里的对话怎么处理英文的Mr.、Dr.会不会被误判成句号最稳妥的工程做法是用正则做保守切分只在明确的句末标点。!?之后切并且过滤掉过短的碎片。下面这段代码是本文的split_sentences# 文件路径hp_analysis/summarizer.py片段 import re SENT_END re.compile(r(?[。!?])) def split_sentences(text, min_len10, max_len200): 把一章文本切成句子列表。 min_len: 过短的碎片直接丢弃避免他说。这种无效句进入摘要 max_len: 过长的句子截断防止单句占用过多相似度权重 text text.replace(\n, ).replace(\r, ) raw SENT_END.split(text) sentences [] for s in raw: s s.strip() if len(s) min_len: continue if len(s) max_len: s s[:max_len] sentences.append(s) return sentences这里有两个参数值得解释。min_len设为 10 是为了过滤对话碎片比如他说。只有 4 个字如果进入摘要会占掉一个宝贵的名额。max_len设为 200 是为了控制句子长度差异带来的偏差——在 TextRank 里长句子天然包含更多词和别的句子更容易相似如果不截断最后六句很可能全是超长描写段。常见误区是直接对整本书的文本做split(。)。这样切出来的句子会丢失标点而且在有引号嵌套的对话场景下会切出大量半截句。建议至少保留句末标点后面展示摘要时读起来更自然。3.2 分词与停用词中文文本分析的地基中文没有空格分隔所以分词是绕不开的一步。jieba提供三种模式精确模式、全模式和搜索引擎模式。做词频统计和摘要用默认的精确模式jieba.lcut就够了搜索引擎模式会把长词再切出短词适合做召回不适合做统计。停用词过滤同样关键。的、了、是、在、我、他这些词在任何中文文本里都排前列如果不过滤你的关键词表会前二十名全是虚词完全看不出内容。本文采用外部停用词表 代码内最小兜底的组合# 文件路径hp_analysis/config.py from pathlib import Path BASE_DIR Path(__file__).resolve().parent CORPUS_DIR BASE_DIR / corpus OUTPUT_DIR BASE_DIR / output STOPWORD_FILE BASE_DIR / stopwords.txt OUTPUT_DIR.mkdir(exist_okTrue) # 兜底停用词即使 stopwords.txt 缺失也能跑 FALLBACK_STOPWORDS { 的, 了, 是, 在, 和, 也, 就, 都, 而, 及, 与, 着, 我, 你, 他, 她, 它, 我们, 你们, 他们, 这, 那, 一个, 没有, 什么, 自己, 但是, 因为, 所以, 如果, 还是, 已经 } def load_stopwords(pathSTOPWORD_FILE): words set(FALLBACK_STOPWORDS) if path.exists(): with open(path, encodingutf-8) as f: for line in f: w line.strip() if w: words.add(w) return words停用词表可以从公开的中文停用词清单里取一份再按你的语料补充。比如分析小说时说道看着似乎这类叙事高频词对内容判断没有帮助建议手动加进去。这一步看起来是脏活但它对结果的影响远大于换算法。3.3 TF-IDF 与 TextRank 关键词两种不同的重要提取关键词有两套常用思路很多教程混着讲其实差别很大。TF-IDF的核心是这个词在当前文档里出现得多但在其他文档里出现得少就重要。它需要多篇文档构成语料库。jieba.analyse.extract_tags内置了一份通用语料算好的 IDF 表所以即使你只传一篇文本也能出结果但那份 IDF 是新闻语料训练的用在小说上会有偏差。TextRank 关键词不看文档集合而是在词与词之间建图两个词在同一个滑动窗口里共现就连一条边然后迭代算重要性。它更擅长找出在本文内部抱团出现的词对单个文档更友好。本文的做法是两种都跑一遍取交集看稳定结果取差集看偏差词# 文件路径hp_analysis/keywords.py from collections import Counter import jieba import jieba.analyse def top_words(chapters, stopwords, topn50): 基于词频的朴素统计作为基准线 counter Counter() for ch in chapters: for w in jieba.lcut(ch): w w.strip() if len(w) 2 or w in stopwords or w.isdigit(): continue counter[w] 1 return counter.most_common(topn) def tfidf_keywords(text, topn30): return jieba.analyse.extract_tags(text, topKtopn, withWeightTrue) def textrank_keywords(text, topn30): return jieba.analyse.textrank(text, topKtopn, withWeightTrue)需要注意的是jieba.analyse.textrank抽的是关键词不是句子。这个词很容易让人误会我第一次用时也以为它能直接做句子摘要结果返回一堆词。句子级摘要需要我们自己实现见下一节。3.4 句子相似度与 TextRank 排序六句话怎么选出来TextRank 的完整流程是句子集合 → 相似度矩阵 → 按行归一化成转移概率 → 幂迭代求稳态分布 → 按分数排序取 TopK。相似度有多种定义方式最常用的是基于词重叠的版本公式大致是相似度 两句共有词数 / (log(句1词数1) log(句2词数1))分母取对数是为了惩罚长句子的天然优势。下面给出完整实现# 文件路径hp_analysis/summarizer.py import math from itertools import combinations import jieba import numpy as np def sentence_similarity(s1, s2, stopwords): w1 {w for w in jieba.lcut(s1) if len(w) 2 and w not in stopwords} w2 {w for w in jieba.lcut(s2) if len(w) 2 and w not in stopwords} if not w1 or not w2: return 0.0 common len(w1 w2) if common 0: return 0.0 denom math.log(len(w1) 1) math.log(len(w2) 1) return common / denom if denom 0 else 0.0 def build_similarity_matrix(sentences, stopwords): n len(sentences) sim np.zeros((n, n), dtypenp.float64) for i, j in combinations(range(n), 2): s sentence_similarity(sentences[i], sentences[j], stopwords) sim[i][j] s sim[j][i] s # 按行归一化每行和为 1代表从该句跳转到其他句的概率 for i in range(n): row_sum sim[i].sum() if row_sum 0: sim[i] sim[i] / row_sum return sim def textrank_scores(sentences, stopwords, d0.85, max_iter100, tol1e-4): n len(sentences) if n 0: return np.array([]) sim build_similarity_matrix(sentences, stopwords) score np.ones(n) / n for _ in range(max_iter): new_score (1 - d) / n d * sim.T.dot(score) if np.abs(new_score - score).sum() tol: score new_score break score new_score return score def summarize(chapters, stopwords, topk6, max_sent_per_chapter300): 跨章抽取先按章分句再统一打分最后按原文顺序输出 all_sents [] for ch in chapters: sents split_sentences(ch)[:max_sent_per_chapter] all_sents.extend(sents) # 语料过大时先按章抽样避免 O(n^2) 矩阵撑爆内存 if len(all_sents) 4000: step len(all_sents) // 4000 1 all_sents all_sents[::step] scores textrank_scores(all_sents, stopwords) if len(scores) 0: return [] top_idx np.argsort(scores)[::-1][:topk] top_idx sorted(top_idx.tolist()) # 还原原文顺序读起来更连贯 return [(i, all_sents[i], float(scores[i])) for i in top_idx]三个参数要重点理解。d0.85是阻尼系数含义是有 85% 的概率沿着边跳转15% 的概率随机跳这个默认值从 PageRank 沿用至今一般不用改。max_iter和tol控制迭代终止文本量小时通常十几次就收敛。max_sent_per_chapter是工程上的保护避免某一章特别长导致整本书的句子分布失衡。还有一个容易被忽略的点TopK 取出来之后要按原位置重排。如果不重排输出的六句话是乱序的读起来完全不知所云。这个细节在多数教程里都没提但它是结果能不能用的分水岭。3.5 共现网络把人物关系变成一张图人物关系分析的思路很朴素如果两个人物在同一个文本窗口里同时出现就认为他们有关系共现次数越多关系越强。用networkx建无向图边权就是共现次数然后算度中心度degree centrality和加权度就能排出谁最重要。# 文件路径hp_analysis/network.py from itertools import combinations import networkx as nx def build_cooccurrence(chapters, characters, window300): 按固定长度滑动窗口统计人物共现。 window 越小越强调同场景越大越强调同章节。 G nx.Graph() for c in characters: G.add_node(c) for ch in chapters: for start in range(0, len(ch), window // 2): # 50% 重叠减少边界截断 seg ch[start:start window] present [c for c in characters if c in seg] for a, b in combinations(present, 2): if G.has_edge(a, b): G[a][b][weight] 1 else: G.add_edge(a, b, weight1) return G def centrality_table(G, topn15): degree nx.degree_centrality(G) weighted {n: sum(d[weight] for _, _, d in G.edges(n, dataTrue)) for n in G.nodes} rows [(n, round(degree[n], 4), weighted[n]) for n in G.nodes] rows.sort(keylambda x: x[2], reverseTrue) return rows[:topn]window的取值直接决定图的形态。取 100 到 200 字人物必须真的在同一段对话或场景里出现才算有边网络会比较稀疏反映的是直接互动取 500 字以上一章里出现过的人物全都连起来网络会变成一团看不出结构。我的经验是从 200 开始试画出图来看密度太密就调小太散就调大。另一半是人物名单。这里必须手动维护一个CHARACTERS列表因为通用分词器不认识伏地魔邓布利多这类专有名词很容易切成伏地魔或者邓布利多。解决办法有两种把人物名加进jieba的自定义词典或者干脆不做分词直接用字符串in判断。本文用的是字符串匹配因为人名足够长误匹配概率很低而且不受分词结果影响。3.6 情感曲线把节奏变成一条可看的线情感分析有两种做法基于词典的和基于模型的。基于模型比如各种预训练情感分类器准确率更高但要装大依赖、跑得慢基于词典的版本几十行就能写完可解释性强适合做趋势分析。# 文件路径hp_analysis/sentiment.py import jieba POS_WORDS { 快乐, 高兴, 温暖, 朋友, 家, 希望, 勇敢, 笑声, 拥抱, 胜利, 喜欢, 爱, 安全, 明亮, 礼物, 庆祝, 骄傲, 信任 } NEG_WORDS { 恐惧, 害怕, 黑暗, 死亡, 痛苦, 哭, 鲜血, 愤怒, 逃跑, 受伤, 孤独, 绝望, 冰冷, 尖叫, 危险, 失去, 恨, 谎言 } def chapter_sentiment(chapters): 返回每章的情感倾向值0 表示偏负面1 表示偏正面0.5 为中性 scores [] for ch in chapters: words [w for w in jieba.lcut(ch) if len(w) 2] pos sum(1 for w in words if w in POS_WORDS) neg sum(1 for w in words if w in NEG_WORDS) total pos neg scores.append(0.5 if total 0 else round(pos / total, 4)) return scores这套词典是最小可用版实际使用时建议按你的语料扩充到几百个词。扩充方法很简单先把正负样本各取几段人工标一遍再把漏掉的词补进去。要注意的是词典法对否定句和反讽基本无解他一点也不害怕会被判为负面所以结论只能看趋势不要拿单章数值下判断。这个限制必须在报告里写清楚否则就是过度解读。4. 完整实战案例4.1 步骤一语料清洗与按章切分先写读取和切分模块。中文小说通常用第一章第十二章这类标题分章用正则匹配即可# 文件路径hp_analysis/load_text.py import re from config import CORPUS_DIR CHAPTER_PATTERN re.compile(r第[一二三四五六七八九十百零〇\d]章) def split_chapters(text): matches list(CHAPTER_PATTERN.finditer(text)) if not matches: text text.strip() return [text] if len(text) 200 else [] chapters [] for idx, m in enumerate(matches): start m.start() end matches[idx 1].start() if idx 1 len(matches) else len(text) chunk text[start:end].strip() if len(chunk) 200: chapters.append(chunk) return chapters def load_corpus(): chapters [] for path in sorted(CORPUS_DIR.glob(*.txt)): text path.read_text(encodingutf-8, errorsignore) chapters.extend(split_chapters(text)) print(f[load] {path.name} - 累计 {len(chapters)} 章) return chapters这里用finditer而不是split是为了保留章节标题。标题里往往包含密室火焰杯这类关键词对后续词频统计有正面作用。另外errorsignore是为了兼容少量编码异常字符避免整个流程因为一个乱码字节崩掉。4.2 步骤二词频统计与关键词提取有了章节列表词频和关键词就是几行调用# 文件路径hp_analysis/main.py片段 from config import OUTPUT_DIR, load_stopwords from load_text import load_corpus from keywords import top_words, tfidf_keywords, textrank_keywords chapters load_corpus() stopwords load_stopwords() full_text \n.join(chapters) words top_words(chapters, stopwords, topn50) with open(OUTPUT_DIR / keywords.csv, w, encodingutf-8) as f: f.write(word,freq\n) for w, c in words: f.write(f{w},{c}\n) print(TF-IDF 前 10, tfidf_keywords(full_text, 10)) print(TextRank 前 10, textrank_keywords(full_text, 10))跑完之后打开keywords.csv你会看到一张按词频排序的表。如果前几名出现大量人名说明语料主要是对话驱动如果出现大量场景词城堡、森林、走廊说明描写比重高。这两种结构对应的叙事风格完全不同是可以直接写进分析的观察。4.3 步骤三跑出六句话摘要摘要部分是整个流程里最需要调参的环节。直接调用summarize(chapters, stopwords, topk6)通常第一次结果不会太理想常见问题是六句话全部来自同一本书或者同一类场景。解决方法是在summarize里加一个来源去重参数# 文件路径hp_analysis/main.py续 from summarizer import summarize picked summarize(chapters, stopwords, topk6) with open(OUTPUT_DIR / summary.txt, w, encodingutf-8) as f: for rank, (idx, sent, score) in enumerate(picked, 1): f.write(f[{rank}] score{score:.4f} idx{idx}\n{sent}\n\n) print(f[{rank}] {sent})如果希望摘要覆盖不同阶段可以按语料前、中、后三段分别取 2 句这样得到的六句话天然带有开端—发展—高潮的层次。这个做法在项目里非常实用与其追求一个全局最优分数不如按业务需求约束结果分布。论文摘要和小说摘要的需求就完全不同前者要覆盖方法、数据、结论后者要覆盖起承转合。4.4 步骤四人物共现网络与中心度人物名单需要手动维护这是无法回避的成本。下面是一份够用的初始名单# 文件路径hp_analysis/config.py续 CHARACTERS [ 哈利, 罗恩, 赫敏, 邓布利多, 斯内普, 伏地魔, 海格, 马尔福, 麦格, 卢娜, 纳威, 金妮, 小天狼星, 多比, 韦斯莱, 卢平, 穆迪, 斯普劳特, 弗立维, 特里劳妮 ]注意名单里只放人物像霍格沃茨格兰芬多斯莱特林这类地点和学院名要单独一个列表。如果混在一起最后算出来的中心度第一名可能是霍格沃茨——它确实出现最多但这对人物分析没有意义。建图之后导出中心度表并画图# 文件路径hp_analysis/main.py续 import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt import networkx as nx from config import CHARACTERS from network import build_cooccurrence, centrality_table G build_cooccurrence(chapters, CHARACTERS, window300) rows centrality_table(G, topn20) with open(OUTPUT_DIR / centrality.csv, w, encodingutf-8) as f: f.write(character,degree,weighted_degree\n) for n, d, w in rows: f.write(f{n},{d},{w}\n) plt.figure(figsize(12, 9)) pos nx.spring_layout(G, k0.6, seed42, weightweight) weights [G[u][v][weight] for u, v in G.edges] nx.draw_networkx_nodes(G, pos, node_size800, node_color#9ecae1) nx.draw_networkx_edges(G, pos, alpha0.4, width[min(w / 50, 4) for w in weights]) nx.draw_networkx_labels(G, pos, font_size10, font_familySimHei) plt.axis(off) plt.tight_layout() plt.savefig(OUTPUT_DIR / network.png, dpi200) print(已生成 network.png)spring_layout的seed参数一定要固定否则每次画出来的布局都不一样做前后对比时容易怀疑自己改错了东西。边宽用min(w/50, 4)做了一次压缩是因为共现次数的量级差异很大不压缩的话少数几条边会粗到糊住整张图。4.5 步骤五情感曲线与节奏分析最后一步把每章的情感值画成折线# 文件路径hp_analysis/main.py续 from sentiment import chapter_sentiment scores chapter_sentiment(chapters) plt.figure(figsize(14, 5)) plt.plot(range(1, len(scores) 1), scores, linewidth1.2, color#3182bd) plt.axhline(0.5, colorgray, linestyle--, linewidth0.8) plt.xlabel(chapter index) plt.ylabel(positive ratio) plt.title(Sentiment curve by chapter) plt.tight_layout() plt.savefig(OUTPUT_DIR / sentiment.png, dpi200) print(f共 {len(scores)} 章情感均值 {sum(scores)/len(scores):.4f})看这条曲线的方法是找低谷和回升。沿 x 轴看如果每隔一段距离就出现一个明显的凹陷随后又缓慢回升说明节奏是危机—解决—再危机的循环结构这种结构对读者非常友好每次都能获得阶段性的情绪释放同时留下新的悬念。4.6 运行方式与输出说明把上面的片段汇总到main.py后一条命令跑完# 文件路径命令行执行 cd hp_analysis python main.py预期会在output/目录下得到五个文件。keywords.csv是词频表centrality.csv是人物排名summary.txt是候选摘要句network.png和sentiment.png是两张图。终端会打印每本书的章节数、加载进度和摘要句子方便你实时判断进度。需要说明的是具体数值取决于你使用的语料版本、章节切分方式和停用词表不同版本会导致词频有差异所以我这里不给某词出现 12345 次这类硬数字。你应该以自己跑出来的结果为准做纵向对比时也要保证三次运行用的是同一份语料和同一份停用词表否则结论不成立。5. 常见问题与排查思路文本分析流程的报错往往很隐蔽——程序不崩但结果是垃圾。下面这张表覆盖了我在实际使用中遇到的高频问题问题现象常见原因解决思路关键词前 20 名全是的了是停用词表未加载或路径错误打印len(stopwords)确认加载数量检查stopwords.txt编码中文图表显示成方框matplotlib 默认字体不含中文设置font_familySimHei或指定已安装的中文字体路径摘要六句话全是同一段相似度阈值过低长句互相抱团加大max_len惩罚或按章节分段取 TopKMemoryError或程序卡死句子数过多n×n 矩阵爆炸对句子抽样控制总量在 4000 以内人物网络是一团糊window太大几乎人人相连把 window 从 500 降到 200 甚至 100人物名被切成碎片分词器不认识专有名词用字符串in匹配或jieba.add_word加自定义词典章节数为 0分章正则与实际标题格式不符打印文本前 500 字观察标题实际写法情感曲线几乎是一条直线正负词典太小命中率低扩充词典到几百词或改用模型方法读取文本出现乱码文件编码不是 UTF-8读取时指定encodinggbk或用编辑器转码挑三个最典型的展开说。第一个是结果全是虚词。这个问题的根本原因通常是路径问题STOPWORD_FILE指向的文件不存在代码走了兜底逻辑而兜底的十几个词根本不够。排查方法是打印加载到的停用词数量正常情况下几百到上千都合理如果只有十几个就是表没读到。第二个是内存爆掉。TextRank 的相似度矩阵是 O(n²)一万个句子就是 1 亿个浮点数单精度也要 400MB。解决办法是控制句子总量或者在计算时只看相邻区间内的句子对。后者在工程上叫窗口化相似度牺牲一点全局性换取可行性非常常用。第三个是中文出图乱码。这是 matplotlib 的经典坑跟你的代码逻辑无关。最简单的解法是在画图前统一设置字体# 文件路径hp_analysis/main.py开头 import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] plt.rcParams[axes.unicode_minus] Falsefont.sans-serif给的是一个候选列表matplotlib 会按顺序找第一个存在的字体所以这么写可以同时兼容 Windows 和部分 Linux 环境。如果全都找不到就手动指定字体文件的绝对路径。6. 最佳实践与工程建议6.1 语料与中间产物分层管理做文本分析最容易踩的坑是语料和结果混在一起。建议严格分三层corpus/只放原始文本只读不改output/只放结果文件随时可删可重建中间产物分句结果、相似度矩阵如果要缓存单独放cache/并加入.gitignore。这样做的好处是流程可重放。当你调整了停用词表删掉output/重跑一遍得到的就是一套全新结果不会出现一半是旧结果一半是新结果的混乱。在真实项目里可重放性比算法先进性更重要。6.2 把脚本拆成可替换的流水线main.py里不应该出现算法细节它只负责按顺序调用。每个模块对外暴露一个函数输入输出都是基本数据结构列表、字典这样任何一个模块都可以被单独测试和替换。比如你想把词典情感分析换成模型方案只要新写一个sentiment_model.py保证chapter_sentiment(chapters)这个签名一致main.py一行都不用改。这也是我在团队里推的一条规则模块之间传数据不传对象。传List[str]、Dict[str, float]这种朴素结构比传自定义类要灵活得多尤其在数据处理这种频繁试错的场景。6.3 性能与内存的实际考量整本书级别的文本分析瓶颈几乎永远在相似度计算。几个实用的优化手段先用max_sent_per_chapter限制每章句数避免个别超长章节主导结果对超长语料做等距抽样而不是随机抽样保证前后期内容都有覆盖相似度矩阵用float32而不是float64内存减半精度损失可以忽略如果只看关键词完全不需要建句子矩阵直接跑词频即可速度快一个数量级。另外jieba首次加载词典会有一两秒的延迟这是正常的。如果要批量处理很多文件把jieba初始化放在程序入口不要放进循环里。6.4 结果可解释性与人工校验自动摘要最容易犯的错是看起来很专业其实读不懂。上线前必须做人工校验建议按这个顺序检查摘要句子单独拿出来读是否语法完整六句话连起来读是否构成一个连贯的叙事每句话是否能对应到原文的某个明确位置我们保留了idx就是为了这个。如果六句话读不通不要急着换模型先检查是不是抽取式方法本身的局限。这时候正确的做法是保留算法输出作为候选池人工重写一版。技术分析报告里说清楚哪部分是机器给的、哪部分是人改的比假装全自动要专业得多。6.5 合规与生产环境的边界涉及文本数据时几条底线要守住只处理你有权使用的文本不把原文大段输出到公开渠道不在结果中还原可替代原著的连续内容如果涉及用户数据必须做脱敏并遵守最小权限原则。如果这套流程要放进生产环境比如给内容平台做自动摘要还要额外考虑摘要结果要有人工复核环节不能直接对外发布处理日志里不要记录原文批量任务要在测试环境验证后再上生产并准备好回滚方案。这些看起来和分析算法无关但恰恰是工程落地时最容易出事的地方。7. 数据背后的答案六句话与它为什么好看7.1 六句话版本先给出结论。下面这六句话不是原文摘抄而是在算法给出的候选句、关键词排行和人物中心度基础上重新组织的概括每一句都对应一个可以在数据里找到支撑的结构特征一个住在楼梯下、额头带疤的男孩在十一岁生日那天被告知自己是个巫师。他走进魔法学校交到两个真正意义上的朋友也第一次撞上那个连名字都不愿被提起的人留下的阴影。每一学年都像一次成人礼石头、密室、囚徒、赛场、预言、洞穴、死亡圣器代价一次比一次重。长辈的死亡与一段迟到多年的记忆把爱与选择这条暗线彻底翻上台面。三个少年放弃学业踏上逃亡之路靠彼此的信任和对逝者的记忆对抗恐惧。最后一战里他取胜靠的不是更强的魔法而是不杀、承认软弱、把魔杖放回原处。这六句话覆盖了起点—关系—升级—转折—低谷—结局六个节点正好对应情感曲线的六个关键位置。这就是为什么它读起来像一条完整的线而不是六个孤立的事实。7.2 关键词给出的证据跑完词频后最值得看的不是前几名而是第 10 到 50 名。前几名往往是人名和书名里的意象词信息量低中间这一段才是主题词。在我的多次运行里稳定出现在这个区间的是三类词关系类朋友、家人、哥哥、同学、情绪类害怕、生气、高兴、担心、选择类决定、相信、愿意、拒绝。这三类词恰好对应青少年读者的三个核心关切我在群体里的位置、我的情绪是否被理解、我能不能自己做决定。一部作品如果在这三个维度上都有高频表达它天然就容易和这个年龄段的读者建立连接。这个结论不需要读完整本书也能验证——你只要看关键词表里这三类词是否成规模出现。反过来如果一部作品的关键词集中在器物、地理、术语上那它更可能是设定驱动而非人物驱动读者记住的是世界观而不是人。这两种取向没有高下之分但传播路径完全不同人物驱动的作品更容易产生同人创作和情感讨论设定驱动的作品更容易产生考据和设定集。7.3 人物网络给出的证据人物共现网络的结构比中心度排名更有意思。用window200跑出来的图通常呈现三个特征第一三个主角之间形成高权重的三角形边权远高于其他连接。这意味着他们在文本里高度绑定几乎共享所有重要场景。对读者来说这是代入入口——不管你是谁三人组里总有一个和你相似。第二存在一批高中心度但低权重的配角。他们的度不低和很多人有连接但每条边的权重都不高互动次数少。这类人物在叙事里承担的是信息节点和场景连接器的功能比如老师、店主、记者。他们的存在让世界显得稠密但不抢主线。第三存在少数低中心度但高权重的人物只和主角有强连接。这类角色通常是关键转折的触发者出现次数不多但每次出现都推动剧情。这三个结构加在一起构成了一种非常耐读的关系拓扑主线清晰三角形世界有厚度稠密配角转折有力度强连接的关键角色。这不是靠写作技巧硬凑出来的而是长篇幅连载自然形成的结构优势。7.4 情感曲线给出的证据逐章情感曲线跑出来之后最直观的观察是波动频率。如果曲线是缓慢的长周期波动说明作品节奏偏慢适合沉浸式阅读如果是高频小幅波动叠加少数深谷说明它采用的是章节级悬念 卷级危机的双层结构。后者对连载阅读极其友好每一章结束时都有一个小钩子让你愿意读下一章每一卷结束时有一个大释放让你觉得这卷值了。等连载变成整本出版、再变成电影这种节奏依然成立因为它的颗粒度足够细。还有一点值得注意曲线的最低点通常不在最后一战而在重要角色死亡那几章。这说明作品的情绪重心是失去而不是对抗。对抗带来紧张失去带来共鸣后者的记忆留存时间要长得多。这也解释了为什么很多读者记不清战斗细节却记得某些角色离开的那一页。8. 总结与继续学习的方向这套流程跑下来你实际掌握的是四件事怎么把杂乱的长文本变成结构化的章节列表怎么用词频和 TF-IDF 快速摸清一份文本的主题分布怎么用 TextRank 做可解释的抽取式摘要怎么用图和曲线把关系和节奏变成可视化证据。这四项能力不只适用于小说换成新闻、财报、客服记录、政策文件流程完全一样只需要调整停用词和关键词表。如果继续往下走建议按这个顺序深入先把jieba的自定义词典用起来解决专有名词识别问题再学scikit-learn的TfidfVectorizer用真正的语料库训练自己的 IDF然后尝试句向量如sentence-transformers替代词重叠相似度摘要质量会有明显提升最后再考虑生成式摘要模型那时候你已经有了完整的评测基线能判断模型是不是真的比基线好。实际项目里最需要优先关注的三个风险我这里再强调一遍一是语料合规别把不该分发的文本放进仓库二是结果可解释任何自动摘要都要保留溯源信息我们保留的idx就是这个作用三是参数留痕停用词表、window 大小、TopK 数量都要写进配置文件不要散落在代码里否则三个月后你无法复现自己的结论。最后给一个可执行的小建议先不要急着分析整部七本拿一章、一节、甚至一段对话先跑通全流程看到output/目录里真的出现了图和表再去扩语料。文本分析这件事跑通一次比读十篇教程管用得多。代码在你的机器上跑起来、结果和预期有偏差、你去调参数把它调对——这个过程本身就是最有价值的部分。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询