Python爬虫实战:整本小说抓取脚本从零搭建与工程化
1. 从零搭建一个整本小说抓取脚本先想清楚这三件事很多人第一次写小说爬虫脑子里只有一个念头把网址丢给requests然后循环翻页把文字存下来。真跑起来才发现目录页的链接抓不全、正文里混着广告、章节顺序乱成一锅粥、跑到一半被封了 IP。问题不在代码写得多复杂而在于动手之前没想清楚三件事目标站点的页面结构长什么样、整本小说的数据流是怎么走的、以及怎么在不给站点添麻烦的前提下把活干完。这篇内容就是围绕“用 Python 爬取小说网站的整本小说”这个目标把从分析页面到落地成可复用脚本的完整链路拆开讲。适合两类人看一类是刚学完requests和BeautifulSoup、想找个真实项目练手的新手另一类是写过零散爬虫、但一到“整本抓取”就翻车的朋友。我会把每一步为什么这么做讲透包括编码坑、限速策略、断点续爬这些文档里不常写、但实际一定会遇到的东西。先明确一个前提本文所有示例都基于公开可访问、允许正常阅读的页面结构做技术演示抓取行为请控制在小规模、低频次仅用于个人学习。真正要长期跑务必先看目标站点的 robots 协议和使用条款这是从业者的基本素养不是可选项。整本小说抓取的本质其实是一条清晰的数据流水线目录页解析出章节列表 → 逐章请求正文页 → 提取正文文本 → 清洗并落盘。听起来简单但每一环都有坑。下面按这条流水线的顺序把每个环节的选型理由、代码实现和踩坑经验讲清楚。2. 目录页解析整本抓取的起点也是最容易漏章节的地方2.1 先判断目录是“单页全量”还是“分页加载”打开一本小说的目录页第一件事不是急着写选择器而是滚动到底部看有没有“下一页”“第 2 页”这类分页控件。这一步决定了你的抓取逻辑是单层循环还是双层循环。单页全量目录所有章节链接都在同一个 HTML 里直接一次解析拿全最简单。分页目录目录本身分了好几页需要先翻完目录页收集全部章节链接再进入正文抓取。动态加载目录滚动时才通过接口拉取章节列表HTML 源码里看不到完整链接这种情况要么找接口要么用能执行 JS 的方案。判断方法很直接在浏览器里按 F12 打开开发者工具切到 Network 面板刷新页面看章节列表是不是跟着某个 XHR/Fetch 请求一起回来的。如果是那目录数据大概率来自接口直接请求接口比解析 HTML 更稳。我一般会先用下面这段代码快速探一下页面结构确认章节链接的分布规律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 Safari/537.36 } url https://example-novel-site.com/book/12345/ resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 关键先探测真实编码 soup BeautifulSoup(resp.text, html.parser) # 打印所有链接观察章节链接的规律 for a in soup.find_all(a, hrefTrue)[:30]: print(a.get_text(stripTrue), -, a[href])跑一遍就能看出章节链接有没有统一的前缀规律比如都带/chapter/或者都以数字结尾。这个规律是后面写选择器的依据。2.2 用 CSS 选择器精准锁定章节列表目录页往往还混着“最新更新”“推荐阅读”“分类导航”等一堆链接如果直接find_all(a)会把无关链接也抓进来。正确做法是先用开发者工具定位到包裹章节列表的那个容器再在容器内部取链接。假设章节列表在一个idlist的div里每个章节是dd a代码就是这样chapter_list [] container soup.find(div, idlist) if container: for a in container.find_all(a, hrefTrue): chapter_list.append({ title: a.get_text(stripTrue), url: a[href] }) print(f共解析到 {len(chapter_list)} 章)这里有个经验点相对链接一定要补全成绝对链接。目录页里的href经常是/chapter/123.html这种相对路径直接拿去请求会失败。用urllib.parse.urljoin处理最稳妥from urllib.parse import urljoin base https://example-novel-site.com for ch in chapter_list: ch[url] urljoin(base, ch[url])2.3 章节去重与顺序校验别让重复链接毁掉整本书有些站点的目录里同一章会出现两次比如置顶的最新章节又出现在列表里或者“上一页/下一页”的链接被误当成章节。抓之前做一次去重和顺序检查能省掉后面大量麻烦。seen set() unique_chapters [] for ch in chapter_list: if ch[url] not in seen: seen.add(ch[url]) unique_chapters.append(ch) print(f去重后剩余 {len(unique_chapters)} 章)顺序方面如果目录本身是按章节顺序排列的直接沿用即可如果发现顺序乱了可以尝试从 URL 里的数字提取章节号排序。但要注意不是所有站点 URL 都带连续数字强行排序反而可能打乱所以优先相信目录页的原始顺序这是最可靠的。提示解析完目录后先把章节数量和第一章、最后一章的标题打印出来人工核对一遍。我踩过的坑里有一半是目录阶段就漏了章节后面正文抓得再漂亮也是残缺的。3. 正文页提取编码、选择器与广告清洗的实战处理3.1 编码问题中文小说乱码的根源与解法中文小说站最容易翻车的就是编码。requests默认会根据响应头猜编码但很多站点响应头写的是ISO-8859-1或者干脆不写导致resp.text出来全是乱码。正确姿势是不要盲目相信resp.encoding而是用apparent_encoding让chardet系列库去探测真实编码resp requests.get(chapter_url, headersheaders, timeout10) resp.encoding resp.apparent_encoding html resp.text如果发现探测结果还是不对可以手动指定。国内小说站常见的是gbk、gb2312、utf-8三种。判断方法用resp.content拿到原始字节分别用这几种编码解码看哪种出来的中文正常for enc in [utf-8, gbk, gb2312]: try: text resp.content.decode(enc) if 第 in text and 章 in text: print(f编码可能是 {enc}) break except UnicodeDecodeError: continue这个技巧在批量抓取时特别有用因为同一个站不同页面偶尔会出现编码不一致的情况。3.2 正文选择器从容器到段落的逐层定位正文提取的核心是找到那个包裹正文的容器通常是idcontent、classcontent或idchaptercontent这类命名。定位到容器后正文往往是一堆br分隔的文本或者包在p标签里。soup BeautifulSoup(html, html.parser) content_div soup.find(div, idcontent) or soup.find(div, class_content) if content_div: # 把 br 替换成换行方便后续按段落处理 for br in content_div.find_all(br): br.replace_with(\n) raw_text content_div.get_text()这里有个细节get_text()默认不加分隔符会把相邻标签的文字粘在一起。加上separator\n或者先处理br更保险。我习惯先把br换成换行符再get_text()这样段落结构保留得最完整。3.3 广告与无关内容的清洗规则正文里混广告是常态常见的有章节末尾的“本站域名”“请记住本站”“手机用户请访问”、正文中间的推广链接、以及一些乱码符号。清洗思路是用规则过滤而不是逐条硬删。我一般会维护一个关键词黑名单逐行过滤import re ad_keywords [本站, 请记住, 手机用户, 最新网址, 免费阅读, 加入书签, 推荐票] def clean_content(text): lines text.split(\n) cleaned [] for line in lines: line line.strip() if not line: continue if any(kw in line for kw in ad_keywords): continue # 去掉常见的全角空格和特殊符号 line re.sub(r[\u3000\xa0], , line) cleaned.append(line) return \n.join(cleaned)这套规则不是万能的不同站点广告词不一样需要针对目标站点微调。但思路是通用的先观察几章正文把反复出现的广告词收集起来形成黑名单。跑个十几章之后黑名单基本就稳定了。注意清洗时不要过度删除比如把正常的对话内容也误伤。建议先跑几章把清洗前后的文本对比一下确认没有误删再批量执行。4. 整本抓取的工程化限速、重试与断点续爬4.1 为什么必须限速以及怎么限才合理一次性几百个请求打过去轻则被临时限制重则 IP 被拉黑整本抓取直接中断。限速不是可选项是整本抓取能不能跑完的关键。最朴素的做法是每请求一章time.sleep一下import time import random time.sleep(random.uniform(1.0, 2.5))用随机间隔比固定间隔更自然避免形成明显的机器节奏。间隔设多少合适我的经验是单线程 1 到 3 秒比较稳妥具体看站点响应速度。如果站点本身很快1 秒也够如果响应慢适当拉长。如果章节特别多单线程太慢可以考虑小规模并发但并发数一定要压得很低比如 2 到 3 个线程并且每个线程各自限速。并发越高被识别的风险越大得不偿失。4.2 请求失败的重试机制网络抖动、临时超时、偶发的 5xx 错误在几百次请求里几乎必然出现。没有重试机制跑到一半断了就得从头再来。用requests配合简单的重试循环就能解决def fetch(url, headers, retries3): for i in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f第 {i1} 次请求失败: {e}) time.sleep(2 * (i 1)) # 退避等待 return None退避等待用2 * (i 1)这种递增方式给站点喘息时间也提高后续请求的成功率。重试次数别设太多3 次足够再多说明站点可能已经在限制你了继续硬刚只会更糟。4.3 断点续爬让中断不再意味着从头再来整本小说动辄几百上千章跑到一半因为网络或限制中断是常事。如果每次都从头开始既浪费时间又增加请求量。断点续爬的核心是记录已完成的章节。最简单的方案是用一个进度文件每抓完一章就追加一行import os def load_done(progress_file): if not os.path.exists(progress_file): return set() with open(progress_file, r, encodingutf-8) as f: return set(line.strip() for line in f) def mark_done(progress_file, chapter_url): with open(progress_file, a, encodingutf-8) as f: f.write(chapter_url \n)主循环里判断一下已完成的直接跳过done load_done(progress.txt) for ch in unique_chapters: if ch[url] in done: continue html fetch(ch[url], headers) if html is None: continue # ... 解析、清洗、写入 ... mark_done(progress.txt, ch[url]) time.sleep(random.uniform(1.0, 2.5))这套机制跑起来之后就算中途断了重新运行脚本会自动从断点继续非常省心。我强烈建议从第一版脚本就加上断点续爬不要等踩了坑才补。4.4 落盘策略边抓边写别攒在内存里新手常犯的错是把所有章节内容先存到一个大列表里最后一次性写文件。问题是一旦中途崩溃内存里的内容全丢而且几百章文本堆在内存里也不优雅。正确做法是边抓边写每抓完一章就追加到文件with open(novel.txt, a, encodingutf-8) as f: f.write(f\n\n{ch[title]}\n\n) f.write(cleaned_text)用追加模式a配合断点续爬整个流程就非常健壮了。如果希望每章单独成文件也可以按章节序号命名方便后续整理。5. 常见翻车场景与排查思路5.1 抓到的正文是空的或者只有几个字这种情况八成是选择器没匹配上。排查步骤先把resp.text存成 HTML 文件用浏览器打开手动确认正文容器的id或class。有时候站点会给正文容器加随机后缀比如content_1234这时候用find(div, idre.compile(content))这种正则匹配更稳。还有一种可能是正文通过 JS 动态渲染HTML 源码里根本没有。判断方法看resp.text里有没有正文关键词。如果没有就得考虑接口方案或者能执行 JS 的工具。5.2 章节顺序错乱或缺失顺序错乱通常是目录页本身排序有问题或者去重时打乱了顺序。解决方法是保留目录页的原始顺序去重时用有序去重比如遍历时判断是否见过而不是用 set 直接转列表。章节缺失则要检查目录解析是否漏了分页。如果目录分了好几页只解析了第一页后面自然全丢。回到目录页确认分页控件把每一页都解析一遍。5.3 跑到一半请求全部失败这基本就是被限速或临时限制了。处理方式立即停止脚本等待一段时间比如十几分钟到半小时然后降低请求频率再继续。断点续爬在这里就体现出价值了不用从头再来。如果降低频率后还是失败可能需要更换请求头里的User-Agent或者检查是不是触发了某种频率阈值。但根本上降低请求量、拉长间隔才是长久之计。5.4 编码在部分章节突然乱码前面提过同一站点不同页面编码可能不一致。解决办法是在fetch函数里做编码探测而不是全局写死一种编码。用apparent_encoding加手动兜底判断基本能覆盖绝大多数情况。6. 把脚本整理成可复用的结构6.1 模块划分建议跑通之后建议把脚本整理成几个清晰的模块方便以后换站点复用配置区目标 URL、请求头、限速间隔、文件路径全部提到最上面。目录解析函数输入目录页 URL返回章节列表。正文抓取函数输入章节 URL返回清洗后的正文。主流程串联目录解析、循环抓取、断点续爬、落盘。这样换一个站点时只需要改配置和两个解析函数里的选择器主流程完全不用动。6.2 一个容易忽略的细节请求头要像正常浏览器只带User-Agent有时候不够加上Referer和Accept-Language会更自然headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: https://example-novel-site.com/, Accept-Language: zh-CN,zh;q0.9, }Referer设成站点首页很多站点会校验这个字段缺失时直接拒绝。这是实测下来很有效的一个小改动。6.3 关于合规与频率的最后提醒技术本身是中性的但抓取行为要有边界。我的原则是只抓自己需要阅读的内容控制频率不并发轰炸不用于任何形式的再分发。如果站点提供了官方阅读渠道优先用官方渠道。写爬虫练手没问题但心里要有一根弦别给站点造成实际负担。我个人在实际操作中的体会是整本小说抓取最难的不是写代码而是耐心。限速、重试、断点续爬这些机制本质上都是在跟“急”这个心态对抗。把节奏放慢把异常处理做扎实一个几百章的脚本跑下来其实非常稳。反过来如果一味追求速度往往跑不到一半就各种报错最后花的时间反而更多。

相关新闻

SpringBoot Banner自定义指南:从加载机制到工程化实践

SpringBoot Banner自定义指南:从加载机制到工程化实践

1. 项目概述与核心价值拆解SpringBoot 应用每次启动时,控制台第一眼跳出来的那段字符画,就是 Banner。很多人把它当成一个花哨的小装饰,启动完就翻篇了。但如果你真正在团队协作、多环境部署、服务治理这些场景里待过,就会发现这个…

2026/10/12 18:17:09 阅读更多 →
植被遥感物理基础:从叶片光谱到参数反演的核心逻辑

植被遥感物理基础:从叶片光谱到参数反演的核心逻辑

写在前面:这年头做遥感的人不少,但很多人一上来就咔咔跑代码、深度学习、训练模型,手上一堆反射率、植被指数,却搞不清楚这些数到底怎么来的、物理意义是什么、换了个传感器为什么就不灵了。这篇东西不是教科书,是我这…

2026/10/12 18:17:09 阅读更多 →
Java连接Access数据库:UCanAccess驱动配置、加权算法与避坑指南

Java连接Access数据库:UCanAccess驱动配置、加权算法与避坑指南

简介:面向高校计算机相关专业毕业设计的Java与Access综合测评系统完整资料包,包含源代码、毕业论文、开题报告与任务书,可帮助完成选题、系统开发与论文撰写全流程。系统针对人工计算综合测评耗时长、易出错、录入繁琐等痛点,实现…

2026/10/12 18:17:09 阅读更多 →

最新新闻

告别丑哭界面!Claude+Cursor一句话生成高颜值设计稿,附完整提示词与TaoToken配置

告别丑哭界面!Claude+Cursor一句话生成高颜值设计稿,附完整提示词与TaoToken配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 19:52:10 阅读更多 →
从零啃DSO源码:初始化阶段核心模块与实现细节解析

从零啃DSO源码:初始化阶段核心模块与实现细节解析

1. 从零啃DSO源码:初始化阶段到底在做什么第一次打开DSO(Direct Sparse Odometry)的源码时,我的反应和大多数人一样——文件不多,但每个文件都像天书。尤其是初始化部分,代码量不算大,但涉及的概…

2026/10/12 19:52:10 阅读更多 →
网友发现的 Claude Code 神器:把 settings 改到 TaoToken 后,GLM 与 MCP 表现实测

网友发现的 Claude Code 神器:把 settings 改到 TaoToken 后,GLM 与 MCP 表现实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 19:52:10 阅读更多 →
Python股市情感分析实战:从文本到可回测的情绪信号

Python股市情感分析实战:从文本到可回测的情绪信号

简介:这份基于Python构建的股市情感分析项目,恰逢毕业设计、课程设计与项目开发需求,提供从股评语料处理、情感分类模型训练,到情绪指数计算、股市行情图绘制的完整方案。项目围绕情绪与股市的关系展开,采用标注语料分…

2026/10/12 19:52:10 阅读更多 →
2026年国内外最新AI大模型汇总(含搜索模型):用TaoToken统一Key实测主流模型接入

2026年国内外最新AI大模型汇总(含搜索模型):用TaoToken统一Key实测主流模型接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 19:52:10 阅读更多 →
Scala与Java混编Spark大数据平台源码解析与实战调优

Scala与Java混编Spark大数据平台源码解析与实战调优

简介:这份源码资源面向大数据开发初学者与进阶工程师,提供一套基于Scala与Java构建的Spark大数据处理平台完整实现,可用于学习分布式计算框架的工程结构、核心模块划分与多语言协作方式。压缩包共约2000个文件,大小103.62MB&#…

2026/10/12 19:51:10 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →