Python爬虫实战:从零构建小说本地TXT库
2026/9/7 22:27:55 网站建设 项目流程

1. 项目概述:从零到一,构建你的小说本地图书馆

每次在网上追更小说,最怕的就是网站突然打不开,或者作者更新后,之前的章节因为各种原因被下架。作为一个多年的Python开发者和小说爱好者,我早就养成了一个习惯:遇到喜欢的小说,第一时间用爬虫把它“请”到我的本地硬盘里。这不仅仅是备份,更是一种数字资产的掌控感。今天要聊的,就是如何用Python爬虫,把网络上的小说章节,规规矩矩地爬取下来,并保存成最通用、最易读的TXT文件。

这个项目听起来简单,但麻雀虽小,五脏俱全。它涵盖了Python爬虫从入门到实践的核心链路:如何发送请求获取网页、如何从复杂的HTML中精准提取文本内容、如何处理翻页逻辑、如何应对网站常见的反爬策略,以及最后如何将数据持久化保存。无论你是刚学完Python基础语法,想找个实战项目练手的新手,还是已经有一定经验,想完善自己数据采集流程的开发者,这个项目都能给你带来实实在在的收获。我们将使用最主流的requests库和BeautifulSoup库,全程代码清晰,步骤详细,确保你能跟着做,做出成果。

2. 核心思路与工具选型解析

2.1 为什么选择Requests + BeautifulSoup组合?

在Python爬虫生态中,库的选择非常多。对于爬取小说这类以静态HTML页面为主、结构相对规整的任务,requests+BeautifulSoup的组合是公认的“黄金搭档”。

requests库负责网络通信,它的API设计极其人性化,几行代码就能完成GET、POST请求,并且能方便地设置请求头、处理Cookie等。相比Python内置的urllibrequests的代码更简洁,功能更强大。而BeautifulSoup是一个HTML/XML解析库,它能够将复杂的HTML文档转换成一个复杂的树形结构,然后让你用类似找东西的方式(比如通过标签名、CSS选择器)来提取其中的数据。这个组合的优势在于轻量、直观、学习曲线平缓,非常适合处理我们目标中的小说网站。

为什么不直接用更高级的框架如Scrapy?Scrapy确实强大,它是一个为大规模爬取设计的异步框架,自带队列、去重、管道等机制。但对于我们这种目标单一、逻辑线性的小说爬虫项目来说,Scrapy显得有些“杀鸡用牛刀”,其框架本身的学习成本可能会分散我们对核心爬取逻辑的注意力。因此,从快速实现和易于理解的角度出发,requests+BeautifulSoup是最佳选择。

2.2 项目整体流程设计

在动手写代码之前,我们需要在脑子里把整个流程过一遍。一个健壮的小说爬虫,不应该只是一个简单的脚本,而应该是一个考虑周全的微型系统。其核心流程可以分解为以下几个步骤:

  1. 目标分析:确定要爬取的小说目录页URL,分析其页面结构,特别是章节链接的分布规律和翻页逻辑。
  2. 请求获取:使用requests模拟浏览器,发送HTTP请求到目标URL,获取网页的HTML源代码。这里的关键是设置合理的请求头(User-Agent),让自己看起来更像一个真实的浏览器。
  3. 内容解析:使用BeautifulSoup加载获取到的HTML,并利用其选择器(如find,find_all,select)定位到小说标题、章节链接、章节正文等关键元素。
  4. 数据提取:从解析后的元素中提取出纯文本信息,如章节标题和正文内容,并进行必要的清洗(去除广告、乱码、多余的空格和换行符)。
  5. 翻页与遍历:判断是否存在下一页,并构建下一页的URL,循环执行步骤2-4,直到爬取完所有章节。
  6. 文件保存:将爬取到的章节内容,按照一定的格式(例如“第X章 章节标题”后跟正文)追加写入到一个TXT文件中。需要考虑文件的创建、编码(通常使用UTF-8)以及写入时的异常处理。
  7. 反爬应对与健壮性:在关键步骤添加异常处理(如网络超时、页面结构变化)、设置请求间隔时间(time.sleep)以避免请求过快被封,以及可能需要的代理IP池准备。

这个流程构成了我们代码的主干。每一个步骤都有需要注意的细节和可能遇到的“坑”,我们会在接下来的实操中一一展开。

3. 实操准备与环境搭建

3.1 Python环境与库安装

首先,确保你的电脑上安装了Python。推荐使用Python 3.6及以上版本。你可以通过在命令行输入python --versionpython3 --version来检查。

接下来,安装我们所需的两个核心库。打开你的命令行终端(CMD、PowerShell或Terminal),使用pip命令进行安装:

pip install requests beautifulsoup4

如果下载速度慢,可以考虑使用国内的镜像源,例如清华源:

pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple

beautifulsoup4是库的包名,但在代码中我们导入的是from bs4 import BeautifulSoup。安装成功后,就可以开始我们的编码之旅了。

3.2 选择目标网站与初步分析

这是整个项目最关键的一步,目标选得好,事半功倍;选得不好,举步维艰。

重要提示:在爬取任何网站前,请务必遵守该网站的robots.txt协议(通常在网站根目录,如https://www.example.com/robots.txt),并尊重版权。本教程仅以技术学习为目的,请勿用于商业用途或侵犯他人权益。最好选择那些明确声明允许爬取,或没有明确禁止爬虫的网站进行练习。

对于学习而言,我们可以找一些结构清晰的网站。这里需要你亲自打开一个小说网站(例如某个小说章节页),使用浏览器的“开发者工具”(按F12键)进行分析。

分析要点:

  1. 查看章节链接:在目录页,找到章节列表所在的HTML元素。右键点击一个章节链接,选择“检查”。观察它的HTML结构,比如它可能在一个<div class="list">下的多个<a>标签里。
  2. 定位正文内容:打开一个章节内容页,同样使用“检查”,找到小说正文所在的HTML元素。通常正文会放在一个具有特定id或class的<div>标签内,例如<div id="content"><div class="chapter-content">
  3. 观察翻页逻辑:目录页如果章节很多,通常会分页。查看“下一页”按钮的链接,它是完整的URL还是一个相对路径?URL中是否有像page=2这样的参数来标识页码?

举个例子,假设我们分析一个虚构的网站,其章节链接结构可能是:

<div id="chapter-list"> <a href="/book/123/1.html">第一章 初遇</a> <a href="/book/123/2.html">第二章 修炼</a> ... </div>

正文结构可能是:

<div class="content" id="htmlContent"> <p>这里是小说正文内容...</p> </div>

而翻页可能是:<a href="?page=2">下一页</a>

记下这些选择器(如#chapter-list a,#htmlContent),它们将是我们编写解析代码的依据。网站结构千变万化,这个分析过程必须由你针对具体目标完成。

4. 核心代码实现与分步详解

下面,我们将按照流程,一步步构建爬虫。我会以一个假设的、结构清晰的目标网站为例,给出代码并详细解释。请注意,你需要将代码中的URL和选择器替换成你实际分析的结果。

4.1 步骤一:获取目录页与解析章节链接

首先,我们从一个目录页开始,获取所有章节的链接和标题。

import requests from bs4 import BeautifulSoup import time def get_chapter_links(index_url): """ 从小说目录页获取所有章节的链接和标题 :param index_url: 小说目录页的URL :return: 包含(章节标题, 章节链接)的列表 """ 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' } try: # 发送GET请求 response = requests.get(index_url, headers=headers) response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 response.encoding = response.apparent_encoding # 自动识别编码 except requests.RequestException as e: print(f"请求目录页失败: {e}") return [] soup = BeautifulSoup(response.text, 'html.parser') # 假设章节链接都在 id 为 “chapter-list” 的div下的所有a标签里 # 你需要根据实际网站结构调整这个选择器 chapter_elements = soup.select('#chapter-list a') chapter_list = [] for elem in chapter_elements: title = elem.text.strip() # 获取链接文本作为章节标题 link = elem.get('href') # 获取href属性作为链接 # 处理相对链接:如果链接不是以http开头,则拼接上基础URL if not link.startswith('http'): # 这里需要根据实际情况拼接基础URL,一个简单的方法是使用urllib.parse.urljoin from urllib.parse import urljoin link = urljoin(index_url, link) chapter_list.append((title, link)) print(f"共找到 {len(chapter_list)} 个章节。") return chapter_list # 使用示例 (URL需要替换) if __name__ == '__main__': book_index_url = "https://www.example-novel-site.com/book/123/" # 替换成真实目录页URL chapters = get_chapter_links(book_index_url) for title, link in chapters[:5]: # 打印前5章看看 print(title, link)

代码解析与注意事项:

  • 请求头(Headers):设置User-Agent是绕过基础反爬的第一步,让服务器认为请求来自浏览器而非脚本。
  • 异常处理:使用try...except包裹网络请求,response.raise_for_status()会在状态码为4xx或5xx时抛出异常,避免程序因网络问题崩溃。
  • 编码处理response.encoding = response.apparent_encoding让BeautifulSoup用更准确的方式解码网页内容,减少乱码。但有些网站可能需要强制指定utf-8
  • 选择器(Select)soup.select(‘#chapter-list a’)使用了CSS选择器语法,意思是“选择id为chapter-list的元素内部的所有<a>标签”。这是最常用的定位方式。如果网站结构复杂,可能需要更复杂的选择器,如‘div.list-main ul li a’
  • 链接拼接:网站上的链接常常是相对路径(如/book/123/1.html)。urljoin函数能智能地将其与基础URL拼接成绝对URL,这是处理链接的稳健做法。

4.2 步骤二:抓取单章内容并清洗

有了章节链接列表,我们就可以遍历它,逐个抓取正文。

def get_chapter_content(chapter_url): """ 获取单个章节的正文内容 :param chapter_url: 章节内容页的URL :return: 清洗后的章节正文文本 """ headers = {‘User-Agent‘: ’Mozilla/5.0...‘} # 同上,最好复用或定义为全局变量 try: response = requests.get(chapter_url, headers=headers) response.raise_for_status() response.encoding = response.apparent_encoding except requests.RequestException as e: print(f“请求章节页失败: {chapter_url}, 错误: {e}“) return “【内容获取失败】“ soup = BeautifulSoup(response.text, ’html.parser‘) # 假设正文在 id 为 “htmlContent” 的div里 # 你需要根据实际网站结构调整这个选择器 content_elem = soup.find(’div‘, id=’htmlContent‘) if not content_elem: # 如果第一种选择器找不到,尝试其他常见选择器 content_elem = soup.find(’div‘, class_=’content‘) if not content_elem: print(f“未找到正文内容元素,URL: {chapter_url}“) return “【正文元素未找到】“ # 获取文本并清洗 content_text = content_elem.get_text() # 清洗步骤 # 1. 替换多个换行符和空格为合理的格式 import re content_text = re.sub(r‘\s+‘, ’\n ‘, content_text) # 将连续空白符(包括换行)替换为一个换行加四个空格作为段首 # 2. 移除常见的网页广告文字(根据实际情况调整) ad_keywords = [‘笔趣阁‘, ’记住本站地址‘, ’最新最快无防盗免费阅读‘] for keyword in ad_keywords: content_text = content_text.replace(keyword, ’’) # 3. 去除首尾空白 content_text = content_text.strip() return content_text # 使用示例 if __name__ == ’__main__‘: # 假设这是其中一个章节的链接 test_url = “https://www.example-novel-site.com/book/123/1.html“ content = get_chapter_content(test_url) print(content[:500]) # 打印前500字符看看效果

内容清洗的艺术:直接get_text()得到的文本往往包含大量多余的换行、空格以及嵌入的广告文本。清洗是提升最终TXT文件可读性的关键。

  • 正则表达式清洗re.sub(r‘\s+‘, ’\n ‘, content_text)是一个非常实用的技巧。\s+匹配任何空白字符(空格、制表符、换行等)一次或多次。我们将其统一替换为“一个换行加四个空格”,这样既能区分段落,又让段落开头有缩进,更符合阅读习惯。
  • 定向替换:观察抓取下来的文本,找出那些固定的、非小说内容的广告词(如“上一章”、“下一章”、“点击这里”等),将它们替换为空字符串。
  • 分段优化:有些网站正文每个句子都放在单独的<p>标签里,导致get_text()后句子间没有空格。你可能需要更精细的处理,比如在获取所有<p>标签文本后,用’ ‘.join()连接。

4.3 步骤三:整合流程并保存为TXT文件

现在,我们把前两步结合起来,并加入文件保存和进度显示功能。

import requests from bs4 import BeautifulSoup from urllib.parse import urljoin import time import re def crawl_novel(index_url, filename=‘novel.txt‘): ”“” 爬取整本小说并保存到文件 :param index_url: 小说目录页URL :param filename: 保存的文件名 ”“” # 1. 获取所有章节链接 print(“正在获取章节列表...“) chapters = get_chapter_links(index_url) # 复用之前定义的函数 if not chapters: print(“未获取到任何章节,程序退出。“) return # 2. 创建或打开文件,使用UTF-8编码写入 with open(filename, ’w‘, encoding=’utf-8‘) as f: # 可以先写入一个书名标题(如果需要可以从目录页解析) f.write(”=== 小说爬取存档 ===\n\n“) total_chapters = len(chapters) for idx, (title, url) in enumerate(chapters, start=1): print(f”正在爬取 [{idx}/{total_chapters}]:{title}“) # 3. 获取单章内容 content = get_chapter_content(url) # 复用之前定义的函数 # 4. 将章节标题和内容写入文件 f.write(f”\n\n第{idx}章 {title}\n“) f.write(”-“ * 40 + ”\n“) # 分隔线 f.write(content + ”\n“) # 5. 礼貌性延迟,避免请求过快 time.sleep(1) # 每秒请求一次,这是一个比较保守友好的间隔 # 可选:每爬取10章打印一次进度 if idx % 10 == 0: print(f”进度:{idx}/{total_chapters}“) print(f”\n爬取完成!小说已保存至 {filename}“) # 主函数入口 if __name__ == ’__main__‘: # 替换成你要爬的小说的真实目录页URL novel_index_url = “https://www.example-novel-site.com/book/123/“ output_filename = “我最爱的小说.txt“ crawl_novel(novel_index_url, output_filename)

文件保存的关键点:

  • 编码:务必使用encoding=‘utf-8’打开文件。这是最通用的编码方式,能确保中文等字符正确保存和显示。如果使用Windows记事本打开UTF-8文件发现乱码,可以尝试用更现代的编辑器如VS Code、Notepad++或Sublime Text。
  • 写入模式:使用‘w’模式会覆盖已存在的文件。如果你想在已有文件后追加内容(例如分多次爬取),可以使用‘a’(追加)模式,但要注意管理好章节顺序和重复内容。
  • 格式美化:在写入章节标题和正文时,加入换行符(\n)和分隔线(-),可以让生成的TXT文件结构更清晰,便于阅读。
  • 延迟(Sleep)time.sleep(1)是至关重要的道德和技术考量。不加延迟地高速请求会对目标服务器造成压力,极易触发反爬机制导致IP被封锁。1到3秒的间隔是常见的礼貌等待时间。对于大规模爬取,这个时间可能需要更长,或者使用更复杂的随机延迟。

5. 进阶优化与反爬策略应对

一个只能爬取“友好”网站的爬虫是脆弱的。实际项目中,你会遇到各种反爬手段。下面介绍几种常见的应对策略。

5.1 处理动态加载内容

越来越多的网站使用JavaScript动态加载内容(如Ajax)。用requests抓取到的HTML可能不包含章节列表或正文,因为它们是在浏览器执行JS后才生成的。

解决方案:

  1. 寻找隐藏的API:打开浏览器的开发者工具,切换到“网络”(Network)选项卡,过滤XHR或Fetch请求。刷新页面或点击翻页,观察是否有新的数据请求(通常是JSON格式)。直接模拟这个请求往往能拿到结构化数据,比解析HTML更简单。
  2. 使用Selenium:Selenium可以控制一个真实的浏览器(如Chrome)来加载页面,等待JS执行完毕后再获取完整的HTML。这种方法强大但重量级,速度慢,资源消耗大。仅作为最后手段。
    from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Chrome() # 需要下载对应浏览器的driver driver.get(url) # 等待某个特定元素加载出来 element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, “chapter-list”)) ) html = driver.page_source driver.quit() # 然后用BeautifulSoup解析html

5.2 应对IP封锁与验证码

频繁请求会导致IP被暂时或永久封锁。

应对策略:

  1. 降低请求频率:增加time.sleep()的间隔时间,或在间隔中加入随机性,如time.sleep(random.uniform(1, 3)),让请求行为更接近人类。
  2. 使用代理IP池:这是应对IP封锁最有效的方法。你可以购买付费代理服务,或者寻找一些免费的代理IP(但稳定性差)。在requests中使用代理很简单:
    proxies = { ‘http‘: ’http://your-proxy-ip:port‘, ’https‘: ’https://your-proxy-ip:port‘, } response = requests.get(url, headers=headers, proxies=proxies)
    你需要一个管理器来维护一组代理IP,并在请求失败时自动切换。
  3. 处理验证码:如果遇到验证码,小型项目可以手动处理(程序暂停,等待用户输入)。自动化识别验证码(OCR、打码平台)成本较高,一般只在商业爬虫中考虑。

5.3 模拟登录与会话保持

有些小说网站需要登录才能观看VIP章节。

解决方案:

  1. 使用Session对象requests.Session()可以自动保持Cookie,模拟登录后的会话状态。
    session = requests.Session() login_data = {‘username‘: ’your_name‘, ’password‘: ’your_pass‘} login_url = ‘https://example.com/login‘ # 通常登录是POST请求 session.post(login_url, data=login_data, headers=headers) # 之后用这个session去请求需要登录的页面 response = session.get(vip_chapter_url, headers=headers)
  2. 直接使用Cookie:如果你已经通过浏览器登录,可以直接从浏览器开发者工具的“网络”选项卡中,复制某个请求的Cookie字符串,将其添加到请求头中。
    headers[‘Cookie‘] = ’你的长Cookie字符串‘

5.4 代码健壮性增强

一个工业级的爬虫必须有良好的错误处理和日志记录。

import logging # 配置日志 logging.basicConfig(level=logging.INFO, format=’%(asctime)s - %(levelname)s - %(message)s‘, filename=’novel_crawler.log‘) def robust_crawl(): chapters = get_chapter_links(index_url) for idx, (title, url) in enumerate(chapters): try: content = get_chapter_content(url) if “【内容获取失败】” in content or “【正文元素未找到】” in content: logging.warning(f”章节爬取异常,标题:{title}, URL: {url}“) # 可以在这里加入重试逻辑 continue # 正常写入文件... time.sleep(random.uniform(1.5, 3.5)) # 随机延迟 except Exception as e: logging.error(f”处理章节时发生未知错误,标题:{title}, URL: {url}, 错误:{e}“) # 记录错误后,根据策略决定是继续还是终止 continue

6. 常见问题排查与实战心得

在实际操作中,你一定会遇到各种各样的问题。下面是我总结的一些常见“坑”和解决思路。

6.1 编码乱码问题

这是新手最常遇到的问题。屏幕上打印出来或保存到文件里的中文变成了乱码。

  • 可能原因1:服务器返回的编码声明与实际不符。
    • 解决:优先使用response.encoding = response.apparent_encoding。如果还不行,可以尝试常见的编码如‘utf-8’,‘gbk’,‘gb2312’进行手动测试。response.content.decode(‘gbk’)
  • 可能原因2:文件保存时编码错误。
    • 解决:确保open()函数中指定了encoding=‘utf-8’
  • 排查技巧:打印response.encodingresponse.apparent_encoding的值,并查看response.text的前几百个字符,看是否有可识别的中文。也可以直接查看网页源码的<head>部分<meta charset=”...”>标签。

6.2 抓取不到内容或内容为空

程序运行了,但章节列表或正文是空的。

  • 可能原因1:选择器写错了。
    • 解决:这是最常见的原因。再次用浏览器开发者工具仔细检查目标元素的id、class或路径。BeautifulSoupfindselect方法非常依赖精确的选择器。可以先用print(soup.prettify()[:2000])打印部分美化后的HTML,确认结构。
  • 可能原因2:网站有反爬,请求被拒绝。
    • 解决:检查请求头是否完备。除了User-Agent,有时还需要Referer,Accept-Language,Cookie等。可以复制浏览器中正常请求的Headers全部加进去试试。检查返回的状态码和HTML内容,是否包含“拒绝访问”、“验证”等字样。
  • 可能原因3:内容是动态加载的。
    • 解决:按照5.1节的方法,检查是否有隐藏的API或考虑使用Selenium。

6.3 被网站屏蔽或封禁IP

程序跑了一会儿就再也获取不到数据了,或者返回错误页面。

  • 立即措施:首先,大幅增加请求间隔,比如time.sleep(10)。然后,更换你的IP地址(重启路由器或使用代理)。检查你的请求频率是否过高。
  • 长期策略:实现一个完整的爬虫礼仪:
    1. 遵守robots.txt
    2. 设置合理的延迟,并加入随机性。
    3. 使用代理IP池轮换IP。
    4. 模拟真实用户行为,如随机滚动页面、随机点击(对于需要JS的网站)。
    5. 设置断点续爬,将已爬取的URL记录到文件或数据库,下次从中断处开始,避免重复请求。

6.4 文件保存格式混乱

TXT文件打开后,段落挤在一起,或出现很多奇怪的符号。

  • 解决:核心在于内容清洗。我们的re.sub(r‘\s+‘, ’\n ‘, content_text)是基础。你需要根据目标网站的具体情况定制清洗规则。例如,有些网站用<br>换行,BeautifulSoup解析时可能需要特殊处理。可以写一个专门的clean_text()函数,通过多次正则替换和字符串操作来净化文本。

我个人最深刻的体会是:爬虫项目,七分在分析,三分在编码。花足够多的时间去理解目标网站的结构、数据加载方式和潜在的反爬机制,比盲目写代码调试要高效得多。另外,一定要有“友好爬取”的意识,控制速度,不要给别人的服务器造成负担。这个简单的“小说爬虫”项目,就像一把钥匙,帮你打开了网络数据采集的大门。理解了它的原理,你就能触类旁通,去应对更复杂的数据抓取任务了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询