ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python爬虫实战:爬取《西游记》全文并实现词频可视化

Python爬虫实战:爬取《西游记》全文并实现词频可视化 刚开始学Python爬虫的时候我踩过不少坑也走了不少弯路。当时最头疼的是教程看了很多语法也懂一点但真让我自己写个爬虫却不知道从哪下手。后来我悟了别想着一口气爬电商、爬短视频、爬App太复杂很容易劝退。最好的练手项目就是你从小听过、内容稳定、结构清晰、又不涉及版权争议的古典名著。我选了《西游记》做了一个能从头到尾把整本小说按章节抓下来、存进数据库、还可以顺手做个词频可视化的完整爬虫。这篇文章就把我整个实战过程拆开讲清楚从目标分析、环境准备、页面结构分析、核心代码实现到数据存储和问题排查全给你捋一遍。代码我放在对应小节里你可以直接照着跑重点是理解每一步背后的“为什么”。这篇文章适合刚学完Python基础语法、想拿真实项目练手的读者也适合那些会用requests但还没系统做过“采集-清洗-存储-可视化”全流程的人。1. 项目整体设计与思路拆解1.1 爬取目标与技术选型先明确我们要做什么。西游原著一共一百回每回有一个标题比如“灵根育孕源流出 心性修持大道生”然后是一大段正文。我们要做的就是把这一百回的标题和正文全部抓下来存成结构化数据然后再考虑怎么用这些数据。技术选型上我用的组合是requests负责发起HTTP请求拿网页HTML源码BeautifulSoup4负责解析HTML定位标题和正文节点lxml作为BS的解析引擎速度比纯Python快pandas用于数据处理和后续的简单分析SQLAlchemy负责把数据存进SQLite数据库不用手动拼接SQLmatplotlib jieba做词频统计和可视化这套组合对新手非常友好。核心只有requests和BeautifulSoup两个库后面几个都是在数据层面做增强的。不要一开始就上Scrapy框架Scrapy虽然强大但如果还不理解“请求-响应-解析-存储”这条链路用框架会变成黑盒子出问题都不知道去哪查。1.2 为什么选择这种方案从入门到可扩展很多教程喜欢直接给你一份完整代码跑完就完事。但我个人更推荐“分层”的思路每一层只干一件事层与层之间用数据接口衔接。这样做的好处是以后你想换一个爬取目标或者把数据从TXT换成MySQL改动都很小不用从头重写。按这个思路我把项目拆成了四层请求层负责下载页面处理Headers、超时、重试解析层负责从HTML里提取标题和正文存储层负责把清洗后的文本写入TXT、CSV或数据库分析层负责读库、分词、统计、画图项目虽然小但也要刻意练习这种分层思维。这不是过度设计而是让你养成分而治之的习惯。很多职场中的工程问题都是因为所有逻辑混在一个大函数里出了问题根本没法定位。2. 环境准备与页面结构分析2.1 环境搭建与依赖安装如果你还没装Python去官网下载对应系统的安装包就行。安装时记得勾选“Add Python to PATH”不然命令行里敲python会提示找不到命令。我建议直接用Anaconda或者Miniconda做环境管理省去很多折腾依赖的麻烦。到这里“Python安装教程”、“Python入门”、“Python环境配置”这个流程就完成一大半了剩下的就是装包。pip install requests beautifulsoup4 lxml pandas sqlalchemy matplotlib jieba装完之后可以跑个命令验证一下版本python -c import requests; print(requests.__version__)能输出版本号就说明环境没问题。如果国内网络下载慢就把pip源换成清华镜像源这里不展开讲各大搜索引擎都有现成教程。2.2 分析西游记章节页真实结构在写任何爬虫之前第一步永远是打开浏览器按F12看这个页面长什么样。不分析DOM结构就瞎写选择器大概率是会失败的。我找了一个公开的古典名著阅读站点它把每一回都放在独立的一个页面里。我们先看目录页一般目录页的结构是一个列表里面每个li包含一个a标签链接指向具体章节页面。再点进其中一个章节页正文一般放在一个特定的容器里比如div classcontent或者div idcont。这个地方要注意很多网站的正文不是直接在HTML里的有可能是JavaScript渲染的但古典小说站点基本都比较“朴素”直接写在HTML里这对新手非常友好。我一般会这样快速验证在浏览器地址栏输入一个章节页链接然后CtrlU查看源代码如果能直接在源码里搜索到正文文字那就是静态HTML可以用requests直接抓如果搜索不到说明是动态加载那就要换Playwright这类工具来解决。这就是热词里“playwright相比于直接解码爬虫的优点”所涉及的真实场景遇到动态站点requests这套就抓不动了得用浏览器自动化。但对于西游记这种静态页面requests就够了没必要上重武器。2.3 挑选合适的抓取入口这里稍微扩展一下讲讲入口URL的选取策略。有些站点的目录页就是首页有些则是二级页面。我建议不要自己去猜URL而是先访问站点首页找到“西游记”的入口链接再跳转到目录页。拿代码来演示就是import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } # 第一步访问站点首页 homepage https://example.com # 示例域名 resp requests.get(homepage, headersheaders, timeout10) print(resp.status_code)这只是第一步先确认能够连通再往下解析。注意这里的示例域名需要替换成你实际要爬的站点。实战中我更推荐用网络上现成的“西游主题”公开数据页这里不写死某个具体链接是避免站点改版导致本文失效你只要掌握这个分析思路换任何一个同类站点都能上手。3. 核心细节解析与实操要点3.1 章节列表采集拿到目录页的HTML后接下来要做的就是“提取章节链接和章节名”。这里要用到BeautifulSoup的选择器语法。目录页里的链接结构通常长这样div classbook-mulu ul lia href/xiyouji/1.html第一回 灵根育孕源流出 心性修持大道生/a/li lia href/xiyouji/2.html第二回 悟彻菩提真妙理 断魔归本合元神/a/li ... /ul /div那么解析代码就是resp requests.get(catalog_url, headersheaders, timeout10) resp.encoding utf-8 # 具体视网页编码而定 soup BeautifulSoup(resp.text, lxml) chapters [] for li in soup.select(div.book-mulu li a): href li.get(href) title li.get_text(stripTrue) if href and title: full_url catalog_url.rsplit(/, 1)[0] / href.lstrip(/) chapters.append({title: title, url: full_url}) print(len(chapters)) # 理想情况下输出100 print(chapters[:5])这里有几个关键点值得展开说。第一是选择器的写法。div.book-mulu li a是CSS选择器表示“div元素里class为book-mulu的容器其内部所有li下的a标签”。如果你发现站点结构不太一样就回浏览器里右键点击链接选“检查”在Elements面板里找到对应的属性再调整选择器。不要死记选择器要学会自己看结构。第二是URL拼接问题。很多站点的链接都是相对路径比如/xiyouji/1.html前面没有域名。这种情况下一定要手动拼接成完整的绝对URL否则后续请求会失败。拼接的时候注意用rsplit(/, 1)[0]把目录页的末段去掉再接上章节的相对路径。第三是数据存储。这里的chapters列表本身就是一个结构化数据。建议先把它打印出来核对一遍确认章节数量是100链接都能访问再继续下一阶段。每步都验证而不是最后一起跑会省很多事。3.2 清洗正文与提取章节内容章节页的解析稍微麻烦一点因为正文里往往混着大量空白字符、换行和HTML标签。我们目标是拿到“排版干净、适合存储或阅读的纯文本”。我通常会写一个专门的函数来做这件事def parse_chapter(url): resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) # 找到正文容器 content_div soup.select_one(div.content) title soup.select_one(h1).get_text(stripTrue) # 获取所有段落文本 paragraphs content_div.find_all(p) if paragraphs: # 有些站点的正文是按p分段那就拼接所有p标签的文本 text \n.join(p.get_text(stripTrue) for p in paragraphs) else: # 有些则是一大坨文本需要先用br或换行作切分 raw content_div.get_text(\n, stripTrue) text re.sub(r\n{2,}, \n, raw) return {title: title, content: text}这里最容易被忽视的是resp.encoding。古典文学网站很多是GBK编码或GB2312编码如果你不手动指定requests会根据响应头里的Content-Type字段去猜猜错了就会出现一堆乱码。遇到乱码怎么快速排查你可以在Python里做一次自动判断resp requests.get(url, headersheaders, timeout10) # 优先查看响应头里的charset charset resp.encoding print(charset) # 如果明显不对就手动强制解码一次 resp.encoding gbk print(resp.text[:200])如果强制改成gbk之后显示正常那就说明站点是GBK编码你就在代码里手动指定resp.encoding gbk。如果改成utf-8才正常同理用utf-8。这个操作是“爬虫乱码处理”的关键很多新手都卡在这一步。另外还有一种做法是直接读取raw bytes然后用chardet这个库自动检测编码。但实测下来对于这种固定站点手动指定一次编码比自动检测更可靠因为自动检测偶尔也会出错。3.3 存储到TXT与CSV有了章节标题和正文最简单粗暴的存储方式就是写TXT文件。每章一个文件或者全部合成一个大文件都可以。我个人推荐按章保存方便后续程序处理import os os.makedirs(xiyouji, exist_okTrue) def save_to_txt(chapter_data): idx chapter_data[id] title chapter_data[title] content chapter_data[content] filepath fxiyouji/{idx:03d}_{title[:20]}.txt with open(filepath, w, encodingutf-8) as f: f.write(title \n\n) f.write(content) print(f已保存: {filepath})这里用{idx:03d}格式化编号确保第2回会补零成002这样在文件管理器里排序不会乱。文件名里保留标题前20个字避免Windows文件名长度限制。如果想把数据集中管理那就写成CSVimport csv def save_to_csv(chapters): with open(xiyouji.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([id, title, content]) for i, ch in enumerate(chapters, 1): writer.writerow([i, ch[title], ch[content]])需要特别说明的是CSV文件建议用utf-8-sig编码而不是utf-8。utf-8-sig会在文件头部加入BOM这样你用Excel打开CSV时中文就不会乱码。这个小细节很多老手都不一定知道但真的很实用。3.4 使用SQLAlchemy存储到数据库如果数据量再大一点或者你想做更灵活的查询那就应该上数据库。这里我用SQLAlchemy配合SQLite来演示因为没有额外服务需要启动最简单。首先要定义一个模型from sqlalchemy import create_engine, Column, Integer, String, Text from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class Chapter(Base): __tablename__ chapters id Column(Integer, primary_keyTrue, autoincrementTrue) chapter_no Column(Integer) # 第几回 title Column(String(200)) content Column(Text)然后初始化数据库engine create_engine(sqlite:///xiyouji.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session()插入数据的时候可以一次性批量插入obj_list [] for i, ch in enumerate(chapters, 1): obj_list.append(Chapter(chapter_noi, titlech[title], contentch[content])) session.add_all(obj_list) session.commit()这里其实已经涉及了SQLAlchemy的基础用法定义模型、建表、创建会话、批量插入。由于原需求里提到了“sqlalchemy储存爬虫数据”所以我专门展开了这块。之后做数据可视化时直接从这个SQLite库里读取比反复解析TXT要方便得多。4. 实操过程与核心环节实现4.1 完整代码流程整个爬虫的主流程我建议写成一个main.py按步骤来组织。我贴一份核心骨架import time import requests from bs4 import BeautifulSoup from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from models import Chapter, Base HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def get_soup(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 return BeautifulSoup(resp.text, lxml) def crawl_catalog(catalog_url): soup get_soup(catalog_url) links [] for a in soup.select(div.book-mulu li a): href a.get(href) title a.get_text(stripTrue) if href and title: full_url catalog_url.rsplit(/, 1)[0] / href.lstrip(/) links.append({title: title, url: full_url}) return links def crawl_chapter(url): soup get_soup(url) title soup.select_one(h1).get_text(stripTrue) content_div soup.select_one(div.content) paragraphs content_div.find_all(p) if paragraphs: content \n.join(p.get_text(stripTrue) for p in paragraphs) else: content content_div.get_text(\n, stripTrue) return title, content def main(catalog_url): # 1. 抓目录 chapters crawl_catalog(catalog_url) print(f目录解析完成共 {len(chapters)} 章) # 2. 初始化数据库 engine create_engine(sqlite:///xiyouji.db) Base.metadata.create_all(engine) session sessionmaker(bindengine)() # 3. 逐章抓取并入库 for idx, info in enumerate(chapters, 1): print(f正在抓取第 {idx} 章: {info[title]}) try: title, content crawl_chapter(info[url]) chapter Chapter(chapter_noidx, titletitle, contentcontent) session.add(chapter) session.commit() except Exception as e: print(f第 {idx} 章失败: {e}) session.rollback() # 4. 礼貌爬取加一个随机延时 time.sleep(1) print(全部完成) if __name__ __main__: main(https://example.com/xiyouji.html)请求频率这块单独提一下。很多初学者写完爬虫就疯狂循环请求速度快到像是把服务器的门都敲烂了。这不是能不能成功的问题而是你这么做非常容易触发对方站点的封禁机制甚至给自己带来麻烦。所以我每次循环里都会加time.sleep(1)或者直接用random.uniform(0.5, 1.5)造出更自然的请求间隔。4.2 数据质量检查爬完数据之后不要急着往下走。先做一轮数据质量检查不然污染数据会直接影响后续分析。我通常会写几个简单的SQL查一下# 统计总章节数 print(session.query(Chapter).count()) # 查看最长的章节 from sqlalchemy import func chapter_lens session.query( Chapter.chapter_no, Chapter.title, func.length(Chapter.content).label(len) ).order_by(func.length(Chapter.content).desc()).first() print(chapter_lens)正常情况下总章节数应该是100。如果少于100说明有章节抓取失败了需要根据日志重新跑一遍或者单独修复。我在实际操作中遇到过两次抓取失败通常都是因为某个页面结构不规则正文容器的class跟其他页面不一样导致选择器没匹配到内容。这种问题没法完全避免靠的就是“异常捕获失败重试日志”这套机制来兜底。4.3 数据可视化西游记词频统计数据都入库了怎么体现爬虫的价值我建议顺手做个简单的文本可视化算是数据分析与可视化方向的入门。先把所有章节的正文拼起来然后用jieba分词统计词频最后画出Top30的词云或柱状图import jieba import pandas as pd from collections import Counter import matplotlib.pyplot as plt # 从数据库读取全部内容 chapters session.query(Chapter).all() full_text .join(ch.content for ch in chapters) # jieba分词 words jieba.lcut(full_text) # 去除停用词和单字 stopwords {的, 了, 是, 我, 你, 他, 也, 都, 就, 着, 一, 不, 在, 有, 和, 与, 这, 那} filtered [w for w in words if len(w) 1 and w not in stopwords] # 统计词频 counter Counter(filtered) top_words counter.most_common(30) df pd.DataFrame(top_words, columns[word, count]) print(df) # 画图 plt.rcParams[font.sans-serif] [SimHei] # 让matplotlib显示中文 plt.figure(figsize(10, 6)) plt.barh(df[word][::-1], df[count][::-1]) plt.xlabel(出现次数) plt.title(《西游记》高频词Top30) plt.tight_layout() plt.show()运行完之后你会非常直观地发现像“行者”、“师父”、“八戒”、“妖怪”这类词高频出现。这种结果虽然不意外但当你亲手从网页爬数据、存库、分析、画图整个链路跑通之后那种成就感是单纯看教程完全体会不到的。5. 常见问题与排查技巧实录5.1 编码乱码问题这是我见到最多的问题。爬下来的中文全是类似“锟斤拷”或“我”这样的乱码。绝大多数情况下是编码指定错误。解决路径是这样的浏览器里打开目标章节页右键查看网页源代码找到meta charset...确认页面声明的编码。在代码中强制设置resp.encoding为你确认的编码。如果手动设置了resp.encoding gbk还是乱码可能是GB2312、GB18030等同族编码挨个试。也可以用resp.content.decode(utf-8, errorsignore)这种方式手动解码。存储文件时也指定encodingutf-8这样读、写两端都统一。最基础的原则先确认网页是什么编码再决定用哪种方式解码。不要依赖requests的自动判断它不一定准。5.2 请求被拒或返回非200状态码如果直接抓遇到StatusCode 403或者418说明站点做了基础的反爬最常见的反爬手段就是校验User-Agent和Referer。解决方法很简单把浏览器里的请求头复制过来headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example.com/, Accept-Language: zh-CN,zh;q0.9, }如果加了UA还是不行那就检查一下是否请求频率太高被站点临时封了IP。这种情况可以先停掉脚本等几分钟再跑并且在每次请求之间加上延时。还有一个偏门的点有些站点会校验Cookie。如果你发现直接带UA访问还是403就先在浏览器里打开页面然后按F12里复制Cookie请求头把Cookie加到headers里。但是注意Cookie一般有过期时间长跑脚本时要定期更新或者用requests.Session()来保持会话。5.3 页面结构差异导致解析不到数据西游记小说站点往往不是一个人维护的很可能同一个站点里第一回正文在div.content第二回就在div#chapter-content里。遇到这种问题就需要在解析代码里写“多重选择器兜底”def extract_content(soup): content_div (soup.select_one(div.content) or soup.select_one(div#chapter-content) or soup.select_one(article.content)) if not content_div: raise ValueError(找不到正文容器请检查页面结构) return content_div.get_text(\n, stripTrue)这种写法配合异常捕获能大幅提升爬虫的稳固程度。你以后写任何爬虫都要有“页面结构会变”的觉悟。5.4 合规与安全提醒最后这部分我必须非常严肃地提醒你。这几年因为爬虫被抓的新闻不少网上搜一下就能看到。爬虫本身不是原罪关键是看你爬什么、怎么用。我们这次的《西游记》案例是安全的因为它是公共版权作品用于个人学习和研究没有版权问题爬取频率也很低不会影响对方服务器的正常运转。但换到其他场景就要格外小心了不要爬取个人隐私数据比如手机号、住址、交易信息不要爬取有明确版权归属的付费内容、原创文章、影视资源不要高频请求导致对方服务器负载过高这已经可能违法不要绕过对方的技术保护措施比如强行破解登录认证爬下来的数据不要用于商业用途尤其是明码标价的“打包出售”一句话总结我自己的实践原则公开数据、低速抓取、合理使用、结果安全。这四个条件同时满足才算是一个合格的爬虫项目。否则即使技术再炫也存在很大的风险。最后再分享两个小技巧第一个技巧开发的时候把所有章节的抓取流程封装成函数然后先用“间隔取样”的方式测试比如先抓第1回、第50回、第100回确认这三个页面的结构都一致、数据能正常解析再全量跑。这样能极大减少跑到一半才发现问题的尴尬。第二个技巧善用日志而不是print。print虽然方便但程序跑起来以后窗口里刷屏滚动你根本看不清错误。建议用Python内置的logging模块把关键步骤和异常信息写入日志文件这样出问题后可以直接定位。在实际操作中我每次写完爬虫都会再问自己一个问题如果这个网站的某个节点改了我的代码最快多长时间能找到问题并修复如果答案是“很久”那说明代码还写得不够清晰。始终带着这种思维去写爬虫你会在一次次迭代中不知不觉就从“复制代码”变成“真正懂爬虫”的人。
返回列表