Python网络爬虫实战:从搜索到下载构建个人小说图书馆

📅 2026/8/14 10:47:14
Python网络爬虫实战:从搜索到下载构建个人小说图书馆
1. 项目概述从搜索到下载构建你的个人小说图书馆每次在网上追更小说最烦人的是什么对我来说不是剧情拖沓而是平台的各种限制需要登录、有广告弹窗、阅读界面不友好或者最头疼的——想看的小说分散在各个网站没有统一的阅读体验。作为一个Python爱好者和重度小说读者我决定自己动手解决这个问题。今天要分享的就是如何用Python打造一个专属于你的“笔趣阁小说搜索与爬取工具”。这个工具的核心目标很简单输入一个小说名字它能自动在笔趣阁这类小说网站上搜索到相关结果并帮你把整本书的章节内容爬取下来整理成干净的TXT或EPUB格式存入你的本地书架。这不仅仅是写几行requests和BeautifulSoup代码那么简单。一个健壮、好用的小说爬虫需要考虑反爬策略、编码处理、章节链接的发现与去重、内容清洗以及断点续传等实际问题。网上很多教程只教了最基础的静态页面抓取一旦遇到动态加载、登录验证或者复杂的反爬机制就束手无策了。我将结合我多次“踩坑”的经验带你从零开始构建一个既能应对常见反爬措施又具备良好用户体验的爬虫项目。无论你是Python新手想找个有趣的实战项目还是有一定基础想深入理解网络爬虫的各个环节这篇文章都能给你提供一条清晰的路径和一堆可以直接“抄作业”的代码。2. 核心思路与架构设计为什么是“搜索爬取”两步走在开始写代码之前我们先要理清整个工具的工作流程。一个完整的小说获取流程可以抽象为“发现”和“获取”两个阶段。“发现”对应搜索功能用户输入关键词如“诡秘之主”我们需要在目标网站如笔趣阁的搜索接口或页面中找到所有相关书籍并提取出每本书的详情页链接。“获取”阶段则是根据选定的书籍详情页链接解析出所有章节的链接然后按顺序爬取每个章节的正文内容最后进行整合。2.1 为何选择“笔趣阁”作为目标站点在众多小说网站中笔趣阁这里指其众多镜像站之一技术原理相通是一个经典的学习案例。它结构相对简单小说资源丰富且通常是静态页面对于初学者理解HTML解析和请求流程非常友好。但请注意其背后也涉及版权等复杂问题我们这里仅从技术学习角度进行探讨爬取的内容请务必限于个人学习研究之用切勿用于商业传播。从技术上看这类网站的反爬手段通常包括简单的请求头校验、IP访问频率限制、以及通过JavaScript生成的动态参数等初级防护。攻克它们能很好地锻炼我们处理常见反爬问题的能力。2.2 工具的核心架构拆解我设计的这个工具主要包含以下几个模块搜索模块负责接收用户输入模拟浏览器向网站的搜索功能发起请求解析返回的HTML提取书籍列表包括书名、作者、最新章节、详情页URL。书籍详情解析模块根据用户选择的书籍进入其详情页。这个页面的核心是“章节列表”。我们需要从这里提取所有章节的标题和链接。这里的关键在于有些网站的章节列表是分页的我们需要能自动发现并遍历所有分页。章节内容爬取模块这是核心中的核心。它需要循环遍历章节链接列表逐个请求并解析出纯净的正文文本。必须加入健壮的错误处理如网络超时、页面结构变化和礼貌的爬取延迟设置time.sleep避免对服务器造成压力也防止自己的IP被封锁。内容清洗与存储模块爬取到的原始HTML通常包含大量广告、无关的脚本和样式标签。我们需要用正则表达式或解析库精准地提取出正文部分并去除多余的空白字符、特殊符号。最后将清洗后的章节按顺序保存为文件如TXT按章节分隔或整合成一个EPUB。控制与交互模块一个简单的命令行界面CLI或图形界面GUI让用户能够输入关键词、选择书籍、查看进度。对于高级版本还可以考虑加入数据库来管理已爬取的书目实现断点续爬记录上次爬到的章节位置。这个架构的优势在于模块化每个部分职责清晰方便调试和扩展。例如如果你想更换目标网站理论上只需要替换搜索和解析模块中的URL规则和HTML解析逻辑即可。3. 关键技术点与工具选型手把手搭建开发环境工欲善其事必先利其器。下面我列出这个项目所需的核心Python库并解释为什么选择它们。requests这是HTTP请求的绝对主力。相比Python内置的urllibrequests的API更加人性化会话保持、代理设置、超时处理都非常方便。我们将用它来发送搜索请求和抓取章节页面。BeautifulSoup4 (bs4)HTML解析神器。当requests拿到网页的HTML源代码后我们需要从中提取出特定的标签和内容。BeautifulSoup支持多种解析器如lxml,html.parser能让我们像操作DOM树一样方便地找到需要的书名、链接和正文。lxml一个高性能的XML/HTML解析库。虽然BeautifulSoup可以用html.parser但lxml的解析速度更快通常作为bs4的后端解析器使用。安装bs4时最好一并安装lxml。fake-useragent一个用来生成随机、真实用户代理User-Agent字符串的库。这是绕过基于User-Agent反爬的最基本、最有效的方法之一。每次请求使用不同的浏览器标识能让我们的爬虫看起来更像普通用户在浏览。可选selenium/playwright如果目标网站的搜索列表或章节列表是通过JavaScript动态加载的即右键查看网页源代码看不到数据那么单纯的requests就无能为力了。这时需要用到浏览器自动化工具。selenium比较传统playwright是后起之秀功能更强大。它们能控制真实浏览器渲染页面等数据加载完成后再获取HTML。不过它们更重速度也慢除非必要否则优先尝试分析网站的Ajax接口。可选ebooklib如果你想把小说制作成EPUB格式的电子书这个库必不可少。它可以用来创建和编辑EPUB文件的结构目录、章节、元数据。注意在实际操作中请务必遵守目标网站的robots.txt协议虽然很多小说网站可能没有或限制严格并合理设置爬取延迟例如每爬取一个章节后time.sleep(1-3秒)。过于频繁的请求不仅是非礼貌的也极易导致你的IP被临时或永久封禁。安装这些库非常简单打开你的命令行终端或CMD使用pip一键安装pip install requests beautifulsoup4 lxml fake-useragent # 如果需要动态渲染再安装 # pip install selenium # 或者 # pip install playwright # playwright install chromium # 安装浏览器驱动4. 实战第一步实现精准的小说搜索功能我们以某个笔趣阁镜像站为例请注意具体网址可能随时变动这里主要讲解原理和代码模式。假设它的搜索URL格式为https://www.biquge.com.cn/search?q{关键词}。4.1 构造请求与处理反爬直接使用requests.get()可能会被网站识别为爬虫。我们需要伪装成一个普通的浏览器请求。import requests from fake_useragent import UserAgent import time def search_novel(keyword): 根据关键词搜索小说 :param keyword: 小说名称或作者 :return: 书籍列表每个元素是包含书名、作者、链接等信息的字典 base_url https://www.biquge.com.cn search_url f{base_url}/search?q{keyword} # 1. 使用随机的User-Agent ua UserAgent() headers { User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,zh-TW;q0.7,zh-HK;q0.5,en-US;q0.3,en;q0.2, Referer: base_url, # 模拟从主页跳转过来 } # 2. 发送请求加入超时和重试机制 try: # 这里可以加入代理IP逻辑如果需要的话 # proxies {http: http://your_proxy:port, https: https://your_proxy:port} response requests.get(search_url, headersheaders, timeout10) #, proxiesproxies) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 # 检查编码很多中文网站是gbk或gb2312 response.encoding response.apparent_encoding # 或者直接指定 gbk html_content response.text except requests.exceptions.RequestException as e: print(f搜索请求失败: {e}) return [] # 3. 解析HTML提取书籍信息 from bs4 import BeautifulSoup soup BeautifulSoup(html_content, lxml) book_list [] # 假设搜索结果的书籍信息在 classbook-list 的div下的各个li标签中 # 实际需要根据目标网站的HTML结构进行调整这里只是示例 items soup.select(div.book-list li) for item in items: try: title_tag item.select_one(h3 a) # 书名链接 author_tag item.select_one(span.author) # 作者 latest_chapter_tag item.select_one(span.latest-chapter a) # 最新章节 link title_tag[href] if title_tag else None if link and not link.startswith(http): # 处理相对链接 link base_url link book_info { title: title_tag.get_text(stripTrue) if title_tag else 未知, author: author_tag.get_text(stripTrue) if author_tag else 未知, latest_chapter: latest_chapter_tag.get_text(stripTrue) if latest_chapter_tag else 未知, link: link, source: 笔趣阁 } book_list.append(book_info) except Exception as e: # 某个条目解析出错跳过不影响其他 print(f解析书籍条目时出错: {e}) continue # 4. 礼貌性延迟避免短时间内连续搜索 time.sleep(1) return book_list关键点解析fake_useragent每次调用ua.random生成一个不同的主流浏览器UA大大降低被屏蔽的风险。headers除了UAAccept和Referer也是常见的校验字段模拟得越像浏览器越好。response.encoding中文网站常出现乱码问题。apparent_encoding是requests库推测的编码有时不准。如果发现是乱码可以手动指定response.encoding gbk或utf-8具体看网页源码里的meta charset标签。BeautifulSoup选择器select方法使用CSS选择器语法比find_all更简洁直观。select_one返回第一个匹配项。这里的选择器路径div.book-list li是示例你必须用浏览器的开发者工具F12查看目标网站的实际结构来修改。这是爬虫编写中最关键的一步。4.2 处理动态加载的搜索页如果你发现用上面的方法获取的html_content里根本没有搜索结果那很可能数据是通过JavaScript异步加载的。这时你有两个选择寻找隐藏的API接口打开浏览器的开发者工具切换到“网络”(Network)选项卡清空记录然后进行一次搜索。在产生的网络请求中仔细寻找类型为XHR或Fetch的请求其响应体Response里通常包含了结构化的数据如JSON。直接模拟这个请求会更高效、更稳定。这是首选方案。使用Selenium/Playwright渲染如果找不到清晰的接口或者接口参数加密复杂那就只能出动浏览器自动化工具了。代码如下以Selenium为例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 def search_novel_with_selenium(keyword): options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式不显示浏览器窗口 options.add_argument(--disable-gpu) # 可以继续添加其他选项来隐藏自动化特征 driver webdriver.Chrome(optionsoptions) try: driver.get(fhttps://www.biquge.com.cn/search?q{keyword}) # 等待搜索结果区域加载出来 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, div.book-list)) ) # 此时页面已渲染完成可以获取HTML html_content driver.page_source # ... 后续的解析逻辑与上面静态页面相同 ... finally: driver.quit()实操心得优先尝试分析API接口。动态渲染不仅速度慢资源消耗大而且网站很容易检测到Selenium的特征如window.navigator.webdriver属性。虽然有一些方法可以隐藏但这是一场持续的攻防战。静态解析永远是最高效、最稳定的方式。5. 实战第二步解析书籍详情与章节列表当用户从搜索列表中选择一本书后我们得到这本书的详情页URL。这个页面包含了小说简介、作者信息以及最重要的——全部章节的链接列表。5.1 解析章节列表的常见模式章节列表的HTML结构通常有两种单页长列表所有章节的链接都在一个页面里可能通过“查看更多”按钮加载但本质上HTML里已经包含了所有链接。我们只需要用BeautifulSoup一次性提取所有a标签并过滤出指向章节的链接。分页列表章节数量太多网站做了分页。详情页只显示第一页的章节底部有分页导航如“1,2,3...下一页”。我们需要先解析出总页数或“下一页”的链接规律然后循环爬取每一页合并所有章节链接。我们以常见的单页列表为例并考虑分页情况def get_chapter_list(book_url): 获取书籍详情页的所有章节链接和标题 :param book_url: 书籍详情页URL :return: 章节列表每个元素是(章节标题, 章节链接)的元组 headers {User-Agent: UserAgent().random} try: resp requests.get(book_url, headersheaders, timeout15) resp.encoding resp.apparent_encoding # 或指定编码 soup BeautifulSoup(resp.text, lxml) except Exception as e: print(f获取书籍详情页失败: {e}) return [] chapters [] # 模式1查找章节列表容器常见id或class有 listmain, chapter-list, #list 等 chapter_container soup.select_one(#list) if not chapter_container: chapter_container soup.select_one(.listmain) if not chapter_container: # 如果找不到尝试更通用的选择器或者打印soup看看结构 print(未找到章节列表容器尝试查找所有a标签并过滤) all_links soup.find_all(a, hrefTrue) # 这里需要根据目标网站章节链接的特征来过滤例如链接中包含‘/book/’和‘.html’ for link in all_links: href link[href] if /book/ in href and href.endswith(.html): chapters.append((link.get_text(stripTrue), href)) return chapters # 在容器内查找所有章节链接 chapter_links chapter_container.find_all(a, hrefTrue) for link in chapter_links: title link.get_text(stripTrue) href link[href] # 处理相对链接 if href and not href.startswith(http): # 需要根据网站URL结构拼接绝对URL这里是一种常见处理方式 from urllib.parse import urljoin href urljoin(book_url, href) # 简单的去重和过滤排除非章节链接如“返回目录”、“下一页” if title and 章 in title and href: # “章”字是一个简单过滤条件可根据情况调整 chapters.append((title, href)) # 模式2处理分页示例需要根据具体网站调整 # 查找分页控件获取总页数或“下一页”链接 pagination soup.select(.pagination a) if pagination: # 假设最后一页的页码在某个标签里或者通过“末页”链接获取 # 这里逻辑比较复杂需要具体分析可能涉及递归调用自身来爬取后续页面 pass return chapters关键点与避坑指南链接绝对化章节链接很可能是相对路径如/123/456789.html必须使用urljoin或手动拼接成完整的URL否则后续无法请求。去重与过滤章节列表里可能混入“上一页”、“目录”、“作者的话”等无关链接。需要通过链接文本title和链接地址href的特征进行过滤。正则表达式在这里很有用例如用re.search(r第[零一二三四五六七八九十百千万\d]章, title)来匹配标题。编码问题再现详情页的编码可能和搜索页不同务必确认resp.encoding设置正确否则中文标题会变成乱码。分页处理如果遇到分页不要慌。先观察分页链接的URL规律。例如第一页是/book/123/index.html第二页是/book/123/index_2.html。这样就可以用循环生成所有页面的URL然后分别解析。更复杂的情况可能需要从第一页的HTML中解析出总页数。6. 实战第三步核心爬取与内容清洗拿到了所有章节的链接列表接下来就是最激动人心的部分爬取正文内容。这一步的挑战在于稳定性和数据质量。6.1 稳健的章节内容爬取函数我们需要一个函数它接收一个章节链接返回清洗后的纯文本正文。def fetch_chapter_content(chapter_url, max_retries3): 爬取单个章节内容 :param chapter_url: 章节链接 :param max_retries: 最大重试次数 :return: 清洗后的章节正文文本失败则返回空字符串 headers {User-Agent: UserAgent().random} for attempt in range(max_retries): try: resp requests.get(chapter_url, headersheaders, timeout15) resp.raise_for_status() # 再次注意编码 resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, lxml) # 寻找正文容器。这是最需要定制化的部分 # 常见的选择器有#content, .content, #chaptercontent, .showtxt content_div soup.select_one(#content) if not content_div: content_div soup.select_one(.content) if not content_div: # 如果还找不到可能需要更复杂的查找或者网站结构已变 print(f警告在 {chapter_url} 中未找到标准正文容器尝试备用方案) # 备用方案查找包含大量文本且没有太多链接的div all_divs soup.find_all(div) for div in all_divs: if len(div.find_all(a)) 3 and len(div.get_text(stripTrue)) 500: content_div div break if not content_div: return [未成功提取正文] # 提取原始文本 raw_text content_div.get_text(separator\n, stripTrue) # 进行内容清洗 cleaned_text clean_content(raw_text) return cleaned_text except requests.exceptions.Timeout: print(f第{attempt1}次尝试请求超时: {chapter_url}) except requests.exceptions.RequestException as e: print(f第{attempt1}次尝试请求错误: {e}) except Exception as e: print(f第{attempt1}次尝试解析错误: {e}) # 重试前等待等待时间逐渐增加指数退避 if attempt max_retries - 1: wait_time (2 ** attempt) 1 # 2, 3, 5秒... print(f等待{wait_time}秒后重试...) time.sleep(wait_time) print(f章节爬取失败已重试{max_retries}次: {chapter_url}) return [爬取失败]6.2 深度内容清洗从杂乱HTML到纯净文本从div里直接get_text()得到的文本往往包含很多“杂质”网站插入的广告文字如“笔趣阁”、“手机用户请访问”、版权声明、多余的换行和空格。一个强大的清洗函数至关重要。import re def clean_content(text): 清洗章节正文文本 :param text: 原始文本 :return: 清洗后的文本 if not text: return text # 1. 移除常见的网站推广文本使用正则表达式 ad_patterns [ r笔趣阁.*?最全|最新|免费, r请收藏.*?笔趣阁, r手机用户.*?访问, r\(本章完\), rPS.*, # 移除作者PS r【.*?】, # 移除某些网站加的括号内容 # 可以根据目标网站的特征不断添加 ] for pattern in ad_patterns: text re.sub(pattern, , text, flagsre.IGNORECASE) # 2. 规范化空白字符将多个连续换行/空格合并为一个换行 text re.sub(r\n{3,}, \n\n, text) # 三个以上换行变两个 text re.sub(r[ \t], , text) # 多个空格/制表符变一个空格 text re.sub(r\n[ \t]\n, \n\n, text) # 移除空行中的空格 # 3. 处理段落首行缩进两个全角空格或四个半角空格 # 可以统一替换或保留这里选择保留但规范化 lines text.split(\n) cleaned_lines [] for line in lines: line line.strip() # 先去除首尾空白 if line: # 非空行 # 如果行首有特定缩进字符可以在这里处理 cleaned_lines.append(line) text \n.join(cleaned_lines) # 4. 移除首尾可能因广告移除而产生的多余空行 text text.strip() return text内容清洗的经验之谈正则表达式是你的利器re.sub()是清洗文本的核心。你需要仔细观察爬取到的原始文本找出那些固定出现的广告词、版权信息然后编写对应的正则模式来剔除它们。清洗顺序很重要一般先移除大块的固定广告文本再进行空白字符的规范化。否则广告被移除后留下的空白可能会干扰后续处理。保留原文格式清洗的目的是去除杂质而不是破坏原文的段落结构。谨慎处理换行符通常保留段落之间的空行一个或两个换行能使文本更易读。动态调整不同网站、甚至同一网站的不同书籍其正文容器和广告插入方式都可能略有不同。你的清洗规则可能需要一个“适配器”模式针对不同网站使用不同的清洗策略。7. 实战第四步整合、存储与进度管理现在我们已经有了搜索、获取章节列表、爬取单章内容的能力。接下来需要把这些模块串联起来并加入存储和进度显示功能。7.1 主流程控制与文件存储def main(): keyword input(请输入要搜索的小说名称或作者: ).strip() if not keyword: print(输入不能为空) return print(正在搜索...) books search_novel(keyword) if not books: print(未找到相关书籍。) return # 显示搜索结果供用户选择 for idx, book in enumerate(books, 1): print(f{idx}. 《{book[title]}》 - 作者{book[author]} - 最新{book[latest_chapter]}) try: choice int(input(f\n请选择要下载的书籍编号 (1-{len(books)}): )) selected_book books[choice - 1] except (ValueError, IndexError): print(选择无效) return print(f\n开始处理: 《{selected_book[title]}》) book_url selected_book[link] # 获取章节列表 print(正在获取章节列表...) chapters get_chapter_list(book_url) if not chapters: print(无法获取章节列表可能页面结构已变化。) return print(f共发现 {len(chapters)} 个章节。) # 询问下载范围 start_ch 1 end_ch len(chapters) range_input input(f输入下载范围 (如: 1-100, 回车则下载全部): ).strip() if range_input and - in range_input: try: start, end map(int, range_input.split(-)) start_ch max(1, start) end_ch min(len(chapters), end) chapters chapters[start_ch-1:end_ch] print(f将下载第 {start_ch} 到第 {end_ch} 章。) except ValueError: print(输入格式错误将下载全部章节。) # 创建存储目录和文件 import os safe_title re.sub(r[\\/*?:|], _, selected_book[title]) # 去除文件名非法字符 download_dir fnovels/{safe_title} os.makedirs(download_dir, exist_okTrue) txt_file_path os.path.join(download_dir, f{safe_title}.txt) # 开始爬取 print(开始爬取章节内容...) total len(chapters) success_count 0 fail_count 0 with open(txt_file_path, w, encodingutf-8) as f: f.write(f书名{selected_book[title]}\n) f.write(f作者{selected_book[author]}\n) f.write(f来源{selected_book[source]}\n) f.write(*50 \n\n) for idx, (chap_title, chap_url) in enumerate(chapters, 1): print(f进度: [{idx}/{total}] 正在下载: {chap_title}) content fetch_chapter_content(chap_url) if content and content not in [[未成功提取正文], [爬取失败]]: f.write(f第{start_ch idx - 1}章 {chap_title}\n\n) f.write(content \n\n) f.write(-*40 \n\n) success_count 1 else: f.write(f第{start_ch idx - 1}章 {chap_title} [内容获取失败]\n\n) fail_count 1 # 礼貌延迟非常重要 time.sleep(1.5) # 根据网站承受能力调整建议1-3秒 print(f\n爬取完成) print(f成功: {success_count} 章失败: {fail_count} 章) print(f文件已保存至: {txt_file_path})7.2 高级功能断点续传与状态保存对于动辄上千章的长篇小说爬取过程可能被中断网络问题、程序崩溃。实现断点续传能极大提升体验。思路很简单在爬取每个章节成功后立即将当前进度如已爬取的章节索引或URL记录到一个状态文件如progress.json中。程序启动时先检查是否存在状态文件如果存在则从中读取进度并从断点处继续爬取而不是从头开始。import json PROGRESS_FILE download_progress.json def save_progress(book_id, last_index): 保存进度 progress {book_id: last_index} with open(PROGRESS_FILE, w, encodingutf-8) as f: json.dump(progress, f) def load_progress(book_id): 加载进度返回断点索引没有则返回0 try: with open(PROGRESS_FILE, r, encodingutf-8) as f: progress json.load(f) return progress.get(book_id, 0) except FileNotFoundError: return 0 # 在主循环中整合断点续传 def download_with_resume(book_id, chapters): start_index load_progress(book_id) print(f从第 {start_index 1} 章开始续传...) for idx in range(start_index, len(chapters)): chap_title, chap_url chapters[idx] # ... 爬取和保存逻辑 ... # 爬取成功后 save_progress(book_id, idx) # 保存当前进度 time.sleep(1.5) # 全部完成后可以删除进度文件 if os.path.exists(PROGRESS_FILE): os.remove(PROGRESS_FILE)8. 常见问题排查与实战技巧实录即使代码写得再严谨在实际爬取过程中也一定会遇到各种问题。下面是我总结的一些典型问题及其解决方法。8.1 编码乱码问题现象爬取到的中文文本显示为乱码如网东文学。排查与解决检查响应头打印resp.headers[Content-Type]看是否指定了编码如charsetgbk。检查HTML元标签查看网页源码head部分是否有meta charsetgb2312或meta http-equivContent-Type contenttext/html; charsetgbk。手动指定编码如果以上明确直接设置resp.encoding gbk。如果不明确可以尝试常见的几种gbk,gb2312,utf-8。resp.apparent_encoding是requests的猜测有时不准。终极方案如果还是乱码可以使用chardet库进行检测pip install chardet。import chardet raw_data resp.content encoding chardet.detect(raw_data)[encoding] resp.encoding encoding if encoding else utf-88.2 请求被拒绝或返回403错误现象requests.get()返回状态码403 Forbidden。排查与解决请求头不足网站可能校验了User-Agent,Referer,Accept-Language,Cookie等。使用fake-useragent更新UA并补全其他常用头信息模拟得越像浏览器越好。IP被限制短时间内请求过于频繁。解决方案增加延迟在请求间加入time.sleep(random.uniform(1, 3))随机化等待时间。使用代理IP池这是应对IP封锁最有效的方法。可以购买付费代理服务或者寻找免费的代理IP列表但稳定性差。在requests.get()中传入proxies参数。降低并发如果你用了多线程/异步请减少并发数。需要Cookie或登录有些网站需要登录后才能查看内容。你可以先用浏览器手动登录然后从开发者工具的“网络”选项卡中复制Cookie字符串将其添加到请求头中。但请注意Cookie会过期。8.3 找不到HTML元素选择器失效现象soup.select_one(#content)返回None。排查与解决确认页面已正确加载先打印resp.text的前1000个字符看看是否包含预期的HTML内容。如果不包含可能是动态加载需用Selenium或者你请求的URL不对。检查选择器用浏览器的开发者工具仔细检查目标元素的id或class名。注意有些class可能有多个用空格分隔如div classcontent show此时选择器应写为.content.show。id通常是唯一的。网站结构已更新这是爬虫的常态。你需要更新你的选择器路径。编写爬虫时对于关键的选择器最好提供备用方案就像我们在fetch_chapter_content函数里做的那样。存在iframe所需内容在iframe标签内。requests获取的是主页面HTML不包含iframe内容。你需要找到iframe的src属性再对其URL发起一次请求。8.4 爬取速度慢现象爬取几千章小说需要数小时。优化方案异步爬虫使用asyncioaiohttp库可以同时发起多个请求极大提升IO密集型任务的效率。这是专业爬虫的标配。但需要注意控制并发量避免把服务器拖垮。多线程/多进程使用concurrent.futures模块的ThreadPoolExecutor。比异步简单但线程切换也有开销。缓存已解析页面如果程序需要多次调试运行可以将已成功获取的HTML页面缓存到本地文件或数据库避免重复请求。优化清洗逻辑正则表达式如果非常复杂可能会成为瓶颈。尽量使用简单的字符串操作或编译后的正则对象re.compile。8.5 存储与格式问题现象TXT文件在部分阅读器上显示格式错乱或者想生成更友好的EPUB格式。解决方案TXT格式确保文件以UTF-8编码保存。段落间用两个换行符分隔通常兼容性最好。EPUB格式使用ebooklib库。你需要创建EPUB的基本结构书名、作者、目录然后将每一章作为一个HTML文件h1标题 p段落添加到书中。这比纯TXT复杂但能在电子书阅读器上获得更好的体验目录导航、字体调整等。数据库存储如果爬取的书很多可以考虑使用SQLite或MySQL来存储。设计表结构例如books表存书目信息chapters表存章节内容便于管理和查询。最后我想强调的是爬虫技术是一把双刃剑。在享受技术带来的便利时务必保持对法律的敬畏和对他人劳动的尊重。将爬取的数据用于个人学习、研究或分析并控制好请求频率避免对目标网站的正常运行造成影响这是每一位爬虫开发者应有的素养。希望这个详细的指南能帮助你顺利构建出自己的小说书架并在过程中深刻体会到Python自动化的魅力与挑战。如果在实操中遇到新的问题不妨多看看浏览器的开发者工具多尝试几种解析思路爬虫的世界里解决问题的过程往往比结果更有趣。