3个坑搞定全本小说下载器,新手避坑指南
看了一堆教程还是不会写项目?别慌,这太正常了。 很多新手卡在“代码能跑”和“项目能用”之间,差的就是那层窗户纸。 今天咱们不聊虚的,直接上手写一个全本小说下载器。 这是新手避坑的绝佳练手项目,逻辑简单,涉及网络请求、文件处理、异常捕获。 写完这个,你对 Python 的实战理解会提升一大截。 很多兄弟在掘金技术社区分享过类似经验,说写爬虫是入行第一关。 没错,但很多人死在了“反爬”和“编码”这两个坑里。 这篇文章,我就带你把这些坑全填了。 咱们目标明确:写出一个稳定、可运行、能处理常见报错的下载器。 不用复杂的 Selenium,纯 Python 标准库 + requests 就够了。 准备好你的编辑器,咱们开始。
概念速懂:下载器到底在干嘛
很多人以为下载器就是“下载文件”,其实没那么简单。 一个合格的下载器,核心逻辑分三步:解析、获取、保存。
- 解析:从网页 HTML 里找出每一章的链接。
- 获取:根据链接去请求服务器,拿到正文内容。
- 保存:把内容格式化,写入本地 TXT 或 HTML 文件。
听起来很简单,对吧? 但坑就在细节里。 比如,网页编码是 UTF-8 还是 GBK? 比如,请求太快被封 IP 怎么办? 比如,某章加载失败,程序是崩溃还是跳过? 这些问题,教程里往往一笔带过,但实战中全是泪。 我们今天要解决的,就是这些“隐形杀手”。
先理清技术栈:
- requests:发送 HTTP 请求,比 urllib 好用太多。
- BeautifulSoup4:解析 HTML,提取数据的神器。
- time:控制请求频率,避免被封。
- os/pathlib:处理文件路径,跨平台兼容。
为什么不用 Selenium? 因为对于纯文本小说,Selenium 太重了。 加载浏览器、渲染 JS,耗时且不稳定。 除非网站是纯 JS 渲染(如 Vue/React SPA),否则 requests 足矣。 90% 的小说网站,都是服务端渲染 HTML,requests 完全够用。 这也是新手避坑的关键:不要过度设计。 能用简单方案解决的,别上重型工具。
环境准备:工欲善其事
别急着写代码,环境没配好,后面全是坑。 第一步,安装依赖。 打开终端(Mac/Linux)或 CMD/PowerShell(Windows)。 执行以下命令:
pip install requests beautifulsoup4
如果你用的是国内网络,pip 可能很慢。 加上清华镜像源,速度起飞:
pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple
第二步,确认 Python 版本。
建议 Python 3.8+。
Python 2 已经退役,别用。
在终端输入 python --version 检查。
第三步,创建项目结构。
不要把所有代码扔在一个文件里。
养成好习惯,建个文件夹 novel_downloader。
里面放 main.py 和 config.py(可选)。
咱们今天为了简单,先写一个 main.py。
后续进阶再拆分模块。
还有一个关键点:User-Agent。
默认 requests 的 UA 是 python-requests/2.x.x。
很多网站会直接拦截这个 UA,返回 403 或空页面。
必须伪装成浏览器。
比如 Chrome:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
这个 UA 字符串,建议存成常量,方便修改。
核心语法:逐行拆解关键代码
现在咱们写核心逻辑。 我分三段讲解,每段都是可运行的片段。 先看最基础的:请求网页并解析。
import requests
from bs4 import BeautifulSoup# 1. 设置请求头,伪装成浏览器
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"
}# 2. 请求目录页
url = "https://example-novel.com/catalog/12345"
try:response = requests.get(url, headers=headers, timeout=10)response.raise_for_status() # 关键:检查 HTTP 状态码# 自动检测编码,防止乱码response.encoding = response.apparent_encodinghtml_content = response.text
except requests.exceptions.RequestException as e:print(f"请求失败: {e}")exit()# 3. 解析 HTML
soup = BeautifulSoup(html_content, "html.parser")
# 假设章节列表在 <div class="chapter-list"> 下的 <a> 标签
chapter_links = soup.select("div.chapter-list a")print(f"找到 {len(chapter_links)} 个章节")
for link in chapter_links:title = link.get_text(strip=True)href = link.get("href")# 处理相对路径if href.startswith("/"):full_url = "https://example-novel.com" + hrefelse:full_url = hrefprint(f"章节: {title} -> {full_url}")
重点解析:
response.raise_for_status():这是新手最爱漏的。如果服务器返回 404 或 500,response.text可能是错误页面 HTML,而不是小说内容。加上这个,错误会直接抛出异常,方便你捕获。response.encoding = response.apparent_encoding:requests 默认猜的编码可能不对,尤其是中文网站。用apparent_encoding让 chardet 库自动检测,能解决 80% 的乱码问题。BeautifulSoup的select:用 CSS 选择器比find_all更直观,写起来更快。
接下来,看单章内容提取。 假设章节页结构是:
<div id="content"><p>第一章内容...</p><p>第二章内容...</p>
</div>
def fetch_chapter_content(url, headers):try:resp = requests.get(url, headers=headers, timeout=10)resp.raise_for_status()resp.encoding = resp.apparent_encodingsoup = BeautifulSoup(resp.text, "html.parser")# 找到内容区域content_div = soup.find("div", id="content")if not content_div:print("未找到内容区域,可能结构变更")return ""# 提取所有 <p> 标签文本paragraphs = content_div.find_all("p")text = "\n\n".join([p.get_text(strip=True) for p in paragraphs])return textexcept requests.exceptions.RequestException as e:print(f"获取章节失败 {url}: {e}")return ""
避坑点:
find_all("p"):不要直接get_text()整个 div,那样会包含换行符和空格,导致 TXT 文件杂乱。提取<p>再拼接,格式更干净。strip=True:去掉首尾空格,保持文本整洁。
完整代码示例:组装成可用工具
现在,把前面的片段拼起来,加上文件保存和异常处理。 这是一个完整的、可运行的脚本。 你可以直接复制,修改 URL 测试。
import requests
from bs4 import BeautifulSoup
import time
import os# 配置区
BASE_URL = "https://example-novel.com"
CATALOG_URL = f"{BASE_URL}/catalog/12345"
SAVE_DIR = "./novel_output"
DELAY = 2 # 每次请求间隔秒数,防封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"
}def init_dir():if not os.path.exists(SAVE_DIR):os.makedirs(SAVE_DIR)print(f"创建目录: {SAVE_DIR}")def get_chapter_list(url):"""获取所有章节链接和标题"""resp = requests.get(url, headers=headers, timeout=10)resp.raise_for_status()resp.encoding = resp.apparent_encodingsoup = BeautifulSoup(resp.text, "html.parser")# 根据实际网站结构修改选择器links = soup.select("div.chapter-list a")chapters = []for a in links:title = a.get_text(strip=True)href = a.get("href")if href.startswith("/"):href = BASE_URL + hrefelif not href.startswith("http"):href = BASE_URL + "/" + hrefchapters.append((title, href))return chaptersdef save_chapter(title, content, index):"""保存单章到文件"""filename = f"{index:03d}_{title.replace('/', '_').replace('\\', '_')}.txt"filepath = os.path.join(SAVE_DIR, filename)with open(filepath, "w", encoding="utf-8") as f:f.write(f"章节: {title}\n\n")f.write(content)print(f"已保存: {filename}")def main():init_dir()print("正在获取目录...")chapters = get_chapter_list(CATALOG_URL)print(f"共 {len(chapters)} 章")for i, (title, url) in enumerate(chapters, 1):print(f"下载第 {i}/{len(chapters)} 章: {title}")content = fetch_chapter_content(url, headers)# 如果内容为空,可能是 VIP 章节或解析失败if not content:print(f"警告: 第 {i} 章内容为空,跳过")continuesave_chapter(title, content, i)# 延迟请求,避免被封time.sleep(DELAY)# 随机延迟,更拟人化# time.sleep(random.uniform(1, 3))print("下载完成!")# 调用前面定义的 fetch_chapter_content
def fetch_chapter_content(url, headers):try:resp = requests.get(url, headers=headers, timeout=10)resp.raise_for_status()resp.encoding = resp.apparent_encodingsoup = BeautifulSoup(resp.text, "html.parser")content_div = soup.find("div", id="content")if not content_div:return ""paragraphs = content_div.find_all("p")return "\n\n".join([p.get_text(strip=True) for p in paragraphs])except Exception as e:print(f"错误: {e}")return ""if __name__ == "__main__":main()
运行前检查清单:
- 修改
CATALOG_URL为你实际测试的小说目录页。 - 检查
get_chapter_list中的 CSS 选择器div.chapter-list a是否匹配目标网站。 - 检查
fetch_chapter_content中的id="content"是否匹配。 DELAY建议设为 2-5 秒,太快容易封 IP。
常见报错与解决策略
新手写爬虫,报错是家常便饭。 这里列出三个最高频的问题,以及对策。
1. 乱码:中文变成 ??? 或 �
- 原因:编码不匹配。服务器返回 GBK,你按 UTF-8 读。
- 对策:始终使用
response.encoding = response.apparent_encoding。 - 备选:如果
apparent_encoding不准,手动指定response.encoding = 'gbk'。 - 保存文件时:务必指定
encoding="utf-8",否则 Windows 记事本打开可能还是乱码。
2. 403 Forbidden 或空页面
- 原因:被反爬拦截。UA 不对,或者请求频率太高。
- 对策:
- 更换 UA,尝试 Firefox 或 Safari 的 UA。
- 增加
DELAY,比如从 2 秒增加到 5 秒。 - 添加 Cookie。有些网站需要登录或首次访问后的 Cookie。
- 代码示例:
cookies = {"session_id": "xxx", "user_id": "123"} requests.get(url, headers=headers, cookies=cookies)
3. 解析不到内容:章节列表为空或正文为空
- 原因:网站结构变了,或者内容是 JS 动态加载。
- 对策:
- 打开浏览器 F12,查看 Network 标签,看真实请求的 URL 和 HTML 结构。
- 如果 HTML 里只有
<script>没有正文,说明是 JS 渲染。 - 对策 A:找 API 接口。很多网站有 JSON API,直接请求 API 比解析 HTML 更稳。
- 对策 B:用 Selenium。但这会增加复杂度,慎用。
- 对策 C:检查 CSS 选择器。用 DevTools 的“检查元素”确认标签名和 class。
新手避坑心法:
- 不要相信文档里的示例 URL 永远有效。网站改版是常态。
- 不要一次性下载整本书。先测前 3 章,确认无误再全量跑。
- 日志要详细。打印每一章的标题和状态,方便定位问题。
小结与进阶方向
到这里,一个基础的全本小说下载器就写完了。 回顾一下,我们解决了:
- 环境配置与依赖安装。
- 请求头伪装与编码处理。
- HTML 解析与数据提取。
- 文件保存与异常捕获。
- 频率控制与反爬基础应对。
这个项目的价值,不在于代码本身,而在于流程思维。 从输入 URL 到输出 TXT,中间每一步都可能出错。 你能不能预判错误?能不能优雅处理?这就是工程师思维。
进阶方向:
- 多线程下载:用
concurrent.futures.ThreadPoolExecutor并发请求,速度提升 3-5 倍。注意控制并发数,别把 IP 搞挂了。 - 代理池:接入代理 IP 服务,轮换 IP,应对更严格的反爬。
- 数据清洗:去掉广告、推广语、空行。用正则表达式清洗文本。
- 格式转换:转成 EPUB 或 MOBI,方便 Kindle 阅读。可以用
ebook-convert工具。 - GUI 界面:用
tkinter或PyQt做个简单界面,方便非技术用户使用。
关于法律与道德: 爬虫技术本身中性。 但请遵守目标网站的服务条款。 不要爬取受版权保护的内容用于商业分发。 个人学习、备份已购书籍,通常风险较低,但仍需谨慎。 尊重原创,尊重开发者劳动。
互动环节: 你公司项目里是怎么处理这类批量数据抓取任务的? 是自建集群还是用现成工具? 遇到过哪些奇葩的反爬策略? 欢迎在评论区分享你的实战经验,一起避坑。