ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从文本清洗到情感曲线:NLP文本挖掘项目harrypotter15-1解析

从文本清洗到情感曲线:NLP文本挖掘项目harrypotter15-1解析

“harrypotter15-1”看起来像某个内部实验目录,或者某个数据集的版本号。说实话我第一次拿到这个代号的时候也愣了一下,下意识以为是谁在整理同人素材时把所有文档编号成了“哈利波特第15卷第1部分”。但拆开看就清楚多了:harrypotter 是主题域,15 是实验序号,1 是迭代版本。这是一个围绕哈利波特系列作品做的文本挖掘项目,核心是用数据的方式重新读一遍七本小说,把故事情节、人物关系和情感变化量化成图表和数字。

这个项目能回答的问题很具体:斯内普这个角色到底在哪一章情绪最低落;韦斯莱双胞胎的台词在全书中的分布是均匀还是集中在某几部;哪些角色虽然出场不多,却在事件串联中起了关键作用。项目涉及的自然语言处理、文本清洗、共现网络分析这些技术,在真实业务中也很常用。如果你正在学数据分析或者内容运营,想找一个有趣又不至于太复杂的练手文本,这套思路非常适合直接复用。

1. 项目代号里的信息量:为什么叫 harrypotter15-1

1.1 代号命名逻辑与项目定位

先解释一下“15-1”这个编号的来由。我习惯把所有实验型项目按“主题领域-实验序号-迭代版本”来命名。“15”意味着这是我在文本分析方向做的第15个实验,“1”代表第一版。这个编号看起来很技术化,但它有一个很实际的好处:复盘的时候可以直接锁定当时用的数据版本和处理流程,不用在文件夹里翻半天。

这个项目本质上是一次关于“叙事结构与情感体验”的数据化尝试。我们以七本哈利波特小说为素材,目标是完成三件事:第一,建立完整的章节级语料库;第二,计算每章的词频、情感倾向、角色出现密度;第三,用共现分析构建人物关系网络。最终输出物是一份交互式图表集和一份“全书关键剧情节点”排行榜。

从这个定位可以看出,项目虽然挂着一个很文化的IP名头,但底层工具链基本上就是通用的自然语言处理流程。这意味着,你在任何长文本分析任务里都能复用同一条路径,把素材换成新闻稿、文学名著、产品评论都一样成立。

1.2 为什么选择哈利波特作为分析对象

选哈利波特不是单纯因为IP热度高,而是因为它作为文本分析素材有四个非常突出的优点。

第一,语料规模合适。七本原著加上中文译本,合计超过百万词。对入门级NLP项目来说,这个量级既足够跑出有统计意义的结论,又不至于大到需要分布式集群。第二,人物关系足够复杂。核心角色、次要角色、只出现过几次的边缘角色加起来超过两百个,恰好适合做网络图中的社区发现实验。第三,情感起伏有明确锚点。书中存在大量读者公认的“高甜时刻”和“高虐章节”,这些可以作为情感分析模型的校验基准。第四,角色发言风格差异明显。同一句话由不同角色说出来,语气完全不同,这给“说话者识别”提供了天然的验证场景。

这四个特点意味着,就算你完全没读过原著,只看数据结果,也能从图表里看出剧情节奏的峰谷变化。比如各本书的高频词能从“魔法石、分院帽”逐步过渡到“魂器、死亡、决斗”,这种词频迁移本身就是一条叙事弧线。

1.3 项目想解决的核心问题与应用场景

深入来看,“harrypotter15-1”要解决的问题并不只是“出几张好看的图”,而是三个更具体的诉求。

第一个诉求是给文本情绪一个参照系。常规阅读时,我们感知“这段很压抑”是主观体验。通过分章节计算情感得分,能定位出全书情感最低谷的十个章节,然后去对比读者社区的讨论热帖,看看主观感受和数据结论是否对齐。

第二个诉求是还原角色之间的隐性连接。有些角色在剧情中从来没有直接对话,但他们总是出现在同一批人的口中,或者总在同一场景周围活动。共现网络能把这些隐性关联挖掘出来,帮创作者找到适合展开二创的“人物组合”。

第三个诉求是做一个可复用的“长文本分析流水线”。从TXT文本到分词结果,再到结构化表格和网络图,中间涉及的所有清洗逻辑、词典配置、参数调整都可以固化下来。下次换一批语料,只需要改改词典就能跑。

所以我一直认为,这个项目的核心不在哈利波特本身,而在于它把“数据分析如何讲一个故事”这件事完整演示了一遍。它很适合三类人:想入门NLP的开发者、做IP向内容策划的运营、以及想给学生找复合型案例的老师。

2. 前期准备与数据获取:不是爬虫,是语料工程

2.1 语料来源与合规注意事项

很多新手一听“七本书的文本数据”就直接想到爬虫。我建议别这么做,而且也没必要。我们当时用的是公开文本库中已有的整理版本,也就是把已经公开的语料文件统一转成UTF-8编码的纯文本。这里要强调一个原则:项目用途是学习研究,不应该涉及商业传播;如果要做公开分享,最好只发布分析结论和统计图表,不传播原始文本文件。

数据源确定之后,还需要做一个“文件清单”来锁定版本。因为不同来源的TXT在章节划分、英文原名处理和排版上都可能有差异。我当时的做法是建了一个data_meta.csv,记录每本书的文件名、章节数量、字符数、清洗时间和备注。这个习惯后来帮了大忙——即使某次分析结果异常,也能快速定位是不是某个文件格式不对。

语料工程的前期处理顺序大致是这样的:

  1. 把所有文件转码为UTF-8,去除BOM头。
  2. 按书籍分成7个独立文本。
  3. 用正则去掉目录、页码、出版社信息等噪声。
  4. 按章节关键词切分,生成“章节ID + 书名 + 正文字符串”的结构化数据。
  5. 针对中文译本,额外标注“翻译用词差异”并建立别名表。

2.2 文本清洗的关键细节

文本清洗是整个项目最耗时、最不讨好的环节。它决定了后续所有统计结果的可靠性。我当时踩过几个比较典型的坑,这里直接列出来。

第一个坑是引号问题。中文译本里很多对话是“某某说:‘……’”嵌套标点,如果只按单引号或双引号简单切割,会把同一段对话拦腰折断。解决办法是先把文本中成对的引号内容先整体提取出来,再处理内层引号,不要试图一次正则到位。

第二个坑是英文版里的人名大小写差异。Harry、harry、HP、Potter在原文中出现的频率很高,如果不做归一化,词频统计会把这些实际上是同一个角色的词拆成多个词。处理方式很简单:对专有名词建一个mapping表,统一替换成标准形式。

第三个坑是章节结尾的多余字符。某些章节末尾会附带“本章完”“Next Chapter”之类的内容,如果不清理,这些词的频率会虚高。我们当时专门做了一次“尾部噪声扫描”,把每个章节的最后50个字抽出来人工检查一遍,问题立刻暴露。

这里给出一段比较常用的清洗代码示例:

import re import pandas as pd def clean_chapter(text): # 去掉常见噪声 text = re.sub(r'第[一二三四五六七八九十百千0-9]+章[^\n]*', '', text) text = re.sub(r'\bPage\s*\d+\b', '', text) text = re.sub(r'[\u3000]', ' ', text) # 统一引号为中文格式,便于后续切分 text = text.replace("“", "「").replace("”", "」") text = text.replace("‘", "「").replace("’", "」") return text.strip()

这段代码不复杂,但它体现了一个原则:清洗优先级是“统一格式 > 去噪声 > 信息压缩”。先把格式标准化,再做删除操作,最后才是维度切分。顺序反过来的话,很容易在去噪声时误删有效正文。

2.3 章节切分与元信息标注

清洗完成之后,需要把每本书切分成章节级块。我当时没有依赖现成的分章库,而是根据每本书的章节标题规律写了一个动态切分函数。切分逻辑是:找到“第X章”或“Chapter X”的位置,作为新章节起点,往前找最近的上一个切分点,两个点之间的内容就是上一章正文。

切分得到的每条记录需要带上元信息,至少包括:book_id、chapter_id、chapter_title、text。这样做有两个好处:第一,可以按章节做所有后续统计;第二,可以在可视化时让横轴同时显示“第几本第几章”,而不是让读者看到一个孤零零的编号。

最后我还额外生成了一列pivot_chapter,把七本书的首尾章节接成一个连续的章节序列。这个序列在绘制全局情感曲线时非常关键,否则折线图会在每本书之间断裂,看不出全书的连续变化趋势。

3. 词频统计与情感曲线:让故事情绪可量化

3.1 停用词过滤与高频主题词提取

语料就绪之后,第一个核心实验是统计每本书的高频词。表面上看就是“数单词”,但实际操作时要小心停用词和专有名词的干扰。中文文本先用jieba分词,然后加载哈利用语自定义词典,避免“霍格沃茨”被切成“霍格”和“沃茨”;英文文本用nltk的word_tokenize,再过滤掉所有英文停用词。

我习惯对每本书提取Top 30的关键词,然后横向对比七本书的关键词变化。这个对比在数据结果里非常直观:前三本中,“魔法、朋友、学校、课程”之类词汇排得很靠前;后几本中,“死亡、魂器、记忆、战争”开始持续上升。这种词频重心迁移,恰好对应了故事从校园冒险转向正邪决战的过程。

如果只看词频还不够,最好再加上“相对词频”指标,也就是“某词在该书中的出现次数 / 全书所有词频总和”。因为七本书篇幅不同,绝对次数没有可比性。这样处理后,才能真正看出“秘密”一词在第七本中的相对密度比第一本高了多少倍。

import jieba from collections import Counter jieba.load_userdict("hp_dict.txt") def tokenize_frequency(text, top_n=30): words = jieba.lcut(text) filtered = [w for w in words if w.strip() and len(w) > 1 and w not in stopwords] return Counter(filtered).most_common(top_n)

3.2 情感分析方案选型:VADER、SnowNLP还是自定义词典

情感分析是整个项目里最容易引起争议的部分,因为不同模型对同一段文字感受不同。我对比了三种方案,最终采用的是“多模型对照 + 人工校验”的混合方式。

第一方案是用英文原文跑VADER。VADER本身为社交媒体文本设计,优点是速度快、对简单情绪句判断较准,缺点是对反讽和魔法背景下的一些特殊词汇处理不够好。第二方案是用中文译本跑SnowNLP,优点是符合大家阅读中文的习惯,缺点是这个模型对长文本的整体倾向经常偏中性,章节级别容易没有区分度。第三方案是自建一个魔法世界情感词典。我们把“摄魂怪、阿瓦达索命、死亡、背叛”标为强负向,把“魁地奇、糖果、圣诞节、朋友”标为正向,然后加权计算每章得分。

实际跑下来的效果是:自定义词典在剧情关键点上的识别能力强于通用模型,但它的问题是覆盖率不足,需要不断补充词汇。所以最终的流程是先用通用模型算出基础得分,再用自定义词典做“情感偏移修正”,两者叠加后输出一个综合分。这样既有通用模型的泛化能力,又有领域词典的针对性。

3.3 章节情感曲线的绘制与关键结论

情感曲线是harrypotter15-1最有视觉冲击力的输出。我们把七本书的所有章节拼接成一条连续横轴,纵轴是每章的情感均值。绘制时我推荐用matplotlib的折线图,再叠加一个滚动平均值曲线,用来平滑单章波动。

项目中最有意思的发现是:全书情感最低点并不集中在最后一本的某些章节,而是分散在前面几本的“角色死亡章节”。这说明读者记忆里的“压抑时刻”和实际文本情绪最低潮不一定重合。这个问题特别值得做内容运营的人注意:如果你是想做“最虐章节盘点”,完全依靠主观记忆很可能漏掉真正的峰值。

可视化代码大概长这样:

import matplotlib.pyplot as plt plt.figure(figsize=(18, 6)) plt.plot(all_books_chapter_seq, chapter_sentiment, alpha=0.4, label="chapter sentiment") plt.plot(all_books_chapter_seq, rolling_mean, color="#d33", linewidth=2, label="rolling mean") plt.xlabel("chapter sequence") plt.ylabel("sentiment score") plt.legend() plt.savefig("hp_sentiment_curve.png", dpi=200)

最终报告里,我们还会把“得分最低章节Top 10”和“得分最高章节Top 10”列成对照表。这些表格非常适合后续做内容脚本,因为它把情感浓度变成了可引用的数据资产。

4. 人物共现网络:谁和谁在同一个空间里出现

4.1 从共现关系到社交网络

人物关系网络是harrypotter15-1中最出彩、也最容易被误解的部分。很多人以为我们要做真实社交关系分析,其实不是。我们不关心角色之间是否有私交,我们只关心他们在文本中是否“共同出现”。

共现的定义很简单:以每一段对话场景作为统计窗口,如果角色A和角色B的名字同时出现在同一个段落内,就计一次共现。然后把所有章节的共现次数累加,作为A和B之间的边权。窗口越小,关系刻画越紧密;窗口越大,越偏向“同场景但未必互动”。我当时测试了“段落窗口”和“章节窗口”两种方案,最终选择段落窗口,因为它更能体现“实际互动”。

逻辑清楚了,但这个定义有它的局限性。两个角色可能出现在同一个段落里,却根本没有直接互动,比如几十个人同时出现在礼堂的场景。所以我在输出图表时特意加了一个提示标签,说明这是“共现关系代理指标”,不是真实社交强度。后面所有分析都在这个前提下进行。

4.2 用NetworkX构建人物网络图

网络图构建流程分三步。第一步,从语料中抽取所有角色名出现的位置;第二步,按段落窗口聚合共现矩阵;第三步,用NetworkX生成图结构,计算每个节点的度、介数中心度等指标。

角色名识别是这一步最麻烦的事。我们没有用复杂的命名实体识别模型,而是直接维护了一份“角色名单 + 常见别名”的对照表。名单覆盖了七本书中有名字的角色,大约240个。然后配置了一个前缀匹配规则,防止把“汤姆”匹配到“汤姆里德尔”之外的无关对象。

import networkx as nx G = nx.Graph() for pair, weight in cooccur.items(): if weight >= 5: G.add_edge(pair[0], pair[1], weight=weight) degree = dict(G.degree()) betweenness = nx.betweenness_centrality(G) nx.write_graphml(G, "hp_network.graphml")

输出GraphML之后,可以用Gephi做可视化,也可以直接按NetworkX数据画图。个人推荐先用Gephi做布局,因为它可以实时调整社区聚类的配色,跑出来的图比matplotlib好看很多。

4.3 中心度指标的解读:谁是真正的剧情枢纽

网络图出来以后,最值得分析的是不同中心度指标的排序差异。

度中心度衡量的是“谁关联的角色最多”,这个指标可以简单理解成“熟人最多的人”。哈利、罗恩、赫敏毫无悬念地排在最前面,因为他们作为主角,会和几乎所有角色产生共现。介数中心度衡量的是“谁处在信息传递的关键路径上”,它反映的是“如果没有这个角色,很多其他角色之间的关联会变弱”。结果让人意外:麦格教授和邓布利多的介数中心度非常高,甚至在某些局部社区里超过哈利。

这个结论给做内容二创的人提了个醒:主角不一定占据所有的“结构洞”。有些配角虽然不常出现在聚光灯下,却承担着串联群体、推进情节的角色。如果你想挖掘“隐藏关键角色”,介数中心度比出场次数更有价值。

社区发现算法(比如Louvain)可以把人物网分成若干个社区。我们的实验结果显示,分出来的社区大致对应“格兰芬多核心圈”“黑魔法阵营”“魔法部官员圈”等群体。这种分群结构和读者直觉高度吻合,说明用共现网络还原群体归属是可行的。

5. 实操中的坑与排查记录

5.1 人名别名不统一:所有错乱的根源

这个项目里最难处理、也最影响结果的就是人名别名。中文译本里同一个角色会有昵称、全名、头衔等多种称呼。最典型的例子是阿不思·邓布利多,他可能被称为“邓布利多”“阿不思”“老校长”“白胡子老头”。这些称呼在文本里有时指同一个人,有时在特定语境中指向不同信息,必须谨慎合并。

我的处理思路是维护一张总表,包含“标准角色名、所有别名、是否默认替换、备注”。默认替换规则要非常小心,比如“汤姆”如果直接替换成“汤姆·里德尔”,可能会误伤某条指代不明的句子。我当时加了一个条件:只有在临近段落中出现“里德尔”“伏地魔”“黑魔王”等关键词时,才把“汤姆”合并到主角阵营。

5.2 引号嵌套导致的说话者识别问题

做角色发言分析时,最难识别的是“某人引用别人说的话”以及“多角色连续对话”。如果只是统计“哪一段落在说什么”,影响不大;但如果想统计“这个角色在全书说了多少句话”,就会因为引号嵌套而严重失准。

我最终没有做过于复杂的说话者识别模型,而是用了一套半自动规则:先按引号成对提取所有对话,再用对话前后的“某某说/笑道/低语”等线索匹配说话者。匹配不上的对话统一归入“未识别”。在结果分析时,“未识别”比例只要低于15%,我就认为可以接受。

5.3 中文分词的专有名词与情感误判

中文分词对哈利波特里大量自创词很不友好。像“霍格沃茨”“阿尼玛格斯”“摄魂怪”这类词,如果不提前加进自定义词典,会被切得七零八落,导致词频统计偏得非常厉害。解决办法是维护一个持续增长的魔法专有名词词典,每次遇到新的切分错误就补录。

情感模型对魔法词汇也存在误判。SnowNLP对“诅咒”默认打负面分没有问题,但“魔法”这个词在很多语境下是中性甚至偏正面的,模型却可能给出负向权重。这个问题没有完美解法,只能通过“在展示结果时同时标注模型版本和词典版本”来增加透明度。

下面整理一个实用的排查速查表:

问题现象可能原因解决方法我的备注
某角色词频异常高别名未合并完善角色别名映射表先跑一遍“候选别名聚合”再统计
某章情感分成极端负值对话内容被错误切分检查引号清洗规则优先检查连续对话段落
网络图孤立点过多共现阈值设置过高调低最小边权阈值40个孤立点可接受
分词干燥无信息量停用词表过宽缩短停用词列表保留“魔法”等情景词
章节数量对不上标题噪声未被清除人工校对切分点每本输出章节预览文本

5.4 可视化排版时的数据噪音处理

最后一个坑是画图时数据噪音很扎眼。原始章节情感得分会在某些极端章节突然跳成一个异常尖峰,如果直接展示全量折线,读者很容易被异常值带偏。建议做两步处理:第一步,对单章得分做一次轻度平滑;第二步,在图上明确标注“平滑窗口大小”。之前看到很多人画情感曲线不带窗口参数,后续读者根本不知道曲线是原始值还是平均值,这就失去了可复现性。

6. 项目还能怎么玩:从实验到产品

6.1 做成自动化的“角色情绪面板”

harrypotter15-1的第一版只输出了静态图表。受到复盘会上的反馈启发,我又把脚本整理成了一个定时任务:每次更新语料后,自动生成各角色的情绪波动热力图,并输出“情绪变化最大的章节”摘要。这个思路放到业务里,就是对客户舆情做日维度情感监测的简化版本。

技术实现上不复杂,就是把前面写好的所有处理逻辑封装成一个pipeline函数,输入原始语料目录,输出汇总表和一个HTML报告。用Jinja2模板把图表和表格拼进同一个页面,跑完就能直接打开看结果。

6.2 给内容创作提供数据佐证

用数据分析结果反过来指导内容策划,是这个项目比较意外的价值。之前某个兴趣小组做“原著最感人台词盘点”时,直接从情感曲线里选出了几章得分异常的片段,再人工摘出台词,效率和说服力瞬间提升。你可以把这个思路迁移到任何一个影视IP或文学IP的二次创作里。

而且重点在于:当你用数据去支撑一个内容选题时,不需要冒“主观感受没有依据”的风险。比如做《最强隐没幕后角色》这类视频时,直接引用介数中心度榜单,比单纯说“我觉得这个角色很关键”更有理有据。

6.3 沉淀为可复用的NLP教学案例

这个项目最好的归宿是变成一个学习案例模板。把代码、数据说明、结果报告、踩坑汇总整理好后,完全可以作为自然语言处理课程的实战作业。学习者不需要重新开发一套分析框架,只需要把“哈利波特”替换成自己感兴趣的长篇文本,换一份词典和角色表,就能跑通整个流程。

从第一版到现在的迭代,我最大的体会是:文本分析项目的成功不是取决于模型多高级,而是取决于语料清理多彻底。harrypotter15-1里的所有结论,几乎没有一项是“算法聪明”算出来的,全是在“数据干净、规则清晰、结果可解释”的前提下得到的。这可能是这个项目留给我的最重要经验。

最后分享一个小技巧:在所有统计结果交付前,花30分钟人工抽查几个极端值。比如找出情感分最高和最低的章节,实际读一小段原文,看看模型判断是否符合上下文。只要这个抽查能通过,报告才算是真的能拿得出手。很多项目最后被质疑,不是因为算法不对,而是因为连自己都没检查过那些“看起来很反常”的数字。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进