Trae+Python小说爬虫3:单页章节链接+多页小说章节的断点续传实战

发布时间:2026/9/28 9:40:51
Trae+Python小说爬虫3:单页章节链接+多页小说章节的断点续传实战 1. 混合章节结构为什么总在第三页断掉小说站点里最容易被低估的一类结构是「单页章节链接 多页小说章节」混排目录页给出的每个章节链接只指向第 1 页真正的正文被拆成xxx.html、xxx_2.html、xxx_3.html……直到某一页返回空内容或 404。用 Trae 写 Python 爬虫时如果只按目录链接抓一次你会得到一堆「半截章节」——开头正常结尾突然断在「本章未完请翻页」。这类结构的难点不在 Selenium 本身而在三件事第一翻页 URL 的拼接规则要摸准常见是-{page}.html或_{page}.html不同站点不一样第二翻到第几页算结束不能靠猜要靠「标题和内容都取不到」来判定终止第三抓几百章必然中途中断断点续传必须精确到「章节 页码」否则重跑会重复写入或漏页。这篇是 Trae Python 小说爬虫系列的第三篇聚焦混合结构的稳定抓取。适合已经能跑通单页章节、想解决多页拼接和中断恢复的人。下面给出一份可复制的config.toml骨架、Selenium 等待与重试配置以及中断后从进度文件恢复的完整验证动作。核心检索词先摆出来Trae 负责改代码和补提示词Python Selenium 负责渲染取文断点续传负责让长任务可恢复。我试过把翻页逻辑写死在循环里结果遇到某章只有 2 页、下一章有 9 页时固定范围直接抓出大量空页。后来改成「探测式翻页 进度落盘」稳定性才上来。下面按可跟做的顺序展开。2. 前置TaoToken 与 Trae 的分工在动手改代码前先把工具链理清。Trae 是编辑器侧的 AI 编程助手负责根据你的提示词修改爬虫逻辑、补全选择器和重试分支而模型调用、密钥管理这类事交给 TaoToken 处理更省心。它的模型对话入口适合边写边问「这个 XPath 为什么取不到内容」Coding Plan 适合长期跑编码和 Agent 任务接入文档则把 API 用法讲清楚。如果你只是想让 Trae 帮你改这段爬虫不一定需要额外配置但当你希望把「章节内容清洗」「异常分类」这类判断交给模型或者用 Agent 长期维护爬虫项目就需要一个稳定的调用入口。TaoToken 的 API 地址是https://taotoken.net/api控制台里可以创建和管理密钥文档里有各语言的接入示例。注意密钥只放在环境变量或本地配置文件里不要硬编码进爬虫脚本更不要提交到公开仓库。具体入口按用途分想验证模型对章节文本的理解用模型对话想长期跑编码任务用 Coding Plan要生成和管理密钥进控制台接入细节看文档。下面进入代码部分。3. 可复制配置config.toml 骨架与 Selenium 等待重试先把配置抽出来避免每次改站点都翻代码。新建config.toml把站点相关的选择器、翻页规则、路径都放进去。# config.toml —— 小说爬虫配置骨架 [site] # 目录页 URL index_url https://example.com/book/12345/ # 章节链接的基础域名用于拼接相对路径 base_url https://example.com # 单章节翻页 URL 模板{base} 为第1页去掉扩展名后的部分{page} 为页码 # 常见两种{base}_{page}.html 或 {base}-{page}.html page_url_template {base}_{page}.html # 翻页起始页码第1页已由目录链接覆盖 page_start 2 # 翻页最大页码防止死循环 page_max 12 [selectors] # 目录页章节链接 chapter_list //div[contains(class,list)]//a # 章节标题按优先级尝试 chapter_title [ //*[idread]/div[1]/span[1], //h1, //div[contains(class,chapter-title)] ] # 章节正文容器 chapter_content [ //*[idchaptercontent], //div[contains(class,content)], //article ] [selenium] headless true page_load_timeout 30 script_timeout 30 # 显式等待元素出现的最长等待秒数 wait_timeout 10 # 请求重试 max_retries 5 retry_delay 3 # 随机延迟区间秒降低被封风险 delay_min 2 delay_max 5 [output] # 保存目录 dir ./novels # 文件名不含扩展名留空则从页面提取 name # 进度文件后缀 progress_suffix .progress配置读进来后Selenium 的等待和重试要单独封装别散落在业务代码里。下面这段是等待与重试的核心直接可复制import time import random import logging from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_for_any(driver, xpaths, timeout10): 在多个 XPath 中等待任意一个出现返回 (元素, 命中的xpath) end time.time() timeout while time.time() end: for xp in xpaths: try: el driver.find_element(By.XPATH, xp) if el and el.text and el.text.strip(): return el, xp except Exception: continue time.sleep(0.3) return None, None def fetch_with_retry(driver, url, max_retries5, retry_delay3): 带重建会话的请求重试 for attempt in range(1, max_retries 1): try: driver.get(url) return True except Exception as e: logging.warning(f请求失败({attempt}/{max_retries}): {e}) if invalid session id in str(e).lower() or timeout in str(e).lower(): # 会话失效交由上层重建 driver raise time.sleep(retry_delay * attempt) return False def random_delay(lo2, hi5): time.sleep(random.uniform(lo, hi))关键点wait_for_any用轮询代替单一WebDriverWait因为多页章节里标题和正文的加载时机不一致固定等一个选择器容易误判「页面为空」。fetch_with_retry把「会话失效」单独抛出让上层决定是否重建 driver而不是在底层盲目重试。4. 翻页探测与断点续传从进度文件恢复抓取这一节是全文重点。混合结构的翻页不能写死范围要用「探测式」从page_start开始逐页请求一旦标题和内容都取不到就判定该章结束。同时进度必须记录到「章节 URL 页码」粒度。先看进度文件的结构。不要只存章节 URL要存一个字典记录每章已完成的页码import json import os class Progress: def __init__(self, path): self.path path self.data self._load() def _load(self): if os.path.exists(self.path): try: with open(self.path, r, encodingutf-8) as f: return json.load(f) except Exception as e: logging.warning(f进度文件损坏重建: {e}) return {chapters: {}} # {chapter_url: {done_pages: [1,2,3], finished: false}} def save(self): tmp self.path .tmp with open(tmp, w, encodingutf-8) as f: json.dump(self.data, f, ensure_asciiFalse, indent2) os.replace(tmp, self.path) # 原子替换避免写一半崩溃 def is_page_done(self, chapter_url, page): return page in self.data[chapters].get(chapter_url, {}).get(done_pages, []) def mark_page(self, chapter_url, page): ch self.data[chapters].setdefault(chapter_url, {done_pages: [], finished: False}) if page not in ch[done_pages]: ch[done_pages].append(page) self.save() def mark_finished(self, chapter_url): ch self.data[chapters].setdefault(chapter_url, {done_pages: [], finished: False}) ch[finished] True self.save() def is_finished(self, chapter_url): return self.data[chapters].get(chapter_url, {}).get(finished, False)注意save()用了临时文件 os.replace这是原子写。爬虫跑到一半被 CtrlC 或断电时进度文件不会变成半截 JSON下次还能读。翻页探测的核心逻辑def crawl_chapter(driver, chapter_url, cfg, progress, file_handle): if progress.is_finished(chapter_url): logging.info(f章节已完成跳过: {chapter_url}) return # 第 1 页 if not progress.is_page_done(chapter_url, 1): ok crawl_one_page(driver, chapter_url, 1, cfg, file_handle) if ok: progress.mark_page(chapter_url, 1) # 后续页探测式 base, ext os.path.splitext(chapter_url) for page in range(cfg[site][page_start], cfg[site][page_max] 1): if progress.is_page_done(chapter_url, page): continue page_url cfg[site][page_url_template].format(basebase, pagepage) ok crawl_one_page(driver, page_url, page, cfg, file_handle) if not ok: logging.info(f第 {page} 页无内容章节结束: {chapter_url}) break progress.mark_page(chapter_url, page) random_delay(cfg[selenium][delay_min], cfg[selenium][delay_max]) progress.mark_finished(chapter_url)crawl_one_page返回False的条件很明确标题和内容都取不到。这正是提示词里要求的「当未获取到 page 标题和内容时终止后续尝试」。def crawl_one_page(driver, url, page, cfg, file_handle): try: fetch_with_retry(driver, url, cfg[selenium][max_retries], cfg[selenium][retry_delay]) except Exception: return False title_el, _ wait_for_any(driver, cfg[selectors][chapter_title], cfg[selenium][wait_timeout]) content_el, _ wait_for_any(driver, cfg[selectors][chapter_content], cfg[selenium][wait_timeout]) if not title_el and not content_el: return False title title_el.text.strip() if title_el else f第{page}页 content content_el.text.strip() if content_el else if len(content) 50: return False if page 1: title 续 file_handle.write(f【{title}】\n\n{content}\n\n) file_handle.flush() return True写入后立刻flush()配合进度文件的原子写即使下一秒崩溃已写入的内容和进度也是对齐的。5. 验证请求中断后从进度文件恢复光写不验等于没写。下面给一套可复现的验证动作确认断点续传真的生效。第一步正常跑几章后手动中断。在终端按 CtrlC或者直接关掉进程。此时检查进度文件cat ./novels/苗疆禁忌档案.progress你应该看到类似结构done_pages里记录了已完成的页码finished标记整章是否结束{ chapters: { https://example.com/book/12345/1001.html: { done_pages: [1, 2, 3], finished: false }, https://example.com/book/12345/1002.html: { done_pages: [1, 2], finished: true } } }第二步重新运行爬虫。观察日志已完成的章节应打印「章节已完成跳过」未完成的章节应从缺失的页码继续而不是从第 1 页重来INFO - 章节已完成跳过: https://example.com/book/12345/1002.html INFO - 处理章节: https://example.com/book/12345/1001.html INFO - 第 4 页无内容章节结束第三步校验输出文件没有重复。用一段小脚本统计标题出现次数import re from collections import Counter with open(./novels/苗疆禁忌档案.txt, encodingutf-8) as f: text f.read() titles re.findall(r【(.?)】, text) dup {k: v for k, v in Counter(titles).items() if v 1} print(重复标题:, dup if dup else 无)如果恢复逻辑正确重复标题应为空。若出现重复多半是进度文件没在写入后立即保存或者mark_page的调用顺序放在了写入之前。第四步验证翻页终止条件。找一章实际只有 2 页的确认日志在第 3 页打印「无内容章节结束」而不是继续请求到page_max。这一步能确认探测式翻页没有退化成固定范围。6. 本篇常见错排查报错一invalid session id或chrome not reachable。长任务里 driver 会话会失效。处理方式是捕获后重建 driver并重新访问目录页建立会话再继续当前章节。不要直接退出否则断点续传就白做了。报错二翻页 URL 拼错xxx.html_2而不是xxx_2.html。这是最常见的坑。用os.path.splitext先拆掉扩展名再按模板拼接别用字符串直接加。不同站点模板不同config.toml里的page_url_template就是为这个准备的。报错三内容取到但全是广告。多页章节的后续页常插入「本章未完」和站点推广。在写入前做一次清洗用正则去掉 URL 和域名再按行过滤空行。清洗逻辑要放在crawl_one_page里保证每页都过一遍。报错四进度文件损坏导致重跑全量。如果没用原子写崩溃时 JSON 会截断。改用临时文件 os.replace并在_load里加异常兜底损坏时重建而不是抛错退出。报错五翻页无限循环。必须设page_max上限并在探测到空内容时立即break。两个条件缺一不可前者防站点返回重复内容后者防正常终止被跳过。报错六标题选择器命中广告位。多选择器按优先级尝试时要校验text非空且长度合理。wait_for_any里已经加了el.text.strip()判断避免把空元素当命中。7. 把爬虫接进长期工作流单次跑通不难难的是让它长期稳定。把配置抽到config.toml、进度落盘、等待重试封装好之后这套爬虫就能反复用。如果你希望用 Agent 长期维护它——比如自动识别新站点的翻页规则、自动补选择器——可以走 Coding Plan把编码任务交给稳定的调用入口。需要生成和管理密钥时进控制台接入细节看文档想先验证模型对章节文本的处理效果就用模型对话。回到代码本身最后留一个实用习惯每次改完站点配置先只跑前 3 章确认翻页终止和进度写入都正常再放开全量。这样能把「翻页规则错」和「断点续传错」两类问题分开定位省下大量重跑时间。