Python实战:用NLTK与spaCy对《Undertow》进行文本分析与情感计算
最近在整理经典文学作品的技术化解读方案时,发现许多开发者对如何将文学文本进行结构化分析、情感计算或主题建模很感兴趣。John Galsworthy的《Undertow》作为其社会批判系列的重要作品,其文本本身就是一个丰富的数据集。本文将围绕如何利用现代技术栈(如Python的NLTK、spaCy,以及基础的文本处理流程)对《Undertow》这类文学作品进行数字化处理和分析,提供一个完整的实战教程。无论你是对计算人文感兴趣的学生,还是希望将自然语言处理技术应用于非结构化文本的开发者,都能从本文中获得一套可复用的方法论和代码。
1. 背景与核心概念:文学文本的计算分析
在开始技术实操之前,我们有必要厘清几个核心概念。所谓“文学文本的计算分析”,并非指对文学价值的量化评判,而是运用计算机科学的方法,对文本数据进行提取、清洗、统计和建模,从而揭示一些人力阅读难以快速归纳的模式,如词汇密度、情感变化趋势、人物关系网络、主题分布等。
- 文本即数据:我们将《Undertow》的全文视为一个字符串序列,进而可以对其进行分词、词性标注、命名实体识别等操作。
- 分析目标:常见的分析目标包括:统计词频以了解作者用词习惯;进行情感分析以追踪情节的情绪起伏;提取人物共现关系以构建社交网络;使用主题模型(如LDA)来发现文本中隐含的讨论议题。
- 技术栈定位:本教程将主要使用Python,因为它拥有最成熟的自然语言处理生态。核心库包括用于基础操作的
NLTK、用于工业级NLP的spaCy,以及用于数据分析和可视化的pandas、matplotlib。
本文不会探讨《Undertow》的具体文学内涵,而是专注于提供一个技术框架。你可以将这套方法应用于任何公开的文本数据。
2. 环境准备与版本说明
工欲善其事,必先利其器。以下是本次实战所需的环境和工具。请注意,库的版本迭代较快,以下版本为撰写本文时的稳定版本,若遇到问题,可适当调整版本号。
操作系统:Windows 10/11, macOS, 或 Linux (如Ubuntu 20.04+)均可。编程语言:Python 3.8 或以上版本。推荐使用3.9或3.10以获得更好的兼容性。开发工具:任选其一即可。 * Jupyter Notebook / Jupyter Lab:适合交互式分析和可视化。 * VS Code / PyCharm:适合完整的项目开发。
Python库依赖:我们将创建一个requirements.txt文件来管理依赖。
# requirements.txt # 基础数据处理与可视化 numpy==1.23.5 pandas==1.5.3 matplotlib==3.6.3 seaborn==0.12.2 # 自然语言处理核心库 nltk==3.8.1 spacy==3.5.3 # 网络数据获取(用于示例) requests==2.28.2 beautifulsoup4==4.11.2 # 主题建模 gensim==4.3.1 # 中文环境可忽略此库,本文以英文文本为例 # jieba==0.42.1安装步骤:
- 创建并进入项目目录:
mkdir text_analysis_undertow && cd text_analysis_undertow - 创建虚拟环境(推荐):
python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/macOS 激活 source venv/bin/activate - 安装依赖:
pip install -r requirements.txt - 下载
spaCy的英文核心模型:python -m spacy download en_core_web_sm - 下载
NLTK的必要数据包。我们可以在代码中动态下载,也可以提前下载:python -c “import nltk; nltk.download(‘punkt’); nltk.download(‘stopwords’); nltk.download(‘averaged_perceptron_tagger’)”
项目结构:
text_analysis_undertow/ ├── data/ │ └── undertow_full_text.txt # 存放《Undertow》的纯文本文件 ├── notebooks/ # (可选) Jupyter notebook文件 │ └── undertow_analysis.ipynb ├── src/ # 源代码目录 │ ├── __init__.py │ ├── text_loader.py # 文本加载与清洗模块 │ ├── basic_analysis.py # 基础统计分析模块 │ └── advanced_analysis.py # 高级分析模块 ├── requirements.txt ├── main.py # 主程序入口 └── README.md3. 核心步骤与原理拆解
整个分析流程可以标准化为以下几个核心步骤,理解每一步的目的和原理至关重要。
3.1 数据获取与清洗
目的:获得干净、结构化的文本数据。
- 来源:对于公版图书,可以从古登堡计划等网站获取。本例假设我们已经从合法渠道获得了
undertow_full_text.txt文件。 - 清洗:移除文本中的版权声明、章节标记、多余的空格和换行符,将文本统一为小写(根据分析目标决定)等。这一步是后续所有分析的基础,噪音数据会导致分析结果失真。
3.2 文本预处理
目的:将原始文本转换为适合算法处理的数值或结构形式。
- 分词:将句子拆分成单词或符号的序列。英文分词相对简单,但需处理缩写(如“I‘m”)和连字符。
- 去除停用词:剔除“the”, “a”, “is”, “in”等高频但信息量低的词汇,使分析聚焦于实意词。
- 词形还原:将单词的不同形态(如“running”, “ran”, “runs”)还原为其基本形式“run”。这比词干提取更准确。
3.3 基础统计分析
目的:对文本形成初步的量化认识。
- 词频统计:计算每个词出现的次数,是生成词云和发现关键词的基础。
- 词汇多样性:计算独特词汇数与总词汇数的比例,反映作者用词的丰富程度。
- 句子/段落长度分析:统计平均句长,可能与文本的阅读难度或风格有关。
3.4 高级分析
目的:挖掘深层次的文本模式和语义信息。
- 情感分析:使用预训练词典或模型,为每个句子或章节计算情感极性(正面/负面/中性)和强度。
- 命名实体识别:自动识别文本中的人名、地名、组织机构名等。
- 主题建模:无监督地发现文本中隐藏的主题,例如使用LDA模型,将文档表示为几个主题的混合。
4. 完整实战案例:分析《Undertow》
现在,我们将上述步骤付诸实践。由于《Undertow》的全文较长,此处展示核心代码片段和流程。
4.1 数据加载与初步清洗
首先,我们编写一个文本加载模块。
# src/text_loader.py import re from typing import List class TextLoader: def __init__(self, filepath: str): self.filepath = filepath self.raw_text = “” self.clean_text = “” def load_raw_text(self): “”“加载原始文本文件。”“” try: with open(self.filepath, ‘r’, encoding=‘utf-8’) as f: self.raw_text = f.read() print(f“成功加载文本,长度:{len(self.raw_text)} 字符”) except FileNotFoundError: print(f“错误:文件 {self.filepath} 未找到。”) self.raw_text = “” except UnicodeDecodeError: # 尝试其他编码 try: with open(self.filepath, ‘r’, encoding=‘latin-1’) as f: self.raw_text = f.read() print(f“使用 latin-1 编码加载文本,长度:{len(self.raw_text)} 字符”) except Exception as e: print(f“加载文件失败:{e}”) def basic_clean(self, remove_header_footer: bool = True): “”“执行基础清洗。 参数: remove_header_footer: 是否尝试移除古登堡计划格式的页眉页脚。 ”“” if not self.raw_text: print(“请先加载原始文本。”) return text = self.raw_text if remove_header_footer: # 这是一个简单的启发式方法,实际需要根据具体文本调整 # 例如,移除“*** START OF THE PROJECT GUTENBERG EBOOK ***”之前和“*** END ***”之后的内容 start_markers = [“*** START OF”, “*END THE SMALL PRINT”] end_markers = [“*** END OF”, “End of the Project Gutenberg”] start_idx = -1 for marker in start_markers: idx = text.find(marker) if idx != -1: # 找到标记行结束的位置 start_idx = text.find(‘\n’, idx) break if start_idx != -1: text = text[start_idx:] end_idx = len(text) for marker in end_markers: idx = text.find(marker) if idx != -1: end_idx = idx break text = text[:end_idx] # 统一换行符,移除多余空白 text = re.sub(r‘\r\n’, ‘\n’, text) # Windows换行转Unix换行 text = re.sub(r‘\n+’, ‘\n’, text) # 合并多个空行 text = re.sub(r‘[ \t]+’, ‘ ‘, text) # 合并多个空格/制表符 # 可选的:将文本转为小写(情感分析或词频统计时常用) # text = text.lower() self.clean_text = text.strip() print(f“清洗后文本长度:{len(self.clean_text)} 字符”) def get_clean_text(self) -> str: “”“获取清洗后的文本。”“” return self.clean_text def split_into_sentences(self) -> List[str]: “”“将清洗后的文本分割成句子列表。 注意:这是一个简单的基于标点的分割,对于复杂情况(如缩写‘Mr.’)需要使用NLTK的sent_tokenize。 ”“” if not self.clean_text: print(“请先执行文本清洗。”) return [] # 简单的句子分割 sentences = re.split(r‘(?<=[.!?])\s+’, self.clean_text) return [s for s in sentences if s] # 过滤空句子4.2 使用NLTK进行预处理与基础分析
接下来,我们进行分词、去停用词和词频统计。
# src/basic_analysis.py import nltk from nltk.tokenize import word_tokenize, sent_tokenize from nltk.corpus import stopwords from nltk.stem import WordNetLemmatizer from collections import Counter import string class BasicTextAnalyzer: def __init__(self): nltk.download(‘wordnet’, quiet=True) nltk.download(‘omw-eng’, quiet=True) self.stop_words = set(stopwords.words(‘english’)) self.lemmatizer = WordNetLemmatizer() self.punctuation = set(string.punctuation) def preprocess_text(self, text: str, use_lemmatization: bool = True) -> List[str]: “”“预处理文本:分词、去停用词、词形还原。 返回:处理后的单词列表。 ”“” # 分词 words = word_tokenize(text.lower()) # 转为小写后分词 # 过滤:去除纯标点、数字、停用词 filtered_words = [] for word in words: if word in self.punctuation: continue if word.isdigit(): continue if word in self.stop_words: continue # 词形还原 if use_lemmatization: word = self.lemmatizer.lemmatize(word) filtered_words.append(word) return filtered_words def calculate_word_frequency(self, words: List[str], top_n: int = 30) -> List[tuple]: “”“计算词频,返回最高频的top_n个词及其频率。”“” word_freq = Counter(words) return word_freq.most_common(top_n) def calculate_lexical_diversity(self, words: List[str]) -> float: “”“计算词汇多样性(独特词数 / 总词数)。”“” if not words: return 0.0 unique_words = set(words) return len(unique_words) / len(words) def analyze_sentence_length(self, text: str) -> dict: “”“分析句子长度。”“” sentences = sent_tokenize(text) if not sentences: return {‘avg_words’: 0, ‘num_sentences’: 0} total_words = sum(len(word_tokenize(sent)) for sent in sentences) avg_words = total_words / len(sentences) return { ‘num_sentences’: len(sentences), ‘avg_words_per_sentence’: round(avg_words, 2) }4.3 主程序串联与可视化
现在,我们在main.py中串联整个流程,并生成简单的图表。
# main.py import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.text_loader import TextLoader from src.basic_analysis import BasicTextAnalyzer import matplotlib.pyplot as plt def main(): # 1. 加载并清洗文本 data_path = ‘./data/undertow_full_text.txt’ # 请确保文件存在 loader = TextLoader(data_path) loader.load_raw_text() loader.basic_clean(remove_header_footer=True) clean_text = loader.get_clean_text() if not clean_text: print(“无法获取清洗后的文本,程序退出。”) return # 2. 基础分析 analyzer = BasicTextAnalyzer() processed_words = analyzer.preprocess_text(clean_text, use_lemmatization=True) print(f“预处理后单词总数:{len(processed_words)}“) print(f“独特单词数:{len(set(processed_words))}“) # 3. 词频分析 top_words = analyzer.calculate_word_frequency(processed_words, top_n=20) print(“\nTop 20 高频词(已去除停用词和词形还原):”) for word, freq in top_words: print(f” {word}: {freq}“) # 4. 词汇多样性 lexical_diversity = analyzer.calculate_lexical_diversity(processed_words) print(f”\n词汇多样性指数:{lexical_diversity:.4f}“) # 5. 句子长度分析 sent_stats = analyzer.analyze_sentence_length(clean_text) print(f”句子总数:{sent_stats[‘num_sentences’]}“) print(f”平均每句单词数:{sent_stats[‘avg_words_per_sentence’]}“) # 6. 可视化:词频条形图 words, freqs = zip(*top_words) # 解压元组列表 plt.figure(figsize=(12, 6)) plt.bar(words, freqs, color=‘skyblue’) plt.xlabel(‘Words’) plt.ylabel(‘Frequency’) plt.title(‘Top 20 Frequent Words in “Undertow” (after preprocessing)’) plt.xticks(rotation=45, ha=‘right’) plt.tight_layout() plt.savefig(‘./top_words_frequency.png’, dpi=300) plt.show() # 7. (进阶)可以在此处调用高级分析模块,例如情感分析 # from src.advanced_analysis import SentimentAnalyzer # sa = SentimentAnalyzer() # sentiment_result = sa.analyze_chapter_sentiment(clean_text) # … 可视化情感趋势 … if __name__ == “__main__”: main()4.4 运行与结果解读
运行python main.py后,你将在控制台看到类似如下的输出(具体数字取决于文本内容):
成功加载文本,长度:425671 字符 清洗后文本长度:401234 字符 预处理后单词总数:75432 独特单词数:11245 Top 20 高频词(已去除停用词和词形还原): man: 450 life: 321 time: 298 old: 287 face: 265 hand: 250 eye: 235 thing: 228 day: 225 heart: 220 ... 词汇多样性指数:0.1491 句子总数:12543 平均每句单词数:15.2同时,程序会生成一张top_words_frequency.png的柱状图,直观展示高频词。
结果解读:
- 高频词:如“man”、“life”、“time”、“old”等词的高频出现,可能与Galsworthy在《Undertow》中探讨的人生、时间、衰老等主题密切相关。
- 词汇多样性:约为0.149,意味着平均每6-7个词中会出现一个新词。这个值可以与其他作品对比,以分析作者的写作风格。
- 平均句长:15.2个单词,属于中等偏长的句子结构,这可能与20世纪初英语文学作品的书面语风格有关。
5. 常见问题与排查思路
在实际操作中,你可能会遇到以下问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
FileNotFoundError或加载文本为空 | 1. 文件路径错误。 2. 文件编码不匹配。 | 1. 使用os.path.exists(‘your_file.txt’)检查路径。2. 尝试不同的编码( utf-8,latin-1,cp1252)打开文件。 |
LookupError或 NLTK 数据包未找到 | NLTK 数据包(如punkt,stopwords)未下载。 | 在代码开头或命令行运行nltk.download(‘punkt’)等。或在初始化时加入quiet=True参数静默下载。 |
| 词频结果中包含无意义的符号或数字 | 文本清洗不彻底,或分词未过滤标点数字。 | 检查preprocess_text函数中的过滤逻辑,确保标点集string.punctuation和数字word.isdigit()被正确过滤。 |
| 情感分析结果全部为中性或不准 | 1. 使用的词典不适合文学文本。 2. 未进行上下文考虑。 | 1. 尝试使用更专业的文学情感词典(如NRC Emotion Lexicon)。 2. 考虑使用基于Transformer的预训练模型(如 textblob或transformers库)进行上下文情感分析。 |
| 运行速度非常慢 | 1. 文本量极大。 2. 使用了复杂的模型(如未优化的spaCy大模型)。 | 1. 对于超长文本,考虑分块处理。 2. 使用 spacy.load(‘en_core_web_sm’)而非‘en_core_web_lg’。对于基础NER和分词,小模型足够且更快。 |
| LDA主题模型结果难以解释 | 1. 预处理不当,噪声多。 2. 主题数量 num_topics设置不合理。3. 未过滤极端高频或低频词。 | 1. 加强清洗,确保使用词形还原。 2. 通过一致性分数或人工解读尝试不同的主题数(如5, 10, 15)。 3. 在生成词典时设置 no_below=5(过滤出现少于5次的词)和no_above=0.5(过滤出现在50%以上文档中的词)。 |
6. 最佳实践与工程建议
将文本分析项目工程化,不仅能提高效率,也便于复现和协作。
- 模块化设计:如示例所示,将数据加载、清洗、基础分析、高级分析分离成不同模块或类。这使得代码清晰,易于测试和维护。
- 配置化管理:将文件路径、停用词列表、模型参数等写入配置文件(如
config.yaml或settings.py),避免硬编码。 - 管道化处理:对于固定的分析流程,可以考虑使用
scikit-learn的Pipeline或自定义处理管道,将清洗、分词、向量化等步骤串联起来。 - 处理大文本:对于小说全集或大型语料库,避免一次性加载到内存。使用生成器或分块读取文件进行处理。
- 版本控制与可复现性:
- 使用
requirements.txt或Pipenv/Poetry严格管理依赖包版本。 - 对原始数据和分析结果(如生成的图表、统计表格)进行快照管理。
- 在Jupyter Notebook中,可以使用
watermark等扩展记录运行环境。
- 使用
- 结果可视化与报告:除了生成图片,可以使用
Jupyter Notebook或Markdown文件将分析过程、代码、结果和解读整合成一份完整的报告。对于交互式可视化,Plotly或Altair是不错的选择。 - 尊重版权与伦理:确保使用的文本数据是公开或已获授权的。在发表分析结果时,注明数据来源。计算分析是辅助工具,应避免对文学作品进行过度简化或断章取义的结论。
7. 总结与扩展方向
通过本文的实践,我们完成了一个完整的文学文本计算分析流程:从数据获取清洗、到预处理、再到基础统计和可视化。这套方法不仅适用于《Undertow》,也适用于任何可供分析的文本数据。
掌握了基础之后,你可以从以下几个方向深入探索:
- 深入情感分析:使用
VADER词典(对社交媒体和文学文本更敏感)或基于BERT的微调模型,进行更精细的章节级情感趋势绘图。 - 构建人物关系网络:利用
spaCy的命名实体识别功能,提取文中所有人名,然后以同一段落或句子中共现为边,用networkx库绘制人物关系图。 - 主题建模探索:使用
gensim库的LDA模型,探索《Undertow》中隐藏的主题结构,并可视化主题-词分布。 - 风格对比分析:获取Galsworthy的其他作品(如《福尔赛世家》)或其他同时代作家的作品,对比他们的词汇多样性、平均句长、高频词等特征,进行量化风格比较。
- 部署为Web应用:使用
Flask或Streamlit快速搭建一个Web应用,上传文本文件即可自动生成分析报告。
技术是解读人文的新视角。希望本文提供的工具和思路,能帮助你打开文本数据分析的大门,在字里行间发现更多有趣的模式。动手尝试修改代码,分析你感兴趣的文本,是学习的最佳途径。如果在实践中遇到新的问题,回顾“常见问题”部分,并善用搜索引擎和开源社区,大多数技术难题都能找到解决方案。