ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python网络爬虫实战:基于笔趣阁的小说内容抓取与数据清洗

Python网络爬虫实战:基于笔趣阁的小说内容抓取与数据清洗 1. 项目缘起与核心价值最近在技术社区和几个做数据分析的朋友聊天发现一个挺有意思的现象很多朋友想用Python做点文本分析或者自然语言处理的练手项目第一步就卡在了“数据从哪里来”这个问题上。网上的公开语料库要么领域太专要么格式死板远不如网络小说这种叙事性文本来得生动、丰富。恰好我前段时间因为一个研究项目需要分析网络文学的叙事结构和词汇使用习惯就动手写了一个专门针对“笔趣阁”这类小说网站的爬虫。今天我就把这个爬虫项目的核心思路、踩过的坑以及一些实用的技巧分享出来。这个项目的目的很明确合法、合规、有限度地爬取笔趣阁网站上的公开小说章节内容用于个人学习与技术研究比如分析作者的写作风格、统计高频词汇、或者作为机器学习模型的训练语料。我必须强调任何网络爬虫行为都必须严格遵守网站的robots.txt协议尊重版权控制访问频率绝不能用于商业目的或对目标网站造成负担。我们在这里探讨的纯粹是技术实现方案。为什么选择笔趣阁作为示例一方面它的结构相对典型是很多小说网站的常见模板掌握了爬取它的方法举一反三到其他类似站点会容易很多另一方面其页面HTML结构清晰对于初学者理解爬虫的核心流程——请求、解析、提取、存储——非常有帮助。当然我们只爬取“部分”公开内容用于演示技术原理这也是标题中“部分”二字的含义。2. 项目整体设计与核心思路拆解一个完整的爬虫项目远不止写几行requests和BeautifulSoup那么简单。在动手敲代码之前我们必须像一个架构师一样把整个流程和可能遇到的问题都想清楚。我的设计思路主要围绕以下几个核心环节展开。2.1 目标分析与技术选型首先我们需要明确爬取的目标是什么。以笔趣阁上一本小说为例我们的终极目标是按顺序获取其所有章节的标题和正文。这个过程可以拆解为获取小说目录页这个页面包含了所有章节的链接列表。解析目录页提取章节链接从目录页的HTML中找出每一个章节对应的URL。遍历章节链接访问并解析每一章逐个请求章节页面。从章节页面提取标题和正文这是数据提取的核心。持久化存储数据将爬取到的内容保存下来比如存为TXT文件或写入数据库。基于这个流程技术选型就清晰了请求库requests。简单易用功能强大是Python社区进行HTTP请求的事实标准。对于更复杂的、需要处理大量异步请求或反爬严格的场景可能会考虑aiohttp或Scrapy框架但对我们这个“部分爬取”的练手项目来说requests绰绰有余。解析库BeautifulSoup。同样是解析HTML/XML的利器语法直观学习成本低。它的选择器虽然不如lxml的XPath表达式在某些场景下精准高效但对于笔趣阁这种结构规整的页面用起来非常顺手。PyQueryjQuery风格也是一个不错的选择但考虑到BeautifulSoup更普及本文以其为例。存储方案本地文本文件。为了简化演示我们将每章内容保存为一个独立的.txt文件文件名用章节标题命名。这对于后续的文本分析处理非常友好。如果数据量大可以考虑SQLite或MySQL。2.2 反爬策略考量与伦理边界这是爬虫项目中最需要谨慎对待的部分。我们必须假设网站是不欢迎爬虫的并做好应对。遵守robots.txt首先访问目标网站的robots.txt通常在网站根目录如https://www.biquge.com.cn/robots.txt查看是否允许爬虫访问我们目标目录。这是网络礼仪也是法律风险规避的第一步。设置请求头这是最基本的伪装。我们需要让我们的请求看起来像一个普通的浏览器访问。核心是设置User-Agent还可以加上Referer、Accept-Language等字段。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 }控制访问频率这是对目标网站最基本的尊重也是避免自己IP被封锁的关键。绝不能在循环中无延迟地疯狂请求。必须在每次请求之间加入随机延时。import time import random time.sleep(random.uniform(1, 3)) # 随机休眠1到3秒处理异常网络是不稳定的页面结构也可能变动。必须用try...except块包裹核心请求和解析代码处理可能出现的连接超时、状态码异常、解析元素找不到等情况并做好日志记录。明确爬取边界我们只爬取用于个人学习研究的少量、公开数据。不爬取VIP章节、不尝试绕过付费墙、不进行大规模全站爬取。这是技术人的操守。3. 核心细节解析与实操要点有了整体设计我们来深入每个环节的魔鬼细节。这些细节往往是爬虫能否稳定运行的关键。3.1 目录页解析与链接提取的稳定性目录页是我们整个爬虫的“地图”。这张地图必须准确无误地拿到。定位章节列表容器使用浏览器的开发者工具F12查看目录页的HTML结构。通常所有章节的链接会包裹在一个div或ul标签中这个标签会有唯一的id或class属性。比如可能是一个idlist的div或者一个classlistmain的dl标签。我们的第一步就是用BeautifulSoup的find()或find_all()方法定位到这个容器。注意不要依赖标签的文本内容如“章节列表”来定位要依赖其稳定的id或class属性。网站前端改版时样式类名可能变化但功能性的id往往更稳定。提取所有a标签在定位到的容器内寻找所有的a超链接标签。这里需要仔细甄别因为容器里可能不仅包含正文章节链接还可能有“作品相关”、“前言”等无关链接。需要观察其规律正文章节的链接通常有规律的href属性并且其文本内容就是章节标题。# 假设章节链接都在 idlist 的div下的所有a标签里 chapter_list soup.find(div, idlist).find_all(a) # 但这样可能抓到太多链接需要进一步过滤链接清洗与补全从a标签的href属性提取出的链接很可能是相对路径如/book/123/1.html。我们需要将其与网站的基础URL拼接形成完整的绝对URL以便后续请求。base_url https://www.biquge.com.cn for link in chapter_links: chapter_url base_url link[href] # 拼接完整URL实操心得有些网站的目录页是分页的或者使用了JavaScript动态加载。如果发现HTML中找不到章节链接但在浏览器里能看到那很可能遇到了JS渲染问题。这时简单的requestsBeautifulSoup就无能为力了需要考虑使用Selenium或Pyppeteer这类能模拟浏览器行为的工具。幸运的是经典的笔趣阁模板站通常没有这么复杂。3.2 正文内容提取的精准度进入章节页面后我们的目标是精准地提取标题和正文剔除导航栏、广告、版权声明等所有杂质。标题提取标题通常位于h1标签内或者是一个具有特定class的div。直接用soup.find(h1).text获取文本即可。但务必做好异常处理因为有些页面结构可能略有不同。title_tag soup.find(h1) if title_tag: chapter_title title_tag.text.strip() else: # 备用方案查找其他可能的标题标签或class chapter_title 未知章节正文定位与清洗这是最核心也最容易出错的步骤。定位正文容器和目录页一样使用开发者工具找到包裹正文的那个div。它通常有非常明显的特征比如idcontent、classcontent或idhtmlContent等。用soup.find(div, idcontent)来定位。处理特殊字符网络小说正文里常常包含大量的HTML实体字符如nbsp;空格、lt;、gt;等。BeautifulSoup在解析时通常会将其转换为普通字符但为了保险起见我们可以使用Python的html模块进行unescape处理。from html import unescape content_text unescape(content_div.text)清洗无用文本正文容器里除了小说文字可能还掺杂着“笔趣阁”、“请记住本站地址”、“上一页/下一页”等垃圾文本。这些文本往往有固定的模式。我们可以用正则表达式re.sub()或简单的字符串替换str.replace()将其清除。import re # 清除常见的干扰文字 cleaned_content re.sub(r[\s]*笔趣阁[\s]*|请记住.*?地址|\(本章完\), , content_text) # 将多个连续换行符替换为一个 cleaned_content re.sub(r\n, \n, cleaned_content)分段处理原始HTML中的段落通常用br/或p标签分隔。当我们用.text属性获取全部文本时段落信息会丢失变成一大段文字。一个更好的方法是遍历正文容器内的所有文本节点和br/标签手动构建带换行的文本这样能保留原始段落结构便于阅读。3.3 健壮性编码与错误处理一个只能跑一次的爬虫是玩具一个能处理各种异常、断点续爬的爬虫才是工具。网络请求异常处理使用requests时必须处理连接超时、请求被拒绝、返回非200状态码等情况。try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 response.encoding response.apparent_encoding # 自动判断编码 except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误: {e}) # 可以选择记录到日志文件然后跳过本章节或终止程序 return None解析异常处理在解析HTML时你假设存在的标签可能并不存在。content_div soup.find(div, idcontent) if content_div is None: print(f警告在页面 {url} 中未找到正文容器尝试备用选择器...) content_div soup.find(div, class_content) # 备用方案 if content_div is None: print(f错误无法解析页面 {url} 的正文已跳过。) return None文件操作异常在写入文件时要处理路径不存在、文件被占用、磁盘已满等问题。import os save_dir ./novels os.makedirs(save_dir, exist_okTrue) # 确保目录存在 file_path os.path.join(save_dir, f{chapter_title}.txt) try: with open(file_path, w, encodingutf-8) as f: f.write(cleaned_content) print(f已保存: {chapter_title}) except IOError as e: print(f文件写入失败: {file_path}, 错误: {e})进度保存与断点续爬爬取大量章节时程序可能因网络或自身原因中断。一个良好的实践是维护一个进度文件如progress.json每成功爬取一章就记录下该章的URL或序号。下次启动时先读取进度文件跳过已爬取的部分。这比每次从头开始要友好得多。4. 完整实操过程与核心代码实现下面我将结合一个模拟的笔趣阁页面结构展示一个完整的、可运行的爬虫脚本。请注意以下代码中的URL和HTML结构是示例你需要根据目标网站的实际结构进行调整。4.1 环境准备与依赖安装确保你的Python环境建议3.6以上已安装必要的库。pip install requests beautifulsoup4 lxml这里安装了lxml解析器因为BeautifulSoup使用lxml作为后端解析时速度更快、容错性更好。4.2 核心爬虫脚本分步实现我们将脚本拆分成几个函数使逻辑更清晰。import requests from bs4 import BeautifulSoup import time import random import re from html import unescape import os # 全局配置 BASE_URL https://www.examplebiquge.com # 替换为目标网站基础URL BOOK_URL f{BASE_URL}/book/123456/ # 替换为具体小说目录页URL 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 } SAVE_DIR ./novel_chapters os.makedirs(SAVE_DIR, exist_okTrue) def get_soup(url): 发送请求并返回BeautifulSoup对象包含异常处理 try: resp requests.get(url, headersHEADERS, timeout15) resp.raise_for_status() # 优先使用响应头或meta标签声明的编码失败则用apparent_encoding resp.encoding resp.apparent_encoding return BeautifulSoup(resp.text, lxml) except requests.exceptions.RequestException as e: print(f[ERROR] 请求 {url} 失败: {e}) return None def parse_directory(soup): 解析目录页提取所有章节的标题和链接 chapters [] # 关键步骤找到章节列表的容器。这里需要你手动分析目标网页HTML结构。 # 示例假设所有章节链接都在一个id为list的div下的dd标签的a标签里 list_div soup.find(div, idlist) if not list_div: print(未找到目录列表容器请检查HTML结构。) return chapters # 查找所有包含章节链接的a标签 # 注意可能需要更精确的选择器来过滤掉非正文链接如“作品相关” link_tags list_div.find_all(a, hrefre.compile(r/\d\.html$)) # 示例匹配以数字.html结尾的链接 for tag in link_tags: chapter_title tag.text.strip() chapter_href tag[href] # 确保链接是绝对路径 if not chapter_href.startswith(http): chapter_url BASE_URL chapter_href if chapter_href.startswith(/) else BOOK_URL.rstrip(/) / chapter_href else: chapter_url chapter_href chapters.append({title: chapter_title, url: chapter_url}) return chapters def parse_chapter(soup, url): 解析单个章节页面提取标题和正文 # 提取标题 title_tag soup.find(h1) if title_tag: chapter_title title_tag.text.strip() else: # 备用方案查找其他可能的标题元素 title_tag soup.find(div, class_bookname).find(h1) if soup.find(div, class_bookname) else None chapter_title title_tag.text.strip() if title_tag else 未知章节_str(int(time.time())) # 提取正文 - 这是最需要根据目标网站调整的部分 content_div soup.find(div, idcontent) if not content_div: # 尝试其他常见的正文容器ID或Class content_div soup.find(div, class_content) if not content_div: print(f[WARN] 在页面 {url} 中未找到正文容器。) return chapter_title, None # 方法一直接获取文本但会丢失段落结构 # raw_content content_div.get_text(separator\n, stripTrue) # 方法二推荐手动处理保留换行。处理br标签为换行符。 content_text for elem in content_div.descendants: if isinstance(elem, str): content_text elem.strip() elif elem.name br: content_text \n # 清洗内容 content_text unescape(content_text) # 处理HTML实体 # 使用正则表达式移除特定广告文本根据实际情况调整模式 content_text re.sub(r[\s]*笔趣阁[\s]*|请记住.*?地址|\(本章完\)|一秒记住.*?笔趣阁, , content_text) # 合并过多的空白行 content_text re.sub(r\n\s*\n, \n\n, content_text) content_text content_text.strip() return chapter_title, content_text def save_chapter(title, content, index): 保存章节到文件 if not content: print(f[SKIP] 章节 {title} 内容为空跳过保存。) return False # 清理文件名中的非法字符 safe_title re.sub(r[\\/*?:|], _, title) filename f{index:04d}_{safe_title}.txt # 用序号前缀保证顺序 filepath os.path.join(SAVE_DIR, filename) try: with open(filepath, w, encodingutf-8) as f: f.write(f{title}\n\n{content}) print(f[SAVED] {filename}) return True except IOError as e: print(f[ERROR] 保存文件 {filename} 失败: {e}) return False def main(): 主函数控制整个爬取流程 print(开始爬取小说目录...) soup get_soup(BOOK_URL) if not soup: print(获取目录页失败程序退出。) return chapters parse_directory(soup) if not chapters: print(未解析到任何章节链接请检查parse_directory函数的选择器。) return print(f共发现 {len(chapters)} 个章节。) # 可以在这里设置爬取的起始和结束位置用于部分爬取 start_idx 0 end_idx min(10, len(chapters)) # 示例只爬前10章 for i, chapter_info in enumerate(chapters[start_idx:end_idx], startstart_idx1): print(f正在处理第 {i} 章: {chapter_info[title]}) chapter_soup get_soup(chapter_info[url]) if not chapter_soup: continue title, content parse_chapter(chapter_soup, chapter_info[url]) save_chapter(title, content, i) # 关键请求间隔避免给服务器造成压力 delay random.uniform(2, 5) # 随机等待2-5秒 print(f等待 {delay:.2f} 秒...) time.sleep(delay) print(爬取任务完成) if __name__ __main__: main()4.3 脚本使用与定制说明替换关键变量你需要将脚本中的BASE_URL、BOOK_URL替换成你目标小说的真实地址。HEADERS中的User-Agent也可以定期更新模拟不同浏览器。调整解析函数parse_directory和parse_chapter函数中的选择器如find(div, idlist)是示例代码的核心必须根据目标网站的实际HTML结构进行修改。使用浏览器开发者工具F12的“检查元素”功能仔细查看目录列表和正文区域的HTML标签和属性。控制爬取范围通过修改main()函数中的start_idx和end_idx变量可以轻松控制从第几章开始爬取到第几章结束实现“部分爬取”。调整请求频率random.uniform(2, 5)这个延迟时间可以根据目标网站的反爬力度和自身需求调整。更保守的做法可以延长到5-10秒。5. 常见问题排查与实战技巧实录即使代码逻辑正确在实际运行中你依然会遇到各种问题。下面是我在多次爬虫项目中总结出的典型问题及其解决方案。5.1 编码问题乱码的根源与解决中文网站最常遇到的问题就是乱码。现象爬取的文本显示为“锟斤拷”或一堆乱码。原因HTTP响应内容的编码如GBK、UTF-8与解析时使用的编码不一致。解决方案让requests自动判断resp.encoding resp.apparent_encoding这行代码让requests库根据内容推测编码在大多数情况下是有效的。手动指定如果自动判断不准可以查看网页源码中meta charset...标签的声明或者在浏览器开发者工具的“Network”选项卡中查看响应头部的Content-Type字段里面会包含charset信息。然后手动设置resp.encoding gbk或utf-8。终极方案如果以上都不行可以将响应内容以二进制形式(resp.content)保存下来然后用chardet库检测编码再解码。import chardet raw_data resp.content encoding chardet.detect(raw_data)[encoding] html raw_data.decode(encoding, errorsignore)5.2 反爬虫机制请求被拒绝或返回验证页现象返回的状态码是403、404或者返回的HTML内容是一个验证页面要求输入验证码、提示“访问过于频繁”。原因与对策现象可能原因应对策略403 Forbidden1. 缺少或User-Agent被识别。2. IP被暂时封锁。1. 完善请求头添加Referer、Accept等字段。2. 使用time.sleep()大幅增加延迟或更换IP对于学习项目建议优先延长等待时间。返回验证页面网站启用了简单的反爬如基于请求频率。1.严格遵守爬虫礼仪大幅降低请求频率time.sleep(random.uniform(5, 15))。2. 模拟更完整的浏览器会话在headers中添加Cookie谨慎使用注意隐私。3. 如果必须爬取考虑使用更专业的工具如Scrapy配合中间件或使用付费代理IP池但这已超出个人学习范畴。核心原则对于个人学习项目一旦遇到反爬首先应该检查自己的行为是否对网站造成了负担并立即停止或极大地放缓爬取速度。技术探索应在法律和道德的框架内进行。5.3 页面结构变动昨天还能跑今天就不行了现象解析函数报错找不到对应的HTML元素返回None。原因网站前端页面改版HTML的标签、id、class发生了变化。解决方案代码鲁棒性在find或find_all之后一定要判断返回结果是否为None并准备好备用选择器就像我们在parse_chapter函数里做的那样。使用更通用的选择器如果原来的idcontent变成了classcontent我们可以尝试用soup.select_one(div.content)CSS选择器来定位它可能更灵活。动态调整策略如果变化很大需要重新分析页面结构更新解析逻辑。这也是为什么将解析逻辑单独写成函数的好处——只需修改一处。5.4 数据清洗不彻底保存的文件里仍有垃圾文本现象正文中混杂着“手机用户请访问m.”、“推荐票”、“上一章/下一章”等无关文字。对策这需要仔细观察这些垃圾文本的模式。观察规律这些文本通常是固定的或者有固定的关键词。加强清洗在清洗步骤中增加更多的正则表达式替换模式。可以先将爬取的几章内容打印出来仔细查看残留的垃圾文本是什么然后将其添加到清洗规则中。# 一个更强大的清洗函数示例 def clean_text(text): patterns_to_remove [ r笔趣阁.*?, r请收藏.*?, r\(.*?推荐票.*?\), r记住.*?网址, rm\..*?, r电脑版.*?, r章节错误.*?, r\(本章完\), ] for pattern in patterns_to_remove: text re.sub(pattern, , text, flagsre.IGNORECASE) # 清理多余空行 text re.sub(r\n\s*\n, \n\n, text) return text.strip()分段处理如果垃圾文本总是出现在正文的头部或尾部可以考虑在按行分割文本后删除前N行和后M行。5.5 性能与效率爬得太慢怎么办对于成百上千章的小说串行爬取加上延时总时间会很长。初级优化适当减少固定延时但必须保证在合理范围内如不低于2秒。使用random.uniform()增加随机性避免规律访问被识别。进阶方案如果需要大量爬取且确认在法律和网站规则允许范围内可以考虑并发/异步请求使用concurrent.futures的ThreadPoolExecutor实现多线程或者使用aiohttpasyncio实现异步IO。但务必注意并发会极大增加服务器压力必须设置全局速率限制否则极易导致IP被封。使用专业框架Scrapy框架内置了异步处理、自动限速、中间件、管道等强大功能是大型爬虫项目的首选。但对于“部分爬取”的学习目标requestsBeautifulSoup的组合更加轻量和直观。最后我想分享一点个人体会。爬虫技术本身是一把双刃剑它强大的数据获取能力背后对应着同等的责任。在动手写任何爬虫之前我都会问自己三个问题我的目的是否纯粹学习/研究我的行为是否会对目标网站造成显著影响我是否尊重了数据的版权和隐私想清楚这些再让代码跑起来。这个笔趣阁小说爬虫项目从技术上看是理解HTTP协议、HTML解析、数据清洗和流程控制的绝佳练习场从实践上看它更是培养我们合规意识、工程思维和解决问题能力的试金石。希望这份详细的拆解能帮你不仅写出能跑的代码更能写出健壮、优雅、负责任的代码。如果在实现过程中遇到新的具体问题不妨再回头看看“常见问题”部分或者拿起开发者工具重新审视一下网页结构——很多时候答案就藏在细节里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进