资讯动态

英文小说PDF文本分析实战:从清洗到人物共现

发布时间:2026/10/15 16:23:38 来源:尧图企业网站定制
简介《The Love Hypothesis》是Ali Hazelwood于2021年出版的当代romance小说PDF电子书面向喜爱轻松幽默爱情故事、关注女性主义与性别议题的英文原版阅读者。全书围绕女主人公Olive展开讲述这位聪明独立的年轻女性在自卑与对爱情的恐惧中如何逐步相信自己也相信他人并巧妙融入grumpy meets sunshine、fake dating等经典桥段兼具娱乐性与社会文化意义。资源包共1个PDF文件大小约3.66MB内容完整含版权页、目录、献词及各章节正文便于在电脑或移动设备上直接阅读。目前已有4761人学习下载热度可观。读者可借此了解romance小说的定义与特点、经典trope的运用方式、女性主义与性别角色在类型小说中的体现以及Ali Hazelwood的写作风格与叙事技巧适合作为英文原版阅读与类型文学研究的参考素材。1. 从一份 PDF 文件名说起为什么“The Love Hypothesis”值得做一次文本分析实战一份名为The Love Hypothesis by Ali Hazelwood (z-lib.org).pdf的文件表面看只是一本小说的电子版但如果你把它当成一个真实的数据对象它其实是一个非常适合练手的非结构化文本处理样本。它包含完整的章节结构、对话段落、叙述性描写还有 PDF 转换过程中常见的断行、连字符、页眉页脚残留。很多做 NLP 的同行拿到英文小说 PDF 后第一反应是直接丢给模型结果被脏数据坑得怀疑人生。这个标题背后真正值得做的是把“一本英文小说 PDF”变成“可检索、可分析、可复用的干净文本”再基于它跑通章节切分、词频统计、对话抽取、人物共现这几条常见管线。适合谁适合刚接触文本清洗的新手也适合想找一个中等规模英文语料做 pipeline 验证的熟手。下面我按自己实际处理这类文件的顺序把每一步拆开讲。2. 把 PDF 变成可处理的文本工具选型与第一轮清洗2.1 为什么我不推荐直接上 OCR拿到The Love Hypothesis by Ali Hazelwood (z-lib.org).pdf这类文件第一步要判断它是“文字版 PDF”还是“扫描版 PDF”。文字版 PDF 里的字符是可选中的用pdfplumber或PyMuPDF就能直接抽取扫描版才需要 OCR。我一般先用一条命令快速判断python -c import fitz doc fitz.open(The Love Hypothesis by Ali Hazelwood (z-lib.org).pdf) page doc[10] text page.get_text() print(repr(text[:300])) print(字符数:, len(text)) 如果输出里能看到成句的英文单词说明是文字版直接走抽取路线。如果输出大量\x00或空字符串才考虑 OCR。OCR 的坑在于小说里大量对话引号、斜体、破折号会被识别成乱码后期清洗成本远高于文字版直接抽取。我处理过的英文小说 PDF 里九成以上是文字版所以优先用PyMuPDF导入名fitz做批量抽取。2.2 用 PyMuPDF 批量抽取并保留页码标记直接逐页抽取会丢掉页码信息后期排查脏数据时找不到源头。我的做法是在每页文本前插入一个不可见标记方便回溯import fitz def extract_pdf_pages(pdf_path): doc fitz.open(pdf_path) pages [] for i, page in enumerate(doc): raw page.get_text(text) # 插入页码标记格式固定方便后续正则清除 marked fPAGE_{i1}\n{raw} pages.append(marked) doc.close() return \n.join(pages) full_text extract_pdf_pages(The Love Hypothesis by Ali Hazelwood (z-lib.org).pdf) with open(raw_extract.txt, w, encodingutf-8) as f: f.write(full_text) print(总字符数:, len(full_text))这里get_text(text)用的是纯文本模式比get_text(blocks)更接近阅读顺序。页码标记用PAGE_N这种不会自然出现的格式避免和正文冲突。抽完后先别急着清洗打开raw_extract.txt看前 2000 字符重点检查三件事页眉页脚是否混入正文、连字符断行是否严重、章节标题是否完整。2.3 第一轮清洗去页眉页脚、修连字符、合并断行英文小说 PDF 最常见的脏数据是页眉页脚重复出现以及行尾连字符断词。我一般写一个组合清洗函数import re def clean_text(text): # 1. 去掉页码标记行本身但保留位置信息用于调试时可注释掉 text re.sub(rPAGE_\d\n?, , text) # 2. 去掉常见页眉页脚纯数字行、书名重复行 text re.sub(r^\s*\d\s*$, , text, flagsre.MULTILINE) text re.sub(r^\s*The Love Hypothesis\s*$, , text, flagsre.MULTILINE | re.IGNORECASE) # 3. 修复行尾连字符断词word-\nword - wordword text re.sub(r(\w)-\n(\w), r\1\2, text) # 4. 合并被硬换行拆开的句子非空行且上一行不以句末标点结尾 text re.sub(r(?![.!?\])\n(?[a-z]), , text) # 5. 压缩连续空行 text re.sub(r\n{3,}, \n\n, text) return text.strip() cleaned clean_text(full_text) with open(cleaned.txt, w, encodingutf-8) as f: f.write(cleaned) print(清洗后字符数:, len(cleaned))参数说明第 3 步的(\w)-\n(\w)只处理行尾连字符不会误伤正常连字符词如well-known因为后者连字符后没有换行。第 4 步用负向后顾(?![.!?\])确保只在句子未结束时合并避免把两个独立句子粘在一起。清洗后字符数通常会比原始抽取少 5% 到 15%如果减少超过 20%说明正则误删了正文需要回看raw_extract.txt对比。3. 章节切分与结构还原让文本可导航3.1 识别章节标题的三种模式The Love Hypothesis这类英文小说通常用Chapter 1、Chapter One或纯数字作为章节标题。清洗后的文本里章节标题往往独占一行。我一般先用正则扫一遍候选import re chapter_pattern re.compile( r^\s*(Chapter\s(\d|[IVXLCDM]|One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven|Twelve|Thirteen|Fourteen|Fifteen|Sixteen|Seventeen|Eighteen|Nineteen|Twenty))\s*$, re.IGNORECASE | re.MULTILINE ) matches list(chapter_pattern.finditer(cleaned)) for m in matches[:10]: print(m.start(), repr(m.group(0))) print(候选章节数:, len(matches))如果候选数在 20 到 40 之间基本可以确认是章节标题。如果候选数为 0说明标题格式不同需要手动看前几页找规律。我遇到过用1.或I单独成行的也有用装饰性分隔符的这时候就得针对具体文件调整正则没有万能公式。3.2 按章节切分并输出结构化 JSON确认章节标题后按匹配位置切分文本每章存成独立记录import json def split_chapters(text, pattern): matches list(pattern.finditer(text)) chapters [] for i, m in enumerate(matches): start m.end() end matches[i1].start() if i1 len(matches) else len(text) title m.group(0).strip() body text[start:end].strip() chapters.append({ index: i 1, title: title, body: body, char_count: len(body) }) return chapters chapters split_chapters(cleaned, chapter_pattern) with open(chapters.json, w, encodingutf-8) as f: json.dump(chapters, f, ensure_asciiFalse, indent2) for ch in chapters[:5]: print(ch[index], ch[title], ch[char_count])输出 JSON 的好处是后续做词频、对话抽取、人物共现时可以直接按章遍历不用反复解析大文本。char_count字段用来快速发现异常章节——如果某一章只有几十字符大概率是切分点误判需要回查。3.3 验证切分质量三个必查指标切分完不能直接往下走我一般查三个指标章节数是否合理、每章字符数分布是否均匀、首章和末章是否完整。用一段简单统计就能看出来counts [ch[char_count] for ch in chapters] print(章节数:, len(chapters)) print(平均字符数:, sum(counts) // len(counts)) print(最短章:, min(counts), 最长章:, max(counts)) print(首章前100字:, chapters[0][body][:100]) print(末章后100字:, chapters[-1][body][-100:])如果最短章和最长章差距超过 10 倍通常是有章节标题被漏识别或误识别。首章前 100 字应该是正文开头如果出现目录或版权页内容说明清洗时没去掉前置页。末章后 100 字应该是故事结尾如果出现“About the Author”之类说明后置页混入了。这些检查花不了两分钟但能省掉后面大量返工。4. 词频、对话与人物共现三条可复用的分析管线4.1 词频统计去掉停用词后看什么词频是最基础的分析但直接统计会得到一堆the、and、to。我一般用nltk的停用词表加自定义过滤from collections import Counter import re STOPWORDS set( a an the and or but if while of to in on at by for with from as is are was were be been being have has had do does did will would could should may might must this that these those it its he she they them his her their you your i me my .split()) def word_freq(text, top_n30): words re.findall(r[a-zA-Z], text.lower()) filtered [w for w in words if w not in STOPWORDS and len(w) 2] return Counter(filtered).most_common(top_n) for word, cnt in word_freq(cleaned): print(f{word}: {cnt})参数说明len(w) 2过滤掉is、an这类短词但会保留not、but等有语义的词。停用词表可以根据具体文本调整比如小说里said出现频率极高如果做主题分析可以把它加进停用词。词频结果里如果出现大量人名说明这本书对话密集适合做人物共现分析。4.2 对话抽取用引号规则抓取角色发言英文小说对话通常用双引号或单引号包裹。我一般用正则抓取引号内内容再按章统计对话密度import re def extract_dialogues(text): # 匹配双引号内内容排除跨行过长的误匹配 pattern re.compile(r([^]{5,300})) return pattern.findall(text) dialogues extract_dialogues(cleaned) print(对话总数:, len(dialogues)) for d in dialogues[:5]: print(-, d[:80])这里限制长度 5 到 300 字符是为了排除空引号和异常长匹配。如果文本里单引号对话更多把正则改成r([^]{5,300})即可。对话抽取后可以按章统计密度密度突然降低的章节往往是叙述性段落密度高的章节是冲突场景这对理解小说节奏很有帮助。4.3 人物共现从对话上下文找关系人物共现的思路是在同一段或同一章里如果两个名字频繁同时出现说明他们互动多。先定义主要人物名单再按章统计共现次数from itertools import combinations from collections import defaultdict CHARACTERS [olive, adam, holden, malcolm, jeremy, annika] def cooccurrence(text, characters, window500): text_lower text.lower() cooc defaultdict(int) for i, ch in enumerate(characters): for j in range(i1, len(characters)): ch2 characters[j] count 0 for m in re.finditer(re.escape(ch), text_lower): start max(0, m.start() - window) end min(len(text_lower), m.end() window) if ch2 in text_lower[start:end]: count 1 cooc[(ch, ch2)] count return cooc cooc cooccurrence(cleaned, CHARACTERS) for pair, cnt in sorted(cooc.items(), keylambda x: -x[1])[:10]: print(pair, cnt)参数说明window500表示以每个名字出现位置为中心前后各 500 字符内如果出现另一个名字就算一次共现。窗口太小会漏掉远距离互动太大则引入噪声。我一般先用 500 试再看结果调整。人物名单需要手动定义可以从词频结果里挑高频专有名词也可以先跑一遍命名实体识别再筛选。5. 避坑与排查处理英文小说 PDF 时最容易翻车的五件事5.1 连字符修复过度把正常词粘在一起现象清洗后出现wellknown、selfesteem这类错误合并。原因正则(\w)-\n(\w)把行尾连字符和下一行开头直接拼接但有些词本身带连字符且恰好断行。解决修复前先判断连字符前后是否构成常见词或者只对长度超过 6 的片段做合并短片段保留连字符。更稳妥的做法是维护一个例外词表把well-known、self-esteem这类词排除。5.2 章节标题漏识别导致整本书变成一章现象chapters.json里只有一条记录char_count等于全书长度。原因章节标题格式和正则不匹配比如用了CHAPTER ONE全大写或者标题前有装饰符号。解决先打印清洗后文本的前 5000 字符肉眼确认标题长什么样再调整正则。不要迷信通用正则每本书的排版都有差异。5.3 页眉页脚残留污染词频结果现象词频统计里出现书名、作者名、页码数字高频出现。原因清洗时只去掉了纯数字行没去掉重复的书名行。解决统计每行出现次数出现超过 10 次的短行大概率是页眉页脚批量删除。也可以按页分析如果某行在超过 80% 的页面同一位置出现直接标记为页眉页脚。5.4 对话抽取把叙述中的引号也抓进来现象抽取的“对话”里混入了引用、强调性引号。原因正则只认引号不区分对话和引用。解决加一层过滤对话通常以said、asked、replied等动词结尾或开头可以用这些词做上下文验证。另外对话引号通常成对出现且长度适中异常短的引号内容可以丢弃。5.5 人物共现窗口设得太大关系失真现象所有人物之间共现次数都很高看不出差异。原因window设得太大整章内所有人都算共现。解决把窗口缩小到 200 到 300 字符或者改成按段落统计而不是按字符窗口。段落边界是天然的关系单元按段落统计更符合阅读直觉。如果段落太长可以按句子统计。6. 进阶技巧把分析结果做成可交互的检索工具走到这一步你已经有了干净文本、章节结构、词频、对话和人物共现数据。最后一个值得做的技巧是用一个轻量级本地检索脚本把这些串起来方便随时查任意词在书中的分布。我一般用 Python 的re加json就能实现不需要额外依赖import json import re with open(chapters.json, r, encodingutf-8) as f: chapters json.load(f) def search_term(term, context60): results [] pattern re.compile(re.escape(term), re.IGNORECASE) for ch in chapters: for m in pattern.finditer(ch[body]): start max(0, m.start() - context) end min(len(ch[body]), m.end() context) snippet ch[body][start:end].replace(\n, ) results.append({ chapter: ch[index], title: ch[title], snippet: snippet }) return results hits search_term(hypothesis) print(命中数:, len(hits)) for h in hits[:5]: print(f[{h[title]}] ...{h[snippet]}...)这个脚本的价值在于你可以查hypothesis在哪些章节出现结合章节标题判断它是在对话里还是叙述里出现。如果命中集中在某几章说明这个概念是故事中后期才展开的。参数context60控制前后文长度查关键词用 60 够用查长句可以调到 120。输出里保留章节标题方便定位。再进一步可以把人物共现矩阵导出成 CSV用表格软件打开看热力图。我一般会加一列“共现密度”即共现次数除以两人各自出现次数之和这样能消除角色戏份多少的影响。密度高的组合才是真正关系紧密的单纯次数高可能只是因为两人都是主角。最后说一个我自己的习惯每次处理完一本 PDF我都会把清洗前后的字符数、章节数、对话数、Top 20 词频记在一个report.txt里。下次处理同类文件时先看这份报告如果章节数或对话数偏离太远就知道清洗参数需要调。这个习惯帮我省了很多重复排查的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑