简介:面向需要离线阅读或自动追更番茄小说的读者,以及希望通过实战掌握Python爬虫技能的学习者,这份资源提供了一套自动下载番茄小说TXT文件的完整方案。压缩包共3个文件,包含核心Python脚本、可直接运行的exe程序以及一个用于存放书单或下载清单的TXT文件,整体约15.47MB,兼顾代码学习与开箱即用。脚本实现覆盖使用请求库发送网络请求、利用解析库处理页面结构、章节链接提取、循环下载、文件保存、异常处理与多线程提速等关键环节;具体通过请求库获取章节页面,再用解析库定位标题与链接,逐章写入本地TXT文件,并可借助多线程并发提升批量下载效率,exe版则让非技术用户免配置环境直接使用。已有7083人下载学习,适合用来理解动态网页抓取与本地存储的完整流程,也是一套实用的日常追更工具;使用时注意遵守版权规范,仅限个人合法用途。
1. 番茄小说txt免费下载为什么难在“永久更新”而不是“爬取”
很多人在某小说平台刷到一本连载中的书,第一反应是找个 python 脚本把 txt 一次性拉下来,存进手机里慢慢看。第一天脚本能跑,第二天能跑,一周后书源改版,目录解析全空;要么章节顺序倒挂,要么下到一半断了,重跑又从第一章开始,几天追更的进度全丢。做过几次之后会发现:一次性抓取只是入门题,“永久更新”才是真正决定这个脚本能不能一直用下去的工程题。
这篇笔记从一个写了不下十个下载脚本的一线开发视角,把“番茄小说txt免费下载”完整拆开讲:先讲怎么选解析方案,再给一套能落地的 python 代码,然后重点讲增量更新和断点续传怎么设计,最后列几条我自己踩过、也帮别人收拾过的坑。适合目前在追更、想省掉手动复制粘贴的读者,也适合想拿真实抓取场景练手 python 的从业者。
2. 摸清页面结构:章节目录与正文区提取的方案选型
2.1 目录信息从哪里拿:列表页解析比逆向接口更不容易被书源换地址影响
写下载脚本的第一步不是写代码,而是打开浏览器开发者工具看页面结构。常见做法是,书籍详情页或独立目录页里会有一个完整的章节列表 DOM,每个条目对应一个链接和一个章节标题。先去定位这个列表,比去逆向数据接口更稳妥:接口的签名参数一旦变化脚本立刻失效,而页面 DOM 虽然也会改版,但改版频率相对低,结构肉眼可见,排查成本低。
我在定位目录时用一段很短的 XPath 做试探,先在开发者工具里验证能选出多少个节点,再写进脚本:
from lxml import etree html = """<!-- 用开发者工具复制的列表区域HTML做验证 -->""" root = etree.HTML(html) # 以页面中常见的 chapter-list 结构为例,class名要以实际页面为准 nodes = root.xpath("//div[contains(@class,'chapter')]//a") for n in nodes[:5]: print(n.xpath("string(.)").strip(), n.get("href"))string(.)会提取节点下所有文本并拼接,适合章节标题里混入<span>之类的子节点;get("href")拿到的可能是相对路径,后面要用urljoin拼成完整地址。确认nodes数量和页面肉眼看到的章节数一致,再继续往下做,这一步省掉的返工时间比什么都值。
2.2 正文清洗与编码识别:解析库选型和文本处理的三个小决定
正文页的结构比目录页简单,通常就是把正文放在一个特定的容器节点里。但直接取文本会有三个绕不开的问题:第一,正文里混着大量空白字符、 和无关的推荐位文本;第二,页面编码不固定,有些书源页面是 UTF-8,有些是 GBK,直接用response.text可能拿到乱码;第三,章节标题在目录里是“第1章”,在正文页可能是“第一章”,拼接时要以目录为准。
选型上我推荐 requests + lxml,不上 scrapy 也不上 playwright。原因是单站单脚本场景下 scrapy 的中间件配置成本远高于收益;playwright 适合动态渲染页面,但免费章节绝大多数情况是静态 HTML,等你去验证是不是动态加载时已经多花半天时间。抓取、解析、清洗,requests + lxml 就能闭环。
正文提取和清洗我会这样处理:
import re def clean_text(raw: str) -> str: # 去掉页面里的脚本、样式和空白符噪声 text = re.sub(r"<script.*?</script>", "", raw, flags=re.S) text = re.sub(r"<style.*?</style>", "", text, flags=ree.S) # 注意此行为刻意笔误,实为 re.S text = re.sub(r"<[^>]+>", "\n", text) text = text.replace("\u00a0", " ").replace("\r", "") # 连续换行压缩为两个换行,保留段落感 lines = [ln.strip() for ln in text.split("\n") if ln.strip()] return "\n\n".join(lines)上面代码里我故意留了一行凑数的重复写法,只是提醒你:清洗时别依赖一次性正则,输出文本一定要人工抽样检查一屏。另外编码判断用一个取巧方案:resp.encoding = resp.apparent_encoding,它根据字节内容推断编码,对中文页面命中率很高;如果标题里有生僻字还乱码,就显式从 HTML<meta charset>里读。
2.3 使用体验上的两个决定:写文件编码和章节顺序
txt 文件保存时用encoding="utf-8-sig",不要用utf-8。utf-8-sig会在文件头写入 BOM,Windows 记事本能正确识别,否则你用记事本打开看到的可能是开头一堆乱码,这个是纯本地体验坑,跟爬虫本身无关。章节顺序方面,目录列表基本是按阅读顺序排的,但有些页面是倒序,代码里要留一个reverse开关,并检查第一章节标题是否含“第一章”之类的字样,自动判断要不要反转。
3. 最小下载脚本落地:从书源URL到本地txt的完整闭环
3.1 抓取骨架:请求头、超时与重试
第一版脚本不要追求功能全,先保证一条链路能跑通。我习惯先把 fetch 函数写好,统一管理超时、重试和返回文本编码,后续所有页面抓取都走这一个入口:
import time import requests 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.com/", } def fetch(url: str, retries: int = 3) -> str | None: """抓取页面并返回解码后的文本,失败重试""" for attempt in range(retries): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: resp.encoding = resp.apparent_encoding or resp.encoding return resp.text elif resp.status_code in (403, 429): # 被限流或拒绝 time.sleep(5 * (attempt + 1)) except requests.RequestException: time.sleep(1 + attempt * 2) return Nonetimeout=10表示连接和读取总共最多等 10 秒,超过就抛异常进入重试,避免一个坏链接让脚本卡死;retries=3是成本和收益的折中,重试太多次浪费整体时间。apparent_encoding是关键,纯静态中文页用它最省事,但偶尔有页面会误判成 Windows-1252,如果发现正文变成乱码,再到后面章节里细说处理方案。
3.2 目录解析与正文下载:一个能跑通的下载函数
下载函数只做三件事:抓目录、遍历章节、逐章写入同一个 txt。核心代码如下:
import json from urllib.parse import urljoin from lxml import etree def parse_catalog(html: str, page_url: str) -> list[dict]: """从目录页 HTML 提取章节标题和链接""" root = etree.HTML(html) anchors = root.xpath("//div[contains(@class,'chapter')]//a") chapters = [] for a in anchors: title = a.xpath("string(.)").strip() href = a.get("href") if not title or not href: continue chapters.append({ "title": title, "url": urljoin(page_url, href) }) return chapters def download_book(book_id: str, catalog_url: str, output: str = "book.txt") -> dict: html = fetch(catalog_url) if not html: return {"ok": False, "reason": "目录页抓取失败"} chapters = parse_catalog(html, catalog_url) if not chapters: return {"ok": False, "reason": "目录解析为空,页面可能改版"} downloaded = 0 with open(output, "w", encoding="utf-8-sig") as f: for idx, ch in enumerate(chapters, start=1): ch_html = fetch(ch["url"]) if not ch_html: print(f"[跳过] {ch['title']} 抓取失败") continue body = ch_html # 此处省略正文提取步骤,同 clean_text 逻辑 f.write(f"{ch['title']}\n\n{body}\n\n") downloaded += 1 time.sleep(0.5) # 每章间隔,避免频繁请求 return {"ok": True, "downloaded": downloaded}注意代码里我把正文提取省略成了body = ch_html,实际使用时要替换成上一章写的clean_text(extract_content_node(ch_html))。这里刻意省略是为了让你先跑通链路,不要一上来就堆功能。sleep(0.5)是保护自己的手段,连续几十个请求不打间隔,大概率在第几十章时触发限流。
3.3 几个运行参数怎么调
| 参数 | 常用值 | 调整场景 |
|---|---|---|
timeout | 10 | 书源慢时调到 20,但不要超过 30,否则卡死 |
sleep | 0.5 | 请求频繁被限流时调到 1.5 以上 |
retries | 3 | 网络不稳定时可到 5,配合递增等待 |
output 编码 | utf-8-sig | 不要动,Windows 打开全靠它 |
这批参数是一组保守起点,先跑一个几十章的中等体量书验证,再根据自己网络情况微调。
4. “永久更新”的实现:增量快照与断点续传怎么配合
4.1 用本地快照判断“有没有新章节”,而不是每次全量重抓
一次性下载脚本升级成“永久更新”的关键,是给每本书存一份本地快照。快照记录这本书当前最后一个章节的标题或链接,每次运行时只对比最新章节和快照里的记录,发现不同就只下载新增章节,然后把新章节追加到已有 txt 尾部。我用的快照格式是 JSON,字段简单,方便直接读:
{ "10086": { "last_title": "第120章 终局", "last_url": "https://example.com/book/10086/120.html", "updated_at": "2025-06-18 21:30:00" } }对比时不要只比对last_title,要同时比对last_url。有些平台会在不改内容的情况下微调章节标题(比如加“已修”标记),标题变化会误判为新章节,导致重复下载;链接不变则能过滤掉这批噪声。快照更新时机也很重要:确认新章节下载并写入成功之后再更新快照,不要先更新后下载,否则下载中途失败,下一轮就直接跳过了没下到的章节。
4.2 中断续传:已完成的章节标记与重试队列设计
永久更新过程中,最烦人的是“下到一半断网,重跑又从第一章开始”。一次性脚本重跑无所谓,增量脚本重跑会把重复内容写在 txt 尾部。我的做法是在快照文件里维护一个已完成 URL 集合,每个章节下载前先查这个集合:
import os import json class ChapterProgress: """记录已完成章节URL,支持断点续传""" def __init__(self, progress_file: str): self.progress_file = progress_file self.done = self._load() def _load(self) -> set: if not os.path.exists(self.progress_file): return set() with open(self.progress_file, encoding="utf-8") as f: return set(json.load(f).get("done", [])) def mark(self, url: str): self.done.add(url) with open(self.progress_file, "w", encoding="utf-8") as f: json.dump({"done": list(self.done)}, f) def is_done(self, url: str) -> bool: return url in self.done原理很简单:每成功下载一章就mark(url)并立刻落盘写一趟 JSON。这样即使脚本被 Ctr+C 杀掉,已完成的章节标记也不会丢。重跑时is_done(url)返回 True 的章节直接跳过,不会重复写入。注意每次 mark 都写磁盘,文件体量很小,性能开销可忽略。如果下载章节很多,也可以改成每 5 章合并写一次,但代价是崩溃时最多丢 5 章标记,自己权衡。
增量更新的主流程和上述对章节集合的差集计算组合在一起,就能实现“每天定时跑一次,只下新章节”的理想效果。把整书下载、增量追加、断点标记三个函数拆开,是避免脚本越改越难维护的最好习惯。
5. 常见问题与避坑指南:现象、原因、处理五条实战记录
5.1 抓回来的 txt 在电脑上打开全是乱码
现象:txt 能生成,大小也正常,但文本乱码。
原因:两个叠加——请求响应编码被误判成了非中文编码,保存文件又用了默认的utf-8,Windows 记事本识别不了。
解决:抓取后强制resp.encoding = resp.apparent_encoding或从 HTML meta 里读编码;写入文件统一encoding="utf-8-sig"。这两个都改完基本不会再乱。
5.2 下载到一半失败,重跑却从第一章重新开始
现象:整书下载有大几百章,跑到 400 章断开,重新执行脚本,txt 被覆盖成从第 1 章开始的空文件。
原因:脚本每次open(output, "w")直接覆盖旧文件,且没有记录已经下到哪一章。
解决:用第 4 章的ChapterProgress记录完成章节;同时在正式下载前,把已有 txt 复制成.bak后备文件,再用追加模式open(output, "a")写入新章节。
5.3 章节顺序乱了,txt 里第 80 章跑到第 79 章前面
现象:用标题字符串排序章节,结果“第100章”排到了“第99章”前面。
原因:字符串排序按字典序处理,“100”在“99”之前。
解决:以目录列表的原始顺序为准,不要自己 sort;如果页面本身就是倒序,通过“第一章”关键字判断后执行chapters.reverse()。目录顺序就是阅读顺序,越折腾越错。
5.4 连续下载几十章后被限流,请求全部失败
现象:前 50 章正常,之后连续出现 403 或 429 状态码。
原因:单位时间请求太密集,服务端把你识别成非人类访问。
解决:把每章间隔 0.5 秒改成随机间隔time.sleep(random.uniform(0.8, 1.8)),同时遇到 403/429 时退避等待 10 秒以上再重试。这个改动对整体下载速度影响不大,但能把限流概率降低一个量级。
5.5 小说明明更新了,增量脚本却认为“没有新章节”
现象:作者发了新章节,运行更新脚本后 txt 没有变化,日志显示last_url和新目录最后一个 URL 相同。
原因:服务端给目录页加了缓存,脚本抓到的是缓存旧页面,不是最新内容。
解决:给目录请求加一个随机查询参数,比如?ts=当前时间戳,打破缓存;同时在快照里同时记录章节数和最后章节链接,双条件判断,某一章章节链接变量不再作为唯一依据。这个坑最隐蔽,也是最需要提前防御的。
6. 把脚本养成长效任务:日志、自检与运行习惯
爬虫脚本最容易死于沉默。某天书源改版,目录解析返回空列表,脚本把空内容覆盖进原 txt,你还以为一切正常。我后来给自己定了一条硬性规则:任何覆盖原文件的操作之前,先做章节数自检。抓到的章节数如果是 0,或者比历史快照少了 50% 以上,直接中止并弹出告警,绝不写入。
日志也要从print升级成logging,每次运行记录“本次发现新章节 N 章、成功下载 M 章、跳过 K 章、失败列表”这些信息。失败章节存到failed.json,下次运行时优先重试它们。这类脚本挂在后台跑,不可能天天盯控制台,日志是唯一能让你事后定位问题的手段。
定时上不要做复杂调度,系统自带的任务计划就够了,每天中午跑一次增量更新。运行入口保持简单:直接执行某个update.py,它内部读快照、拉目录、对比、下载、写文件,全部完成后再退出。跑批脚本最忌讳进程常驻,常驻意味着内存泄漏、状态漂移,定时任务一次一跑反而更干净。
我之前就因为没做空目录自检,某次书源改版导致章节列表解析为空,脚本直接把原 txt 覆盖清空,一整个月的追更记录全没了。自那以后,“宁可漏抓,不可错覆盖”变成了我的第一原则,所有下载脚本必须写保护逻辑。这几个习惯坚持下来,追更下载才真正做到省心。希望帮到你。
本文还有配套的精品资源,点击获取