ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python爬虫实战:从零构建小说采集工具与反爬策略详解

2026/8/17 9:08:08 拓冰建站 浏览量
Python爬虫实战:从零构建小说采集工具与反爬策略详解

1. 从零到一:为什么用Python爬小说是门手艺活

最近在技术社区和论坛里,经常看到有朋友问怎么用Python把网上的小说爬下来存成txt。乍一看,这需求简单直接:不就是找个网页,把文字扒下来,然后写进文件吗?但真上手操作过几次你就会发现,这事儿远没想象中那么容易。它不像调用一个现成的API那么简单,更像是在跟网站的后台工程师玩一场“猫鼠游戏”。你需要处理千奇百怪的网页结构,应对防不胜防的反爬虫机制,还要保证数据抓取的高效和稳定。今天,我就结合自己这些年爬过上百个小说站点的经验,来聊聊怎么把“爬小说”这件事,从一个简单的脚本,打磨成一个可靠的生产力工具。我们最终的目标,不仅仅是“能爬下来”,而是“能优雅、稳定、高效地爬下来”,并且生成一份干净、易读的txt文档。

2. 战前准备:环境搭建与核心库选型

工欲善其事,必先利其器。在动手写代码之前,选择合适的工具库至关重要。对于Python爬虫,尤其是小说这种以文本内容为主的场景,库的选择直接决定了代码的简洁度和后续维护的复杂度。

2.1 基础环境与库安装

首先,确保你有一个可用的Python环境(建议3.6及以上版本)。接下来,通过pip安装我们需要的核心库。打开你的终端或命令提示符,执行以下命令:

pip install requests beautifulsoup4 lxml

这里简单解释一下这三个库的分工:

  • requests:这是HTTP库的“事实标准”。它的API设计极其人性化,用来发送网络请求、获取网页的HTML源代码,比Python自带的urllib要方便太多。
  • BeautifulSoup4(bs4):网页解析神器。它能把一堆杂乱无章的HTML标签,变成一个结构清晰的树形对象,让你可以像操作字典和列表一样,轻松地找到并提取出标题、正文、下一章链接等关键信息。
  • lxml:这是一个高性能的HTML/XML解析器。BeautifulSoup本身只是一个“包装器”,它需要依赖一个底层的解析引擎。lxml的解析速度非常快,稳定性也好,是我们首选的解析器。

注意:在某些Linux发行版或纯净的Docker环境中,可能需要先安装lxml的系统依赖。例如在Ubuntu上,可以运行sudo apt-get install python3-lxmlsudo apt-get install libxml2-dev libxslt-dev

2.2 为什么是它们?选型背后的逻辑

你可能会问,网上也有scrapy这样的重型框架,为什么不用?对于小说爬虫这种典型的“顺序抓取”(一章接一章)任务,scrapy的异步架构和中间件机制显得有些“杀鸡用牛刀”,学习曲线也陡峭。而requests+BeautifulSoup的组合,足够轻量、直观,特别适合初学者快速上手和理解爬虫的核心流程:请求 -> 解析 -> 提取 -> 存储。

另一个常见的替代解析方案是pyquery(语法类似jQuery)或直接用lxml的XPath。BeautifulSoup的优点是容错性极好,即使网页HTML写得不太规范,它也能尽力帮你解析出来,这对于结构各异的小说网站来说是个巨大的优势。它的findfind_all方法,通过标签名、属性、CSS选择器来定位元素,非常符合直觉。

3. 核心攻防:网页解析与反爬虫策略

拿到网页HTML只是第一步,如何精准地“挖出”我们想要的小说正文和章节链接,才是真正的挑战。同时,你必须意识到,你的爬虫程序是不受欢迎的访客,网站会有各种手段来阻止你。

3.1 解析策略:如何定位正文与链接

每当你拿到一个新小说网站的章节页面,第一件事不是写代码,而是“侦察”。用浏览器的“开发者工具”(F12打开)仔细分析页面结构。

  1. 定位正文容器:查看小说正文部分对应的HTML标签。常见的情况有:

    • 正文在一个<div id="content"><div class="content">里。
    • 可能包裹在<article>标签内。
    • 更复杂的情况下,正文可能由多个<div><p>标签组成。

    你需要找到那个能唯一标识正文区域的标签和属性。右键点击正文,选择“检查”,就能在Elements面板里看到它的HTML结构。我们的目标是找到那个包裹所有正文、且相对最外层的标签。

  2. 定位“下一章”链接:同样使用开发者工具,找到“下一章”或“下一页”按钮对应的<a>标签。查看它的href属性(链接地址),以及它周围的标签结构(比如它是否在一个<div class="bottem2">里)。

  3. 编写解析代码:基于你的侦察结果,使用BeautifulSoup编写提取逻辑。例如,假设正文在<div id="content">里,下一章链接是id="next"<a>标签:

from bs4 import BeautifulSoup import requests url = ‘你的章节页面URL’ headers = { ‘User-Agent‘: ’Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36‘ } response = requests.get(url, headers=headers) response.encoding = response.apparent_encoding # 自动识别编码 soup = BeautifulSoup(response.text, ’lxml‘) # 提取正文 content_div = soup.find(’div‘, id=’content‘) if content_div: # 清理正文中可能存在的广告、脚本等无关标签 for tag in content_div([’script‘, ’style‘, ’div‘, ’a‘]): tag.decompose() content_text = content_div.get_text(strip=True, separator=’\n‘) else: content_text = “正文未找到” # 提取下一章链接 next_link_tag = soup.find(’a‘, id=’next‘) if next_link_tag and next_link_tag.has_attr(’href‘): next_url = next_link_tag[’href‘] # 处理相对路径链接 if next_url.startswith(’/‘): from urllib.parse import urljoin next_url = urljoin(url, next_url) else: next_url = None

3.2 反爬虫应对:如何让你的爬虫“活”得更久

直接使用上面的代码,很可能第一次请求就失败了,或者爬了几章就被封了IP。这是因为你没有进行任何伪装。下面是一些必须掌握的基础反反爬策略:

  1. 设置请求头(Headers):这是最基本的伪装。一个真实的浏览器请求会携带大量的头信息,其中User-Agent是最关键的。务必在每次请求中都设置一个常见的浏览器User-Agent。此外,Referer头(告诉服务器你从哪个页面跳转过来)有时也很重要。

    headers = { ’User-Agent‘: ’Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36‘, ’Referer‘: ’https://www.example.com/‘, # 通常是小说网站的主页或上一章地址 ’Accept-Language‘: ’zh-CN,zh;q=0.9‘, }
  2. 处理Cookie与Session:有些网站需要登录才能看小说,或者通过Cookie来跟踪会话。你可以使用requests.Session()对象,它会自动在多次请求间保持Cookie。

    session = requests.Session() session.headers.update(headers) # 如果需要登录,可以先post登录信息 # login_data = {‘username‘: ’...‘, ’password‘: ’...‘} # session.post(login_url, data=login_data) # 后续都用session.get(url) response = session.get(chapter_url)
  3. 控制访问频率(延迟):这是最重要的道德和技术准则。疯狂地连续请求会给服务器带来巨大压力,导致你的IP被迅速封禁。务必在每次请求之间添加随机延迟

    import time import random def crawl_chapter(url): # ... 发送请求和解析的代码 ... time.sleep(random.uniform(1, 3)) # 随机等待1到3秒 return content, next_url
  4. 处理动态加载内容:越来越多的网站使用JavaScript动态加载内容(比如滚动到页面底部才加载下一章)。requests只能获取初始HTML,对于JS加载的内容无能为力。这时就需要用到SeleniumPlaywright这样的浏览器自动化工具来模拟真人操作,或者更高级地,去分析网站的网络请求(XHR/Fetch),找到直接返回数据的API接口,然后用requests去调用那个接口。这属于进阶技巧,如果遇到纯静态页面无法获取内容的情况,就要往这个方向思考。

4. 工程化实践:构建健壮的小说爬虫框架

一个只能爬一章的脚本是玩具,我们需要的是一个能自动爬完整本书、并且能处理各种异常情况的程序。这就涉及到程序的结构设计。

4.1 核心爬取循环与状态管理

爬虫的核心逻辑是一个循环:从起始章节URL开始,爬取内容,保存,然后找到下一章链接,更新URL,继续循环,直到没有下一章链接为止。

import os def crawl_novel(start_url, save_path=‘novel.txt‘): ”“” 爬取整本小说的主函数 ”“” current_url = start_url chapter_count = 0 with open(save_path, ’w‘, encoding=’utf-8‘) as f: while current_url: chapter_count += 1 print(f’正在爬取第 {chapter_count} 章: {current_url}‘) try: # 爬取单章内容 chapter_title, chapter_content, next_url = crawl_single_chapter(current_url) # 写入文件 f.write(f’\n\n第{chapter_count}章 {chapter_title}\n‘) f.write(’-‘ * 40 + ’\n‘) f.write(chapter_content) f.write(’\n‘) # 更新当前URL为下一章链接 current_url = next_url # 随机延迟,避免请求过快 time.sleep(random.uniform(2, 5)) except requests.exceptions.RequestException as e: print(f’网络请求出错 ({current_url}): {e}‘) # 可以选择记录错误URL,稍后重试,或者直接退出 break except Exception as e: print(f’解析或处理出错 ({current_url}): {e}‘) # 同样记录或处理 break print(f’爬取完成!共爬取 {chapter_count} 章,已保存至 {save_path}‘)

4.2 异常处理与持久化

网络世界充满不确定性,你的爬虫必须足够健壮。

  • 网络异常requests可能抛出多种异常,如连接超时、拒绝连接等。使用try...except包裹请求代码,并做好重试机制。一个简单的重试装饰器可以大大提升稳定性。

    import functools def retry(times=3, delay=2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: if i == times - 1: raise e print(f’第{i+1}次尝试失败,{delay}秒后重试...‘) time.sleep(delay) return None return wrapper return decorator @retry(times=3, delay=5) def fetch_page(url): return requests.get(url, headers=headers, timeout=10)
  • 数据异常:解析时可能找不到预期的标签。你的代码不能假设一切都会按计划进行,要对soup.find返回的None值做判断,并提供备选方案或清晰的错误日志。

  • 进度持久化:爬一本长篇小说可能耗时数小时,如果中途程序崩溃或网络中断,从头开始爬是灾难性的。一个简单的改进是将当前爬取到的章节URL和序号保存到一个本地的状态文件(如progress.json)中。每次循环开始前读取状态,程序重启后可以从断点继续。

    import json STATE_FILE = ’crawl_state.json‘ def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, ’r‘) as f: return json.load(f) return {’current_url‘: START_URL, ’chapter_num‘: 0} def save_state(url, num): with open(STATE_FILE, ’w‘) as f: json.dump({’current_url‘: url, ’chapter_num‘: num}, f) # 在主循环中集成 state = load_state() current_url = state[’current_url‘] chapter_count = state[’chapter_num‘] while current_url: # ... 爬取和保存 ... save_state(next_url, chapter_count) # 每成功爬取一章就保存一次状态

5. 数据清洗与TXT文件优化

直接从网页扒下来的文本往往包含许多“杂质”:多余的空格、换行、HTML实体字符(如&nbsp;)、甚至烦人的“防盗版”乱码文字。直接保存这样的内容,阅读体验会很差。

5.1 文本清洗的常见操作

在将content_text写入文件前,应该进行一系列清洗:

import re def clean_text(text): ”“” 清洗小说正文文本 ”“” if not text: return “” # 1. 替换HTML实体字符 text = text.replace(’&nbsp;‘, ’ ‘).replace(’&amp;‘, ’&‘).replace(’&lt;‘, ’<‘).replace(’&gt;‘, ’>‘) # 2. 合并过多的空白字符(多个空格、制表符、换行符) # 将任何连续的空白字符(包括空格、换行、制表)替换为单个换行符,这通常能很好处理段落 text = re.sub(r’\s+‘, ’\n‘, text) # 3. 处理特定网站的防盗版乱码(例如,在段落中随机插入无关汉字) # 这需要针对具体网站观察规律,可能使用正则表达式替换 # 例如,假设无关字总是“【看】【无】【错】【小】【说】”这些字符的组合 # text = re.sub(r’【看】【无】【错】【小】【说】‘, ’‘, text) # 4. 去除首尾空白 text = text.strip() return text

5.2 文件编码与格式

编码是文本处理的基石。中文小说必须使用UTF-8编码进行保存,否则在其它设备上打开就会出现乱码。

with open(‘novel.txt‘, ’w‘, encoding=’utf-8‘) as f: f.write(content)

在Windows的记事本中,UTF-8文件可能会被误判。一个更稳妥的做法是写入UTF-8 with BOM(字节顺序标记),但这不是标准做法,可能影响其他工具。对于绝大多数现代文本编辑器和阅读器(如VS Code, Sublime, Notepad++),纯UTF-8完全没有问题。

关于TXT文件的格式,我个人的习惯是:

  • 每章有一个明确的标题,如“第XXX章 章节名”。
  • 章节标题上下用分隔线(如==========)或空行隔开,增强可读性。
  • 正文段落之间保持一个空行。这可以通过在清洗时,将连续的两个换行符\n\n保留,而将单个换行符替换为空格来实现更精细的段落控制。

6. 实战中的“坑”与进阶技巧

纸上得来终觉浅,绝知此事要躬行。下面分享几个我踩过坑后总结的经验。

6.1 编码检测:解决乱码的第一道关卡

response.encoding = response.apparent_encoding这行代码非常有用,它让requests库去猜测页面的正确编码。但它的猜测并非100%准确。有些网站会错误地声明编码,或者使用一些特殊的编码。如果发现解析出的中文是乱码,你需要手动检查并指定编码。

  1. 查看网页源代码中的<meta charset="...">标签。

  2. 查看HTTP响应头中的Content-Type字段,如Content-Type: text/html; charset=gbk

  3. 使用chardet库进行更精确的检测(需要额外安装pip install chardet)。

    import chardet raw_data = response.content encoding = chardet.detect(raw_data)[’encoding‘] response.encoding = encoding if encoding else ’utf-8‘ # 备选方案

6.2 应对IP封锁:代理IP池的简单应用

当你严格遵守了延迟策略,但仍然被封锁时,很可能你的IP已经被网站列入黑名单。这时就需要使用代理IP。

  • 免费代理:网上有很多免费代理IP列表,但质量极不稳定,速度慢,存活时间短,仅适用于学习或低频爬取。
  • 付费代理服务:对于严肃的项目,建议使用可靠的付费代理服务,它们提供高匿、稳定的代理IP,通常按流量或时间计费。

requests中使用代理非常简单:

proxies = { ‘http‘: ’http://12.34.56.78:8080‘, ’https‘: ’http://12.34.56.78:8080‘, } # 在请求时传入proxies参数 response = requests.get(url, headers=headers, proxies=proxies, timeout=10)

一个简单的代理IP池可以是一个列表,每次请求随机选取一个,并在代理失效时将其从池中移除。

6.3 分布式与增量爬取思路

如果项目规模变大(例如需要监控成千上万本书的更新),单机单线程的爬虫就力不从心了。

  • 增量爬取:只爬取更新的章节。这需要你维护一个本地数据库(如SQLite),记录每本书已爬取的最新章节URL。每次运行时,从网站目录页获取最新的章节列表,与数据库对比,只爬取新增的URL。
  • 简单分布式:利用multiprocessingthreading模块进行多线程/多进程爬取。但请注意,多线程爬取同一网站极易触发反爬,必须非常谨慎地控制总请求速率,并确保使用不同的代理IP或User-Agent。更常见的做法是将不同的书(甚至不同网站)分配给不同的进程去爬,避免对单一目标站点造成过大压力。

爬虫技术是一个不断对抗和升级的过程。网站会更新,反爬策略会加强,你的代码也需要持续维护。保持耐心,多观察、多分析、多测试,从每一次“爬取失败”中学习,你会逐渐从一个爬虫新手,成长为能应对各种复杂场景的老手。记住,尊重网站的robots.txt规则,合理控制爬取速度,是每个爬虫开发者应有的素养。