1. 项目缘起:为什么选择飞卢小说网作为爬取目标?
最近在整理自己的电子书库,想找几本特定题材的小说离线阅读,结果发现很多平台要么需要付费订阅,要么就是阅读体验被广告和弹窗搞得支离破碎。作为一个有十多年经验的开发者,我第一个想到的自然是自己动手,丰衣足食——写个爬虫。在众多小说网站里,飞卢小说网成了一个很有意思的目标。它不像起点、纵横那样有极其复杂的反爬机制和动态渲染,但也不像一些纯静态的小站那样毫无挑战。飞卢的页面结构相对清晰,更新频率高,小说题材也很有特色,非常适合作为中级爬虫练手和实战的项目。
这个项目的核心,就是通过Python爬虫技术,自动化地从飞卢小说网抓取指定小说的完整内容,并整理成结构化的文本文件(如TXT或EPUB),方便我们在任何设备上离线阅读。听起来简单,但里面涉及到的细节可不少:如何模拟真实用户请求绕过基础反爬?如何处理分页和章节链接?怎样优雅地处理网络异常和编码问题?以及,最重要的,如何在满足个人学习需求的同时,严格遵守法律法规和网站的服务条款,做到“取之有道”?接下来,我就把自己这次爬取飞卢小说的完整过程、踩过的坑以及总结的经验,毫无保留地分享出来。
2. 环境准备与核心工具选型
工欲善其事,必先利其器。在开始写代码之前,选择合适的工具库能让我们事半功倍。对于飞卢小说网这样的现代网站,直接使用requests库裸奔式地抓取html内容,大概率会吃闭门羹。我们需要一套能更好模拟浏览器行为的工具链。
2.1 为什么选择 Requests + BeautifulSoup + 动态Headers?
最初我尝试了最经典的requests+BeautifulSoup组合。Requests库简单易用,是处理HTTP请求的瑞士军刀;BeautifulSoup则是解析HTML/XML的利器,用起来非常顺手。但很快我就遇到了第一个障碍:直接使用requests.get()获取飞卢小说网的章节页面,返回的往往是状态码200但内容为空,或者是一段提示“请开启JavaScript”的页面。
这其实是网站最基础的反爬手段之一:检查请求头(Headers)。服务器会通过分析User-Agent、Referer、Cookie等字段来判断请求是来自真实的浏览器还是脚本。我们的脚本如果使用默认的Headers,一眼就会被识别出来。
因此,我们的第一个关键步骤就是伪装Headers。我们需要让我们的请求看起来像一个从谷歌浏览器发起的普通访问。
import requests from bs4 import BeautifulSoup # 定义一个完整的、模拟Chrome浏览器的请求头 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', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Cache-Control': 'max-age=0', }这里的User-Agent字符串至关重要,它直接告诉服务器我们使用的是哪个浏览器。你可以通过打开浏览器的开发者工具(F12),在Network标签页中查看任意一个请求,复制它的User-Agent值。Accept-Language告诉服务器我们优先接收中文内容,这对获取正确编码的页面也有帮助。
2.2 备用方案:Selenium/Playwright的必要性评估
尽管加上了Headers,但在实际爬取过程中,我仍然发现有些页面内容(特别是涉及动态加载的推荐列表、评论区)无法通过简单的requests获取。这时,就需要考虑更高级的工具:Selenium或Playwright。
这两个工具可以自动化控制一个真实的浏览器(如Chrome、Firefox),执行点击、滚动、输入等操作,并能获取到JavaScript完全执行后的页面源码。这对于爬取高度动态化的网站几乎是必备的。
- Selenium:老牌自动化测试工具,社区庞大,资料多。但需要单独下载浏览器驱动(如
chromedriver),配置稍麻烦,运行速度相对较慢。 - Playwright:后起之秀,由微软开发。它内置了浏览器内核,无需单独管理驱动,API更现代,执行速度也更快,对动态内容的处理能力更强。
对于飞卢小说网,我的经验是:核心的小说正文内容,用requests+伪装Headers基本可以搞定;但如果你需要爬取需要登录后才能看的小说(如VIP章节),或者网站后期升级了更复杂的反爬(如滑块验证),那么Selenium或Playwright就是必须的。
在本项目中,为了聚焦于核心的爬取逻辑和降低环境复杂度,我优先使用requests方案。但我会在代码结构中预留接口,方便后续无缝切换到浏览器自动化方案。这是工程实践中一个很好的习惯:将数据获取层(Fetcher)与数据解析层(Parser)解耦。
2.3 项目依赖安装
确定了技术栈,我们就可以创建虚拟环境并安装依赖了。强烈建议使用虚拟环境(如venv或conda)来管理项目依赖,避免污染全局环境。
# 创建并激活虚拟环境(以venv为例) python -m venv feilu_env source feilu_env/bin/activate # Linux/Mac # feilu_env\Scripts\activate # Windows # 安装核心依赖 pip install requests beautifulsoup4 lxml # 可选:如果后续决定使用Playwright pip install playwright playwright install chromium # 安装Chromium浏览器这里解释一下几个包:
requests: 用于发送HTTP请求。beautifulsoup4: 用于解析HTML。lxml: 是BeautifulSoup的一个解析器,速度比Python内置的html.parser快,容错性更好,建议安装。playwright: 如果需要,用于控制浏览器。
3. 网站结构分析与爬取策略制定
在动手写代码之前,花时间分析目标网站的结构是最高效的一步。盲目开始写解析规则,后面大概率会因页面结构变动而返工。
3.1 飞卢小说网页面结构拆解
飞卢小说网的小说阅读页通常有比较固定的URL模式,例如:https://b.faloo.com/1234567_1.html。其中1234567是小说ID,_1代表第一章。我们可以通过访问小说目录页来获取所有章节的链接。
- 找到目录页:通常,在小说简介页或阅读页的侧边栏或顶部,会有“目录”链接。其URL可能类似于
https://b.faloo.com/f/1234567.html。 - 解析目录页:目录页列出了所有章节的标题和链接。我们需要用BeautifulSoup解析这个页面,提取出每一个
<a>标签的href属性和文本内容。这里要注意,飞卢的目录可能是分页加载的,我们需要检查是否有“下一页”按钮,并循环抓取所有目录页。 - 解析章节页:进入具体的章节页面(如
https://b.faloo.com/1234567_1.html),我们需要定位到小说正文所在的HTML元素。通过浏览器开发者工具的“检查”功能,可以很容易地看到正文通常包裹在一个具有特定id或class的<div>标签内,例如<div id="content”>或<div class="content”>。
注意:网站的前端结构可能会改版。今天有效的CSS选择器,明天可能就失效了。因此,我们的解析规则要尽量健壮。不要依赖于过于复杂或脆弱的选择路径,优先使用
id属性,其次是具有唯一性的class名。同时,写好异常处理和日志,一旦解析失败能立刻知道。
3.2 设计健壮的爬取流程
一个健壮的爬虫不应该是一次性的脚本,它需要考虑到网络波动、页面结构微调、反爬策略升级等各种情况。我设计了以下核心流程:
- 输入与初始化:接收用户输入的小说ID或目录页URL。初始化请求会话(
requests.Session()),复用TCP连接和Cookies,提升效率。 - 目录抓取模块:
- 发送请求获取目录页HTML。
- 解析出所有章节链接和标题,存储为列表
[(‘第1章 标题’, ‘url1’), (‘第2章 标题’, ‘url2’), …]。 - 处理目录分页逻辑。
- 内容抓取模块:
- 遍历章节链接列表。
- 对每个链接,发送请求(带上Headers,并合理设置
timeout和重试机制)。 - 解析响应HTML,提取正文内容。
- 清洗内容:去除HTML标签、广告段落、无关的空格和换行符。
- 存储模块:
- 将清洗后的正文与标题组合,按顺序写入到一个TXT文件中。
- 可以考虑更复杂的存储,如每章一个文件,或者生成EPUB格式。
- 控制与容错模块:
- 在每章抓取之间插入随机延时(如
time.sleep(random.uniform(1, 3))),避免请求过于频繁被服务器封禁IP。 - 实现重试逻辑:当请求失败(超时、状态码非200)时,自动重试若干次。
- 记录日志:成功抓取了哪些章节,哪些失败了,失败原因是什么。
- 在每章抓取之间插入随机延时(如
这个流程的核心思想是“模块化”和“可容错”。每个模块职责单一,方便单独测试和替换(比如把解析器从BeautifulSoup换成parsel)。容错机制保证了即使个别章节抓取失败,整个任务也不会崩溃,我们可以事后根据日志手动补抓。
4. 核心代码实现与逐行解读
理论说得再多,不如一行代码。下面我将分模块展示核心代码,并解释关键行背后的考量。
4.1 请求会话与工具函数
首先,我们创建一个Session对象并定义一些工具函数。
import requests import time import random import logging from bs4 import BeautifulSoup from urllib.parse import urljoin # 配置日志,方便调试和追踪 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s') logger = logging.getLogger(__name__) class FeiluNovelSpider: def __init__(self): self.session = requests.Session() # 设置会话级别的Headers,该会话下的所有请求都会携带这些头信息 self.session.headers.update({ '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', 'Accept-Language': 'zh-CN,zh;q=0.9', }) self.base_url = ‘https://b.faloo.com’ # 飞卢小说网基础域名 def _get_page(self, url, max_retries=3): """获取页面HTML,包含重试机制""" for i in range(max_retries): try: # 随机延时,模拟人类操作 time.sleep(random.uniform(1, 2)) resp = self.session.get(url, timeout=10) resp.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 # 检查编码,飞卢通常使用utf-8或gbk,这里先尝试utf-8 resp.encoding = resp.apparent_encoding # 让requests自动判断编码 return resp.text except requests.exceptions.RequestException as e: logger.warning(f”第{i+1}次尝试请求 {url} 失败: {e}”) if i < max_retries - 1: wait_time = 2 ** i # 指数退避策略,等待时间递增 logger.info(f”等待{wait_time}秒后重试...”) time.sleep(wait_time) else: logger.error(f”请求 {url} 失败,已达最大重试次数。”) return None关键点解读:
requests.Session(): 创建一个会话对象,可以自动处理Cookies,并在多次请求间保持连接,比单次requests.get更高效。_get_page方法:这是整个爬虫的基石。它包含了随机延时(time.sleep)和指数退避重试机制。指数退避(2 ** i)是一种经典的网络请求重试策略,首次失败等2秒,第二次等4秒,以此类推,避免在服务器临时故障时加剧其负载。resp.encoding = resp.apparent_encoding: 自动检测响应内容的编码并设置,可以解决大部分中文乱码问题。如果发现还有乱码,可以尝试强制指定resp.encoding = ‘gbk’或’utf-8’。
4.2 目录解析器实现
目录页是爬虫的“地图”,我们必须准确无误地解析它。
def parse_catalog(self, catalog_url): """解析小说目录页,获取所有章节的标题和链接""" html = self._get_page(catalog_url) if not html: logger.error(“无法获取目录页,请检查URL或网络。”) return [] soup = BeautifulSoup(html, ‘lxml’) chapter_list = [] # 关键步骤:找到包含所有章节链接的容器。 # 需要通过浏览器开发者工具仔细分析目录页的HTML结构。 # 假设章节链接都在 class 为 ‘chapter-list’ 的 div 下的 a 标签里 catalog_div = soup.find(‘div’, class_=‘chapter-list’) if not catalog_div: # 如果找不到,尝试其他可能的选择器,这是解析器需要适应性的地方 catalog_div = soup.find(‘div’, id=‘chapterList’) if not catalog_div: logger.error(“未找到章节列表容器,网站结构可能已更改。”) # 可以尝试打印一部分HTML来调试 # logger.debug(soup.prettify()[:1000]) return [] # 找到所有的章节链接 for a_tag in catalog_div.find_all(‘a’, href=True): chapter_title = a_tag.get_text().strip() chapter_url = a_tag[‘href’] # 处理相对链接,拼接成完整URL full_chapter_url = urljoin(self.base_url, chapter_url) # 过滤掉可能不是正文章节的链接(比如“最新章节”、“作品相关”) if chapter_title and ‘chapter’ in full_chapter_url.lower(): chapter_list.append((chapter_title, full_chapter_url)) logger.debug(f”找到章节: {chapter_title} -> {full_chapter_url}”) logger.info(f”共解析到 {len(chapter_list)} 个章节。”) return chapter_list关键点解读与避坑:
soup.find(‘div’, class_=‘chapter-list’): 这是解析的核心,依赖于网站的CSS类名。这是整个爬虫最脆弱的部分。一旦网站改版,这个选择器就失效了。因此,代码中提供了备选选择器(id=‘chapterList’),并记录了详细的错误日志。在实际操作中,你可能需要根据实际的页面结构调整这个选择器。urljoin: 非常重要!章节链接(href)很可能是相对路径(如/1234567_1.html)。urljoin函数能将其与基础URL(base_url)正确拼接成绝对路径。忽略这一步会导致后续请求失败。- 过滤逻辑:
if ‘chapter’ in full_chapter_url.lower()是一个简单的过滤,防止把“推荐票”、“评论”等无关链接也当成章节抓取。你可能需要根据实际情况调整过滤条件。
4.3 正文内容提取与清洗
获取到章节链接后,下一步就是抓取并清洗正文。
def parse_chapter_content(self, chapter_url): """解析单个章节页面,提取并清洗正文内容""" html = self._get_page(chapter_url) if not html: return “【内容获取失败】” soup = BeautifulSoup(html, ‘lxml’) # 关键步骤:定位正文所在的元素。同样需要分析页面结构。 # 假设正文在 id 为 ‘content’ 的 div 里 content_div = soup.find(‘div’, id=‘content’) if not content_div: # 尝试其他常见的选择器 content_div = soup.find(‘div’, class_=‘content’) if not content_div: logger.error(f”无法定位正文内容,URL: {chapter_url}”) return “【正文定位失败】” # 提取文本并清洗 raw_text = content_div.get_text(separator=‘\n’, strip=True) # 进一步清洗:移除常见的广告语、网站声明等 lines = raw_text.split(‘\n’) cleaned_lines = [] for line in lines: line = line.strip() # 过滤空行和包含特定广告关键词的行 if line and not any(ad in line for ad in [‘飞卢小说网’, ‘www.faloo.com’, ‘最新网址’, ‘PS:’]): cleaned_lines.append(line) cleaned_content = ‘\n’.join(cleaned_lines) # 简单处理首行缩进(两个全角空格) cleaned_content = cleaned_content.replace(‘ ‘, ‘ ‘) # 将两个全角空格替换为四个半角空格,或根据喜好调整 return cleaned_content关键点解读与清洗技巧:
get_text(separator=‘\n’, strip=True): BeautifulSoup的get_text方法可以提取标签内所有文本。separator=‘\n’指定用换行符连接不同标签的文本,这样能保留段落结构。strip=True去除首尾空白。- 二次清洗:直接提取的文本往往包含网站版权声明、广告插入等无关信息。我们需要根据这些信息的特征进行过滤。上面的代码通过检查行是否包含特定关键词(如“飞卢小说网”)来过滤。这是一个持续的过程,你可能会在爬取过程中发现新的“垃圾行”,需要不断更新这个过滤列表。
- 格式美化:
cleaned_content.replace(‘ ‘, ‘ ‘)是一个简单的格式处理,将中文段落首行常见的两个全角空格替换为四个半角空格,让生成的TXT文件排版更美观。你可以根据个人阅读习惯调整。
4.4 主控流程与文件存储
最后,我们把所有模块串联起来,并实现文件存储。
def crawl(self, catalog_url, output_file=‘novel.txt’): """主爬取流程""" logger.info(f”开始爬取小说,目录页: {catalog_url}”) chapters = self.parse_catalog(catalog_url) if not chapters: logger.error(“未获取到任何章节,程序退出。”) return logger.info(f”开始抓取正文,共 {len(chapters)} 章。”) with open(output_file, ‘w’, encoding=‘utf-8’) as f: for idx, (title, url) in enumerate(chapters, 1): logger.info(f”正在抓取 [{idx}/{len(chapters)}] {title}”) content = self.parse_chapter_content(url) # 写入文件 f.write(f”{title}\n\n”) # 写入章节标题 f.write(f”{content}\n\n”) # 写入章节内容 f.write(“-” * 50 + “\n\n”) # 章节分隔线 logger.info(f”章节 [{title}] 抓取完成。”) logger.info(f”小说爬取完成!已保存至: {output_file}”) # 使用示例 if __name__ == ‘__main__’: # 替换成你要爬取的小说的实际目录页URL catalog_url = ‘https://b.faloo.com/f/1234567.html’ spider = FeiluNovelSpider() spider.crawl(catalog_url, output_file=‘我的飞卢小说.txt’)关键点解读:
enumerate(chapters, 1): 在遍历章节列表时获取索引,1表示索引从1开始,方便日志显示进度(如[1/100])。with open(...) as f: 使用with上下文管理器打开文件,可以确保在任何情况下(即使程序异常中断)文件都能被正确关闭。encoding=‘utf-8’: 指定以UTF-8编码写入文件,这是最通用的中文编码,能确保在任何设备上打开都不会乱码。- 进度与状态日志:在关键步骤(开始、抓取每个章节、完成)都输出日志,这对于长时间运行的爬虫至关重要。当程序因网络中断而停止时,你可以通过日志知道抓取到了哪一章,下次可以从断点续爬。
5. 高级话题:反爬应对、效率优化与伦理边界
一个只能在自己电脑上跑一次的爬虫是玩具,一个能稳定、高效、可持续运行的爬虫才是工具。这就涉及到更高级的话题。
5.1 应对更复杂的反爬机制
如果网站加强了反爬,我们可能会遇到以下情况,需要升级我们的爬虫:
- IP封禁:这是最常见的反爬手段。解决方案是使用代理IP池。你可以购买付费的代理服务,或者使用一些免费的代理(但稳定性差)。在代码中,可以通过为
requests.Session或单次请求配置proxies参数来使用代理。proxies = {‘http’: ‘http://your-proxy-ip:port’, ‘https’: ‘https://your-proxy-ip:port’} resp = self.session.get(url, proxies=proxies) - 请求频率限制:即使有代理,过快的请求频率也会触发风控。除了在代码中设置随机延时,更科学的做法是遵守
robots.txt协议,并设置一个合理的、尊重服务器的爬取间隔(例如,每请求一次等待3-5秒)。 - JavaScript验证与动态加载:如前所述,当
requests无法获取到有效内容时,就需要动用Selenium或Playwright。你需要编写控制浏览器点击、滚动、等待元素加载的代码。这会使爬虫速度大幅下降,但能解决最棘手的问题。 - 验证码:遇到验证码就比较麻烦了。对于简单的图形验证码,可以使用OCR库(如
ddddocr、tesseract)尝试识别。对于复杂的点选、滑块验证码,通常需要接入打码平台(人工或AI识别),成本会急剧上升。个人爬虫项目如果遇到验证码,最务实的建议是考虑是否值得继续,或者寻找其他数据源。
5.2 效率优化:从单线程到异步
上述代码是单线程的,抓取100章小说可能需要几十分钟。对于大量数据的抓取,效率是关键。我们可以采用异步IO(asyncio+aiohttp)来并发发送请求,理论上可以将速度提升数十倍。
异步爬虫的编写比同步复杂,核心思想是创建一个任务列表(每个章节一个抓取任务),然后用一个异步的HTTP客户端(aiohttp.ClientSession)同时发起多个请求,并等待所有结果返回。这需要对Python的异步编程有基本了解。对于新手,我建议先确保同步版本稳定运行,再考虑异步优化。
5.3 不可逾越的伦理与法律边界
这是所有技术从业者必须严肃对待的一课。爬虫技术是一把双刃剑。
- 遵守
robots.txt:这是网站所有者表明爬虫访问意愿的文件。访问https://b.faloo.com/robots.txt可以查看飞卢小说网对爬虫有哪些限制。尊重这些规则是基本的网络礼仪。 - 限制爬取速度:不要对目标服务器进行DoS攻击式的疯狂请求。你的爬虫行为不应该影响网站的正常服务。
- 明确数据用途:本项目仅限个人学习、研究使用。绝对禁止将爬取到的小说内容用于任何商业用途,包括但不限于:
- 重新发布到其他网站或App。
- 制作成盗版电子书进行销售。
- 用于AI模型训练(除非获得明确授权)。
- 尊重版权:小说是作者和平台的智力财产。爬取行为本身可能游走在法律边缘,但将内容用于商业分发或大规模传播则明确构成侵权。
- 规避技术保护措施:如果网站采用了明确的技术手段(如加密、混淆)来防止爬取,强行破解可能违反相关法律法规。
我的个人原则是:爬虫技术用于学习原理、自动化处理个人可公开访问的数据是合理的。但一旦涉及版权内容、商业数据或个人隐私,就必须慎之又慎,最好寻求合法授权。技术让我们有能力做很多事,但智慧和操守决定我们应该做哪些事。
6. 项目总结与个人实操心得
回顾整个项目,从环境搭建、页面分析、代码编写到反爬应对,是一个完整的Web爬虫开发流程。飞卢小说网作为一个中等难度的目标,很好地锻炼了HTML解析、请求模拟、异常处理和流程设计的能力。
我踩过最大的坑,不是技术,而是“想当然”。最初我以为目录页的解析规则半小时就能搞定,结果因为网站前端用了多层嵌套的div和动态加载,花了整整一个下午才找到稳定的选择器。教训就是:一定要用浏览器的开发者工具,把HTML结构看清楚、看完整,不要只看一眼就写代码。对于重要的元素,多用几种选择器(find,find_all, CSS选择器)尝试,并做好找不到元素时的异常处理。
另一个心得是关于“健壮性”。最初的脚本没有重试机制,网络一波动就全盘崩溃。加上指数退避重试和详细日志后,脚本的稳定性大大提升,甚至可以晚上挂着跑,第二天早上直接看结果。日志是你的眼睛,一定要把关键步骤(开始、成功、失败、失败原因)都记录下来。
最后,关于这个脚本的用途,我再次强调,它是我为了在通勤地铁上(网络不好)看一本感兴趣的小说而写的,生成的文件只存在于我自己的硬盘里。技术的学习过程很有趣,但请务必将它用在正途。如果你对爬虫技术本身感兴趣,我建议可以从一些公开的、允许爬取的数据源开始练习,比如各大高校的公开数据集、政府开放数据平台等,那里有海量的、合法的数据等着你去探索和分析。