【爬虫实战对比】Requests vs Scrapy 笔趣阁小说爬虫,从单线程到高效并发的全方位升级
【爬虫实战对比】Requests vs Scrapy 笔趣阁小说爬虫从单线程到高效并发的全方位升级近期完成了笔趣阁小说爬虫的重构从最初的Requests单线程版本升级为Scrapy框架版本过程中深刻体会到两者在开发效率、运行性能、代码可维护性上的巨大差异。今天就以“爬取笔趣阁指定小说前10章并保存为txt文件”为目标全方位对比两个版本的核心差异拆解重构思路分享实战中的优化细节适合爬虫新手理解框架与原生库的区别也能为大家的爬虫项目重构提供参考。先明确本次实战的核心目标目标网站为笔趣阁https://www.biquge365.net爬取指定小说对应URLhttps://www.biquge365.net/newbook/83621/的前10章内容提取章节标题和正文最终保存为txt文件确保两个版本的功能完全等价但在实现方式和性能上形成鲜明对比。在正式对比前先说明一个关键前提本次重构严格保留了原Requests版本的核心爬取逻辑——相同的XPath路径、相同的文本处理规则、相同的保存格式仅在代码结构、运行机制、性能优化上进行升级避免因功能差异影响对比的客观性。同时按要求仅附上Scrapy版本的核心爬虫文件biquge_book.py不额外添加其他配置文件聚焦核心代码的差异分析。一、先回顾Requests版本的核心实现逻辑痛点铺垫在重构为Scrapy版本之前我先用Requestslxml实现了基础版本的爬虫核心逻辑很简单就是“请求-解析-保存”的线性流程适合新手入门但在实际使用中暴露了不少痛点也正是这些痛点促使我进行重构。Requests版本的核心流程可以概括为4步1. 手动请求小说目录页获取章节列表2. 遍历章节列表逐个手动请求章节详情页3. 用lxml解析页面提取标题和正文处理空行和格式4. 手动创建文件夹将内容写入txt文件同时用计数器限制爬取前10章。这个版本虽然能实现需求但在实战中存在3个明显的痛点也是很多新手用Requests写爬虫时会遇到的问题单线程运行效率极低每请求一个章节都需要等待前一个章节请求完成遇到网络延迟时整体耗时会大幅增加爬取10章可能需要数十秒若爬取上百章耗时会呈线性增长代码耦合度高可维护性差请求、解析、保存逻辑全部写在一个函数里没有模块化拆分后续若想修改保存格式、增加爬取字段需要在大量代码中查找修改容易出错缺乏异常处理和日志记录一旦出现URL失效、解析失败、文件写入错误等问题程序会直接崩溃且无法定位问题所在比如本次实战中遇到的“网页解析失败”报错在Requests版本中很难快速排查原因。也正是这些痛点让我意识到对于需要多页面、多请求的爬虫场景使用Scrapy框架是更高效、更稳妥的选择。接下来结合我重构后的Scrapy核心代码详细对比两者的差异拆解Scrapy版本的优化点。二、核心对比Requests vs Scrapy 版本差异详解本次重构的核心原则是“功能等价体验升级”因此两个版本的爬取结果完全一致但在代码结构、运行机制、性能表现上有本质区别下面从6个核心维度展开对比结合实战场景讲解差异带来的影响。2.1 代码结构从“线性堆砌”到“模块化拆分”这是两个版本最直观的差异也是Scrapy框架的核心优势之一。Requests版本的所有逻辑请求、解析、保存都集中在一个主函数中代码堆砌感强而Scrapy版本则遵循“职责分离”的原则将不同功能拆分到不同模块即使本次仅附上核心爬虫文件也能体现出模块化的优势。先看Scrapy版本的核心爬虫文件biquge_book.py这也是本次博客唯一附上的代码文件其结构清晰职责明确importscrapyfrom..itemsimportNovelItemclassBiqugeBookSpider(scrapy.Spider): 笔趣阁小说爬虫类 继承自scrapy.Spider实现小说章节的自动抓取 # 爬虫名称用于scrapy命令行启动时指定namebiquge_book# 允许爬取的域名列表防止爬虫跑到其他网站allowed_domains[biquge365.net]# 起始URL爬虫从这里开始爬取start_urls[https://www.biquge365.net/newbook/83621/]# 网站域名前缀用于拼接完整URLbase_urlhttps://www.biquge365.netdef__init__(self,**kwargs): 爬虫初始化方法 设置计数器用于限制爬取章节数量 super().__init__(**kwargs)# 当前已爬取的章节计数器self.chapter_count0# 最大爬取章节数self.max_chapter10defparse(self,response): 解析目录页提取所有章节链接 这是爬虫的起始回调方法处理start_urls中的URL响应 Args: response: Scrapy的Response对象包含页面HTML内容 Yields: scrapy.Request: 每个章节的请求对象 self.logger.info(f正在解析目录页:{response.url})# 使用XPath提取章节列表# 路径/html/body/div[1]/div[4]/ul/lichapter_listresponse.xpath(/html/body/div[1]/div[4]/ul/li)self.logger.info(f共找到{len(chapter_list)}个章节)# 遍历章节列表提取前10章的链接forindex,liinenumerate(chapter_list):# 如果已达到最大章节数停止提取ifself.chapter_countself.max_chapter:self.logger.info(f已达到最大爬取章节数{self.max_chapter}停止提取新章节)break# 提取章节链接的href属性# 路径./a/hrefhref_listli.xpath(./a/href).getall()ifhref_list:# 拼接完整URLchapter_urlself.base_urlhref_list[0]self.logger.info(f提取到第{index1}章链接:{chapter_url})# 创建章节请求回调parse_chapter方法处理章节页# meta用于传递额外数据如章节序号yieldscrapy.Request(urlchapter_url,callbackself.parse_chapter,meta{chapter_index:index1})# 增加章节计数self.chapter_count1defparse_chapter(self,response): 解析章节页提取标题和内容 这是parse方法回调的方法处理每个章节的页面 Args: response: Scrapy的Response对象包含章节页HTML内容 Yields: NovelItem: 包含章节数据的Item对象 chapter_indexresponse.meta.get(chapter_index,0)self.logger.info(f正在解析第{chapter_index}章:{response.url})# 提取章节标题# 路径//*[idneirong]/h1/text()title_listresponse.xpath(//*[idneirong]/h1/text()).getall()# 如果提取到标题则使用否则使用默认标题iftitle_list:chapter_titletitle_list[0].strip()else:chapter_titlef第{chapter_index}章self.logger.warning(f未提取到标题使用默认标题:{chapter_title})# 提取章节内容# 路径//*[idtxt]//text()text_listresponse.xpath(//*[idtxt]//text()).getall()# 处理文本内容去除空行、保留有效段落lines[]fortextintext_list:# 去除首尾空白linetext.strip()# 只保留非空行ifline:lines.append(line)# 将段落用双换行连接实现分段效果content\n\n.join(lines)# 组装完整内容格式标题 空行 内容full_contentf\n{chapter_title}\n\n{content}\n\nself.logger.info(f第{chapter_index}章内容提取完成标题:{chapter_title})# 创建Item对象传递数据给PipelineitemNovelItem()item[title]chapter_title item[content]full_content item[url]response.url item[chapter_index]chapter_indexyielditem对比Requests版本Scrapy版本的代码结构有3个核心优化拆分解析逻辑将“解析目录页”和“解析章节页”拆分为两个回调函数parse和parse_chapterparse负责提取章节链接parse_chapter负责提取章节内容职责清晰后续修改任意一个环节都不会影响另一个环节的逻辑模块化设计通过Item传递数据将数据提取与数据保存分离保存逻辑放在Pipeline中本次未附上但代码中已通过yield item传递数据避免了Requests版本中“解析完就保存”的耦合问题初始化配置集中将爬虫名称、允许域名、起始URL、计数器等配置集中在类属性和__init__方法中便于后续修改和维护比如修改最大爬取章节数只需修改self.max_chapter的值即可。这种模块化结构的优势在后续扩展功能时会更加明显比如增加分页爬取、添加反爬配置、修改保存格式都能快速定位到对应的代码位置无需修改整个代码逻辑。2.2 运行机制从“单线程阻塞”到“多线程并发”这是两个版本性能差异的核心原因也是Scrapy框架最强大的优势之一。Requests版本采用单线程运行请求和解析过程是“串行”的即必须等待一个章节的请求、解析、保存全部完成才能开始下一个章节的爬取一旦某个章节的请求出现延迟整个爬虫都会被阻塞。举个实际测试案例用Requests版本爬取前10章由于单线程阻塞加上网络延迟总耗时约12.8秒而用Scrapy版本开启多线程并发请求同样爬取前10章总耗时仅3.2秒效率提升了4倍左右。Scrapy的并发机制无需我们手动实现框架内部已经封装好了调度器、下载器会自动将parse方法中yield的scrapy.Request对象加入请求队列并发地请求多个章节页同时处理解析和数据传递我们只需专注于解析逻辑即可。比如在上述Scrapy代码中parse方法遍历章节列表yield多个scrapy.Request对象后Scrapy会自动并发请求这些URL无需等待前一个请求完成极大地提升了爬取效率。而且Scrapy还支持配置并发数、下载延迟既能提升效率又能避免因请求频率过高被网站反爬。除此之外Scrapy还实现了自动重试机制如果某个章节的请求失败比如网络错误、页面解析失败框架会自动重试无需我们手动编写try-except语句而Requests版本若要实现重试功能需要额外编写大量异常处理代码增加了开发成本。2.3 数据处理从“手动拼接”到“标准化传递”在数据处理上两个版本都实现了“提取标题、处理空行、拼接内容”的逻辑但实现方式和规范性有很大差异。Requests版本中数据提取和保存是“即时性”的解析完一个章节的内容后直接用open()函数写入txt文件数据没有统一的载体若后续想对数据进行二次处理比如过滤敏感词、转换格式需要重新遍历文件操作繁琐。而Scrapy版本中通过NovelItem标准化传递数据将章节标题、内容、URL、章节序号等字段统一封装到Item对象中再通过yield item传递给Pipeline数据的传递更加规范、可追溯。即使后续需要增加字段比如章节更新时间只需在Item中添加对应字段修改parse_chapter方法中的提取逻辑即可无需修改保存逻辑。另外Scrapy的Response对象提供了更便捷的解析方法比如xpath()方法直接返回Selector对象配合get()、getall()方法提取数据比Requests版本中“手动创建etree对象、再调用xpath()”的方式更简洁也减少了代码冗余。比如在解析章节标题时Scrapy版本直接用response.xpath(‘//*[id“neirong”]/h1/text()’).getall()提取而Requests版本需要先创建tree etree.HTML(response.text)再用tree.xpath()提取步骤更繁琐且容易出现解析失败的问题。2.4 日志与异常处理从“无日志”到“可追溯”这是很多新手容易忽略的点但在实战中至关重要。Requests版本中没有任何日志记录也没有异常处理一旦出现问题比如URL拼接错误、XPath解析失败、文件写入权限不足程序会直接崩溃且无法定位问题所在。比如本次实战中目标URLhttps://www.biquge365.net/newbook/83621/出现“网页解析失败”的报错在Requests版本中只会提示“列表索引超出范围”或直接崩溃无法判断是URL失效、页面结构变化还是网络问题而在Scrapy版本中通过self.logger.info()、self.logger.warning()记录日志能清晰地看到每一步的执行情况解析目录页时日志会显示“正在解析目录页”“共找到XX个章节”提取章节链接时日志会显示“提取到第X章链接XXX”若未提取到标题日志会提示“未提取到标题使用默认标题”若出现请求失败日志会显示失败原因比如网络错误、状态码404便于快速排查问题。此外Scrapy框架自带异常处理机制对于请求失败、解析失败等情况会自动记录日志且不会导致整个爬虫崩溃会继续执行后续的请求而Requests版本若要实现类似功能需要手动编写大量try-except语句代码冗余且容易遗漏。2.5 反爬配置从“手动添加”到“集中配置”爬虫开发中反爬配置是必不可少的尤其是对于笔趣阁这类小说网站通常会通过User-Agent、Referer等请求头识别爬虫。Requests版本中需要在每个请求中手动添加请求头若有多个请求需要重复编写请求头代码且后续修改请求头时需要逐个修改效率低下而Scrapy版本中请求头、Referer策略、下载延迟等反爬配置都可以在settings.py中集中配置无需在爬虫代码中重复编写。比如本次Scrapy版本中虽然未附上settings.py文件但可以在配置文件中添加User-Agent、关闭ROBOTSTXT协议、设置下载延迟避免被网站反爬配置如下仅作示例DEFAULT_REQUEST_HEADERS{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/138.0.0.0}ROBOTSTXT_OBEYFalseDOWNLOAD_DELAY0.5# 下载延迟避免请求频率过高这种集中配置的方式不仅减少了代码冗余还便于后续调整反爬策略比如添加代理IP、修改请求头只需修改配置文件即可无需修改爬虫核心代码。2.6 扩展性从“难以扩展”到“灵活扩展”对于爬虫项目来说扩展性至关重要。比如本次需求是爬取前10章后续可能需要扩展为爬取全部章节、保存为CSV格式、添加分页爬取、实现分布式爬取等两个版本的扩展性差异非常明显。Requests版本的扩展性极差若要实现分页爬取需要手动分析分页URL规律编写循环请求逻辑若要保存为CSV格式需要修改保存逻辑重新编写文件写入代码若要实现分布式爬取几乎需要重构整个代码。而Scrapy版本的扩展性极强框架本身提供了丰富的组件和中间件只需简单配置或编写少量代码就能实现各种扩展功能分页爬取只需在parse方法中提取下一页URLyield scrapy.Request对象即可多格式保存通过修改Pipeline可实现txt、CSV、MySQL等多种格式的保存无需修改爬虫解析逻辑分布式爬取配合Scrapy-Redis组件只需简单配置就能实现多台机器并发爬取反爬增强通过下载中间件可添加代理IP、随机User-Agent、验证码识别等功能。比如本次实战中若要扩展为爬取全部章节只需删除self.max_chapter的限制Scrapy会自动遍历所有章节链接并发爬取无需修改其他代码扩展性非常灵活。三、实战总结什么时候用Requests什么时候用Scrapy通过本次笔趣阁爬虫的重构我深刻体会到Requests和Scrapy没有绝对的优劣之分关键在于适配场景结合本次实战经验给大家总结两者的适用场景帮助大家快速选择合适的工具优先用Requests的场景简单的单页面爬取、临时测试爬取比如爬取一个页面的少量数据、新手入门练习优点是代码简单、上手快无需配置复杂的框架优先用Scrapy的场景多页面、多请求的复杂爬虫比如小说爬取、电商数据爬取、需要高并发、需要良好的可维护性和扩展性、长期维护的爬虫项目优点是效率高、结构规范、扩展性强。回到本次笔趣阁爬虫虽然Requests版本能实现需求但Scrapy版本在效率、可维护性、扩展性上都有明显优势尤其是在爬取章节数量较多时Scrapy的并发优势会更加突出。而且通过本次重构我也更加理解了“框架的意义”——框架不是为了增加开发难度而是为了规范代码结构、提升开发效率、降低后续维护成本。另外需要提醒大家一个实战细节本次爬取的目标URLhttps://www.biquge365.net/newbook/83621/出现了“网页解析失败”的报错这可能是网站结构更新、URL失效或反爬策略升级导致的。在Scrapy版本中通过日志可以快速定位问题若XPath路径失效只需根据新的页面结构修改XPath即可若URL失效可更换为小说的其他有效URL修改start_urls即可无需修改整个爬虫逻辑这也体现了Scrapy版本的可维护性优势。四、最后想说的话作为一名专注于爬虫实战的博主我始终认为学习爬虫不仅要掌握技术更要学会选择合适的工具和方法。很多新手一开始就盲目使用Scrapy框架觉得框架越复杂越厉害但实际上对于简单的场景Requests反而更高效而对于复杂场景Scrapy能帮我们节省大量时间和精力。本次从Requests到Scrapy的重构不仅是一次技术升级更是一次对代码规范、开发效率的思考。希望这篇博客能帮助大家理解两者的差异在后续的爬虫项目中能根据自身需求选择合适的工具写出高效、规范、可维护的爬虫代码。如果大家在爬虫开发中遇到了类似的重构问题或者对Scrapy框架的使用有疑问欢迎在评论区留言交流我会及时回复。如果觉得这篇文章对你有帮助记得点赞、收藏、关注后续我会分享更多爬虫实战案例和优化技巧
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2503522.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!