首页 / 资讯中心 / 文章详情

小说网站爬虫实战:从目录到TXT的完整Python实现

小说网站爬虫实战:从目录到TXT的完整Python实现 ★ FEATURED ARTICLE
搞爬虫的人应该都见过这类需求任选一个小说网站爬取任意一部小说以记事本的形式保存。看起来就一句话实际上踩坑点非常多。我前阵子接了一个类似的项目目标是抓取一部网络签约小说并输出为纯文本文件最终能直接用系统自带的记事本打开、阅读、检索。这篇文章就把我从选型到实现、再到各类坑的完整经历捋一遍代码可以直接复用遇到同类需求改改 URL 和选择器就能跑。先说结论别看小说网站长得都差不多它们的反爬策略、编码规范、DOM 结构千差万别。你要是没做过这类采集第一个坑就会在编码上栽倒第二个坑大概率是把章节内容缓存串了至于第三个坑90% 的人会忽略——正文里混着广告和 HTML 标签。这篇文章我会把这些问题全部展开给出从零到一的可运行方案。1. 需求拆解与整体方案设计1.1 项目目标到底要怎么理解“任选一小说网站抓一部小说以记事本保存”听起来自由发挥但落到交付层面就变成几个明确约束输入是小说网站不是本地文件也不是现成的下载接口输出是一部完整小说的正文内容不是目录、不是书评保存格式必须能被“记事本”直接打开也就是最传统的 .txt 文本文件中间不能手工复制粘贴全过程应该是代码自动化处理的。你要是做给老师看、做给甲方看这就是一个非常标准的 Python 爬虫综合训练项目。它涵盖了网页请求、HTML 解析、编码转换、文件读写、异常处理、反爬应对等几乎全部基础知识点。这也是它在各种毕设、实训课程中出现频率极高的原因。我遇到过不少人拿这个题目来问我他们第一反应是选大站比如很多人习惯开抓某知名阅读平台结果反爬强到连目录都爆不出来。我的建议很实际目标站点不一定选最出名的要选结构最“诚实”的最好是列表页和正文章节能直接用标准选择器匹配的。1.2 方案选型请求库、解析库与分析工具我这次选用 Python 3 requests BeautifulSoup4没有上 Scrapy。原因很简单单本小说、百来个章节、数据量也就几 MB上框架属于杀鸡用牛刀。requests 轻量直接适合快速验证BeautifulSoup 的 find() 和 select() 对新手极其友好标签层级复杂时也不容易写崩。正则表达式我也用了但不是作为主解器而是用来做文本清洗。章节名、正文段落、分页跳转这些用 bs4 处理很舒服但把页面里混着的 script 标签、注释符、全角空格清理干净用 re 更痛快。浏览器开发者工具是整个项目里最核心的分析工具。你不要上手就写代码必须先打开目标网站按 F12 切到网络面板刷新页面观察请求的 URL 结构、请求方法、响应内容。我通常先看 HTML 响应确认数据到底是服务端直接渲染出来的还是前端调用接口动态返回的。如果直接在响应里能看到小说名和章节正文说明是服务端渲染requests 就能搞定如果看到一堆 JSON 或者空白那你可能还得补一层 JS 渲染逻辑复杂度立刻上升。有些人喜欢用 Selenium 或 Playwright 兜底遇到页面全动态就直接开浏览器模拟。我的观点是一个小说站如果连正文内容都不敢直接渲染在服务器端那它的反爬强度大概率远超你的单本采集需求换一站会更划算。做单本抓取不需要硬刚避开强风控站点本身就是一种工程智慧。2. 环境准备与目标页面结构分析2.1 本地环境搭建与依赖安装如果你是从零开始先建一个干净的虚拟环境这是避免依赖冲突的最稳做法。python -m venv novel_env source novel_env/bin/activate # Windows 用 novel_env\Scripts\activate pip install requests beautifulsoup4 lxml我使用 lxml 作为 BeautifulSoup 的解析器原因有二它是 C 实现的解析速度快容错能力强面对不太规范的 HTML 也能尽量保证结构完整。解析器之间的差异在你测试多几个站点后会感受特别明显。标准库的 html.parser 也能跑但遇到缺失闭合标签的时候列表容易把章节顺序搞错。然后我建议直接进入目标站点选一部全书章节数适中的书。有人非要选上千章的也可以但初版调试时建议控制章节量先抓前十章验证流程跑通了再全量跑。2.2 页面结构分析与信息路径梳理我们进入目标站后有价值的信息通常分布在三个层级第一层是小说详情页这里至少有书名、作者、简介、封面图以及最重要的——章节目录入口。大多数传统小说站的目录页要么就是详情页本身的下半部分要么是单独的“全部章节”链接。我在这次项目里遇到的站点就是后者详情页中存在一个a href/read/12345/查看目录/a这样的跳转按钮。第二层是目录列表页这里会有完整的章节目录结构通常是div idlist dl dt正文卷/dt dda href/chapter/12345/1.html第一章 起点/a/dd dda href/chapter/12345/2.html第二章 转折/a/dd /dl /div这个结构非常友好重点提取所有dd下的a标签同时拿到 href 和文本一个是章节链接一个是章节标题。第三层是正文页它承载了整部小说的内容。常见结构大约长这样div idcontent 正文第一段内容…… 正文第二段内容…… br/br/ /div有些站点的正文会按段落拆到多个p标签里有些则挤在一个div里用br分隔。我在这次项目中用的是后者清洗策略也随之不同。要特别留意正文块里是否混有其他元素。真实的小说网站正文区经常夹带导航链接、上一章下一章按钮、广告脚本、甚至是作者的话。如果你直接用 get_text() 全量提取最终记事本打开后满屏都是乱七八糟的 URL那肯定不合格。所以必须先用 CSS 选择器锁住正文容器再做内部清理。2.3 链接拼接与相对路径处理目录页和正文章节里爬出来的 href 大多是相对路径比如/read/12345/1.html。这类路径必须以站点域名拼接成完整 URLimport requests from urllib.parse import urljoin base_url https://example.com chapter_url urljoin(base_url, href)urljoin 是标准库函数比自己拼字符串安全得多。它能够处理好目录层级、顶层域名拼接等边界情况。别小看这一步很多人抓了一堆数据最后发现 HTTP 请求全部 404往往就是 href 本身是完整的 but 被当成相对路径拼错了或者相反。为了更直观地理解页面间关系我当时手工画了一遍书序结构很清晰首页 → 书籍详情页 → 章节目录页 → 正文章节页。每个层级对应一个函数层层推进不写面条代码。3. 爬虫核心逻辑从目录到章节3.1 详情页与目录页采集采集流程的第一步是确定书籍详情的入口 URL然后用 session 发起请求顺手定制一个可以识别的 User-Agent。import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } session requests.Session() session.headers.update(HEADERS) res session.get(detail_url, timeout10) soup BeautifulSoup(res.text, lxml)这里我故意用 Session 而不是直接 requests.get()目的就是一个后续所有并发请求共享同一个连接池同时保留 Cookie。有些小说站会给首次访问的请求种下访问令牌第二次请求需要带着它。用 Session 可以起到类似浏览器的连续性效果比你每次都重新握手要稳。拿到详情页后下一步是定位目录页。如果详情页结构稳定可以直接用soup.find(a, string全部章节)这类写法。要是列表页直接就在详情页里就用soup.select(div#list dd a)把章节一次捞出来。我这里遇到的需要二次跳转所以先解析“查看完整目录”按钮catalog_link soup.find(a, hrefre.compile(r/read/)) catalog_url urljoin(detail_url, catalog_link[href])然后把目录页重新抓一遍提到所有章节chapter_list [] catalog_res session.get(catalog_url, timeout10) catalog_soup BeautifulSoup(catalog_res.text, lxml) for dd in catalog_soup.select(div#list dl dd a): title dd.get_text(stripTrue) href urljoin(catalog_url, dd[href]) chapter_list.append({title: title, url: href})注意我保存的是一个字典列表而不是纯链接列表。这个习惯很好因为后续写文件要标题请求要链接分两个列表反而容易错位。章节顺序也是一个需要关注的点。有的小说站目录页会区分“正文卷”“番外卷”“VIP 卷”直接用 select 全捞可能出现顺序跳跃甚至后出的番外穿插在正文中间。所以我在拿到 chapter_list 后补了一个排序检查确认第一个章节标题里包含“第一章”或“第1章”验证顺序正确再开始正文抓取。顺序一旦反了整本小说文件根本没救。3.2 章节正文页抓取与正文抽取拿到章节 URL 后就可以逐个请求正文页。先写一个单章节抓取函数再循环调用这样出错时方便调试def fetch_chapter_text(chapter_url): resp session.get(chapter_url, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) content_div soup.find(div, idcontent) if content_div is None: return None # 清理不必要的子节点 for tag in content_div.find_all([script, style]): tag.decompose() paragraphs content_div.get_text(\n, stripTrue) return paragraphs这里我做了三个关键动作每一个都有对应的实战理由第一指定resp.encoding utf-8。虽然很多站点声明了 UTF-8但实际响应头里的 charset 可能是错的Requests 判断编码靠响应头头不对就乱码。显式声明的目的就是绕过这个坑。不过这个值不是死的如果你发现抓出来的文本是“乱码方块”那多半是 GBK 编码改成resp.encoding gbk实测一下就知道。第二decompose()清掉正文容器里的 script 和 style。小说网站的正文区经常会被插入各种统计脚本和样式标签不清理的话记事本打开后能看到一堆 JavaScript 源代码非常影响阅读体验。第三get_text(\n, stripTrue)是把所有文本按换行拼接并去除每行首尾空白。这是处理那种“整块正文挤在一个 div”类型的站点最有效的方式。一个自然段间的br会被自动映射成换行正文最终呈现出来就是一排排清晰的段落。3.3 章节标题与正文整合正文抽取完整后和标题组织成一条记录data { title: chapter_title, url: chapter_url, content: chapter_text }然后按照目录顺序逐条追加写入最终文本文件。写入策略我用了两种结合内存中用一个列表缓冲 50 章再统一写盘同时每写 20 章打印一次进度日志。这样做的好处很明显减少磁盘 I/O 次数提高抓取速度崩溃后日志至少能告诉你跑到哪一章重启时能断点续传。写入格式我做了严格的规范化便于记事本阅读chapter_content f{title}\n\n{content}\n\n标题空行、正文空行章节之间分隔明显。整本小说文件打开后非常干净没有多余的空格、看不见的字符、也不会有浏览器残留的全角空格。这是交付质量的核心观察点。4. 编码处理与记事本兼容性4.1 文件保存编码的取舍你说“以记事本形式保存”核心就是 .txt 文件。编码选择直接决定了记事本打开是否乱码。在 Windows 现代系统里记事本默认能正确识别 UTF-8 带 BOM 和无 BOM 的文件但为了最大兼容旧环境和老版本系统我这次选择了utf-8-sig编码写入。这样会在文件开头自动加入一个 BOM 头\ufeffWindows 记事本看到这个标记就会立刻按 UTF-8 解码不会误判成 ANSI 或乱码。写入代码如下with open(novel.txt, w, encodingutf-8-sig) as f: f.write(.join(all_chapters))如果你希望文件体积更小也可用纯utf-8。但utf-8-sig相比只大了 3 个字节换来的是跨平台打开无误的保障怎么看都划算。4.2 保持电脑文件系统友好的文件名小说文件名也应处理否则会有坑。比如书名里含:、?、*这些字符时Windows 会拒绝创建。我写了统一的 sanitize 函数import re def safe_filename(name): return re.sub(r[\\/:*?|], _, name)这个函数把非法字符统一替换成下划线避免保存阶段报[Errno 22] Invalid argument。这类错误一报很多人第一反应是代码语法问题实际上是文件名惹的祸。4.3 正文清洗细节清洗是整个项目里最能体现质量差异的部分。你如果不做清洗抓完的小说打开全是问题有的段落首尾带着全角空格“ ”有的正文里还嵌着“推荐阅读”, 有的核心段落被广告文字切断。我当时的做法分两步。第一步去掉明显的 navigate 块和广告区块用.decompose()删除对应标签。第二步处理文本内部的多余空白import re def clean_text(text): text re.sub(r[ \t\u3000], , text) text re.sub(r\n{3,}, \n\n, text) return text.strip()第一行正则把全角空格、制表符、连续半角空格全部折叠成单个半角空格第二行把三个以上连续换行压缩成两段分隔的标准格式。这样整本读下来页面里那些位移空格和排版垃圾就没有了。5. 反爬与稳定性常见问题全记录5.1 请求被拦截、IP 被限制怎么处理小说网站虽然技术门槛不高但很多都有反爬策略具体表现为请求频率限制比如 1 秒内超过 3 次就开始返回验证码缺少 User-Agent 的请求直接 403访问过快时跳转到一个“人机验证”页面某些章节链接有时效性隔几小时就失效。我的处理策略是基础不玩骚操作把合规性放在第一位。请求间隔固定控制在 1.2 秒到 2 秒之间不做恶意高频并发User-Agent 直接声明为浏览器标准标识遇到 403 先退避 3 秒并重试一次连续失败 5 次就暂停程序等待手动干预。做个单本采集上万请求都会造成很大负担了真的没有必须为了几十秒的时间去再压站点资源。稳定拿完一本才是目标的重点。我在这台主机跑完一整本 300 章的小说总耗时大约 15 分钟没有触发任何限制。5.2 编码识别失败的排查方法乱码问题永远排在小说采集疑难杂症第一名。排查思路顺路给一个表格方便你按图索骥现象可能原因解决方案中文全部变成“锟斤拷”Requests 按 ISO-8859-1 解码了 UTF-8 字节流显式设置resp.encoding utf-8中文显示为“閿熷崵”解码用了 GBK但实际是 UTF-8设置resp.encoding utf-8-sig英文字符正常中文乱码页面真实编码是 GB2312/GBK设置resp.encoding gbk打开 txt 后首行有小黑点这是 BOM 头被当字符显示清洗文件开头或改用utf-8打开 txt 后整体乱码记事本默认 ANSI 解码了 UTF-8 文件用utf-8-sig写入强制识别核心心法在resp.text打印出来那一刻就判断编码是否正常发现乱码立刻手动指定编码重试这比写完几百章再统一转换要省事得多。5.3 目录页解析失败的情况小说站长年累月更新目录页结构也会变。比如原本div idlist变成div classchapter-list。这种情况下选择器选不到就输出空列表。应对方案是提前加断言assert chapter_list, 章节列表为空检查选择器断言不通过程序立刻退出不会傻乎乎跑到最后生成一个空 txt 文件给你。这种面向防御的编程习惯能帮你省掉大量 debug 时间。还有一类坑是目录页存在分页一本书章节太多分成 3 页显示只抓第一页会漏掉后 2/3 内容。我的排查方式是抓完后对比目录页上标注的“总章节数”如果差得多就去看看是否有分页链接。很多站点会有一个“下一页”按钮它的 href 同样要用 urljoin 拼接不能直接拼根域名。5.4 章节正文缺失或为空正文缺失一般有几种情况本章为免费试读章节需要登录才能看完整本章内容被拆分到了多个页面请求返回了默认页面或错误页。遇到正文缺失我先打印出该章节的content_div用 HTML 文档结构比对是否和正常章节一致。如果确认当前站点无异常大概率是登录保护。对这种单本抓取的场景我的建议是直接放弃该书选择另一个无需登录就能看到全部正文的站点。这不是认怂而是做工程需要计算成本收益。为了一本书去逆向登录流程太考验隐蔽性而且后续风控很难对付。5.5 抓取中断与断点续传长任务跑一半网络断掉、电脑休眠、程序意外退出太常见了。我的项目采用两种简单可靠的断点方式方式一每章保存时顺手写入进度文件progress.txt记录最后一个成功章节的序号。重启后读取该序号从下一章继续。方式二边抓边写而不是全部抓完再写。代码从内存缓冲列表里抽出一段就写盘一次即使中间崩了已经写完的章节不会丢。配合进度文件整个流程可以说相当皮实。6. 最终成果与经验总结6.1 生成文件质量检查跑完整个流程我习惯用以下检查清单确认成果可用文件是 .txt可以用记事本直接打开无乱码开头是书名接着是“第一章”等清晰标题每个章节标题独立成行正文段落间有空行全文搜索“http://”或“https://”无结果证明没有网页链接残留搜索“推荐阅读”“广告”等词正文区干净。如果全部通过这个项目就算真正交付了。6.2 我在实操过程中发现的小技巧整个项目做完我最想强调的一点是不要在写完爬虫的第一天就开全量抓取。先抓 3 章看看文件文本是不是你预期的那样的清晰格式再抓 20 章看看中间有没有报错确认无误后再放手跑全本。章节多的小说动辄几十万次字符处理一旦文本清洗规则有瑕疵最后就要重新来过。还有一个容易被忽略的保存前把书名和作者写成一行注释类的元信息放在文件首部阅读时非常友好。比如第一行“书名: xxx”第二行“作者: xxx”之后空一行再进正文。记事本打开时一眼就知道这是什么而不是点开一堆纯内容猜测书名。在小说的采集逻辑里“爬取”只是最浅的一层真正决定质量的往往在编码处理、文本清洗、稳定性保障这些场外动作上。你把这些细节做到位整个交付物才会有“可以直接当电子书读”的体验而不是“一坨能打开的乱码文本”。最后说一个经验实际调试时建议从几十章的小体量作品入手。别一上来就选千章巨著流程已经跑通了自然会顺利。我这次一开始就踩了目录分页的坑选了一本几百章的书原以为三分钟抓完结果页面结构识别失败直接失败。换成小体量后十分钟就成功产出了第一份完整 txt。从那以后我先固定推荐一本几十章的作品做小规模验证再换心头好跑全量。省下的时间远比想象中多。如果你也正在做类似的项目遇到问题可以把报错信息、目标页面的一小段 HTML、以及你用的选择器贴出来一起看。顶多加个朋友交流。
阅读完成 · 觉得有帮助?
咨询建站