热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Python实战:异步爬虫(协程技术)与分布式爬虫(多进程应用)深入解析

本文将深入探讨Python异步爬虫和分布式爬虫的技术细节,重点介绍协程技术和多进程应用在爬虫开发中的实际应用。通过对比多进程和协程的工作原理,帮助读者理解两者在性能和资源利用上的差异,从而在实际项目中做出更合适的选择。文章还将结合具体案例,展示如何高效地实现异步和分布式爬虫,以提升数据抓取的效率和稳定性。

转自:https://blog.csdn.net/SL_World/article/details/86633611

在讲解之前,我们先来通过一幅图看清多进程和协程的爬虫之间的原理及其区别。(图片来源于网络)

这里,异步爬虫不同于多进程爬虫,它使用单线程(即仅创建一个事件循环,然后把所有任务添加到事件循环中)就能并发处理多任务。在轮询到某个任务后,当遇到耗时操作(如请求URL)时,挂起该任务并进行下一个任务,当之前被挂起的任务更新了状态(如获得了网页响应),则被唤醒,程序继续从上次挂起的地方运行下去。极大的减少了中间不必要的等待时间。

对于协程(Asyncio库)的原理及实现请见:《Python异步IO之协程(详解)》
对于多进程的知识讲解及实现请见:《廖雪峰-Python多进程》

 

在有了Asyncio异步IO库实现协程后,我们还需要实现异步网页请求。因此,aiohttp库应运而生。

使用aiohttp库实现异步网页请求
  在我们写普通的爬虫程序时,经常会用到requests库用以请求网页并获得服务器响应。而在协程中,由于requests库提供的相关方法不是可等待对象(awaitable),使得无法放在await后面,因此无法使用requests库在协程程序中实现请求。在此,官方专门提供了一个aiohttp库,用来实现异步网页请求等功能,简直就是异步版的requests库

【基础实现】:在官方文档中,推荐使用ClientSession()函数来调用网页请求等相关方法。然后,我们在协程中使用ClientSession()的get()或request()方法来请求网页。(其中async with是异步上下文管理器,其封装了异步实现等功能)

import aiohttp

async with aiohttp.ClientSession() as session:
    async with session.get(\'http://httpbin.org/get\') as resp:
        print(resp.status)
        print(await resp.text())

ClientSession()除了有请求网页的方法,官方API还提供了其他HTTP常见方法。

session.request(method=\'GET\', url=\'http://httpbin.org/request\')
session.post(\'http://httpbin.org/post\', data=b\'data\')
session.put(\'http://httpbin.org/put\', data=b\'data\')
session.delete(\'http://httpbin.org/delete\')
session.head(\'http://httpbin.org/get\')
session.options(\'http://httpbin.org/get\')
session.patch(\'http://httpbin.org/patch\', data=b\'data\')

 

【案例】:爬取2018年AAAI顶会中10篇论文的标题。

 

一、测试普通爬虫程序

import time
from lxml import etree
import requests
urls = [
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488\',
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583\',
    # 省略后面8个url...
]
\'\'\'
提交请求获取AAAI网页,并解析HTML获取title
\'\'\'
def get_title(url,cnt):
    response = requests.get(url)  # 提交请求,获取响应内容
    html = response.content       # 获取网页内容(content返回的是bytes型数据,text()获取的是Unicode型数据)
    title = etree.HTML(html).xpath(\'//*[@id="title"]/text()\') # 由xpath解析HTML
    print(\'第%d个title:%s\' % (cnt,\'\'.join(title)))
    
if __name__ == \'__main__\':
    start1 = time.time()
    i = 0
    for url in urls:
        i = i + 1
        start = time.time()
        get_title(url,i)
        print(\'第%d个title爬取耗时:%.5f秒\' % (i,float(time.time() - start)))
    print(\'爬取总耗时:%.5f秒\' % float(time.time()-start1))

执行结果如下:

第1个title:Norm Conflict Resolution in Stochastic Domains
第1个title爬取耗时:1.41810秒
第2个title:Algorithms for Trip-Vehicle Assignment in Ride-Sharing
第2个title爬取耗时:1.31734秒
第3个title:Tensorized Projection for High-Dimensional Binary Embedding
第3个title爬取耗时:1.31826秒
第4个title:Synthesis of Programs from Multimodal Datasets
第4个title爬取耗时:1.28625秒
第5个title:Video Summarization via Semantic Attended Networks
第5个title爬取耗时:1.33226秒
第6个title:TIMERS: Error-Bounded SVD Restart on Dynamic Networks
第6个title爬取耗时:1.52718秒
第7个title:Memory Management With Explicit Time in Resource-Bounded Agents
第7个title爬取耗时:1.35522秒
第8个title:Mitigating Overexposure in Viral Marketing
第8个title爬取耗时:1.35722秒
第9个title:Neural Link Prediction over Aligned Networks
第9个title爬取耗时:1.51317秒
第10个title:Dual Deep Neural Networks Cross-Modal Hashing
第10个title爬取耗时:1.30624秒
爬取总耗时:13.73324秒


可见,平均每请求完一个URL并解析该HTML耗时1.4秒左右。本次程序运行总耗时13.7秒。

二、测试基于协程的异步爬虫程序

  下面,是使用了协程的异步爬虫程序。etree模块用于解析HTML,aiohttp是一个利用asyncio的库,它的API看起来很像请求的API,可以暂时看成协程版的requests。

import time
from lxml import etree
import aiohttp
import asyncio
urls = [
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488\',
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583\',
    # 省略后面8个url...
]
titles = []
sem = asyncio.Semaphore(10) # 信号量,控制协程数,防止爬的过快
\'\'\'
提交请求获取AAAI网页,并解析HTML获取title
\'\'\'
async def get_title(url):
    with(await sem):
        # async with是异步上下文管理器
        async with aiohttp.ClientSession() as session:  # 获取session
            async with session.request(\'GET\', url) as resp:  # 提出请求
                # html_unicode = await resp.text() 
                # html = bytes(bytearray(html_unicode, encoding=\'utf-8\'))
                html = await resp.read() # 可直接获取bytes 
                title = etree.HTML(html).xpath(\'//*[@id="title"]/text()\')
                print(\'\'.join(title))
\'\'\'
调用方
\'\'\'
def main():
    loop = asyncio.get_event_loop()           # 获取事件循环
    tasks = [get_title(url) for url in urls]  # 把所有任务放到一个列表中
    loop.run_until_complete(asyncio.wait(tasks)) # 激活协程
    loop.close()  # 关闭事件循环

if __name__ == \'__main__\':
    start = time.time()
    main()  # 调用方
    print(\'总耗时:%.5f秒\' % float(time.time()-start))

执行结果如下:

Memory Management With Explicit Time in Resource-Bounded Agents
Norm Conflict Resolution in Stochastic Domains
Video Summarization via Semantic Attended Networks
Tensorized Projection for High-Dimensional Binary Embedding
Algorithms for Trip-Vehicle Assignment in Ride-Sharing
Dual Deep Neural Networks Cross-Modal Hashing
Neural Link Prediction over Aligned Networks
Mitigating Overexposure in Viral Marketing
TIMERS: Error-Bounded SVD Restart on Dynamic Networks
Synthesis of Programs from Multimodal Datasets
总耗时:2.43371秒

可见,本次我们使用协程爬取10个URL只耗费了2.4秒,效率是普通同步程序的8~12倍。

【解释】:

  • request获取的text()返回的是网页的Unicode型数据,content和read()返回的是bytes型数据。而etree.HTML(html)接收的参数需是bytes类型,所以①可以通过resp.read()直接获取bytes;②若使用text()则需要通过先把Unicode类型数据转换成比特数组对象,再转换成比特对象, 即bytes(bytearray(html_unicode, encoding=\'utf-8\'))。
  • 发起请求除了可以用上述session.request(\'GET\', url)也可以用session.get(url),功能相同。
  • 如果同时做太多的请求,链接有可能会断掉。所以需要使用sem = asyncio.Semaphore(10) ,Semaphore是限制同时工作的协同程序数量的同步工具。
  • async with是异步上下文管理器,不解的请看Python中的async with用法。

三、测试基于多进程的分布式爬虫程序
下面,我们测试多进程爬虫程序,由于我的电脑CPU是4核,所以这里进程池我就设的4。

import multiprocessing
from multiprocessing import Pool
import time
import requests
from lxml import etree
urls = [
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488\',
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583\',
    # 省略后面8个url...
]
\'\'\'
提交请求获取AAAI网页,并解析HTML获取title
\'\'\'
def get_title(url,cnt):
    response = requests.get(url)  # 提交请求
    html = response.content       # 获取网页内容
    title = etree.HTML(html).xpath(\'//*[@id="title"]/text()\') # 由xpath解析HTML
    print(\'第%d个title:%s\' % (cnt,\'\'.join(title)))
\'\'\'
调用方
\'\'\'
def main():
    print(\'当前环境CPU核数是:%d核\' % multiprocessing.cpu_count())
    p = Pool(4)  # 进程池
    i = 0
    for url in urls:
        i += 1
        p.apply_async(get_title, args=(url, i))
    p.close()
    p.join()   # 运行完所有子进程才能顺序运行后续程序
    
if __name__ == \'__main__\':
    start = time.time()
    main()  # 调用方
    print(\'总耗时:%.5f秒\' % float(time.time()-start))

执行结果:

当前环境CPU核数是:4核
第2个title:Algorithms for Trip-Vehicle Assignment in Ride-Sharing
第1个title:Norm Conflict Resolution in Stochastic Domains
第4个title:Synthesis of Programs from Multimodal Datasets
第3个title:Tensorized Projection for High-Dimensional Binary Embedding
第5个title:Video Summarization via Semantic Attended Networks
第6个title:TIMERS: Error-Bounded SVD Restart on Dynamic Networks
第7个title:Memory Management With Explicit Time in Resource-Bounded Agents
第8个title:Mitigating Overexposure in Viral Marketing
第9个title:Neural Link Prediction over Aligned Networks
第10个title:Dual Deep Neural Networks Cross-Modal Hashing
总耗时:5.01228秒
可见,多进程分布式爬虫也比普通同步程序要快很多,本次运行时间5秒。但比协程略慢。

 

【时间对比】:
对于上例中10个URL的爬取时间,下面整理成了表格。

CPU核数\实现方式 普通同步爬虫 多进程爬虫 异步爬虫
4核 13.7秒 5.0秒 2.4秒


其中增加多进程中进程池Pool(n)的n可加速爬虫,下图显示了消耗的时间(单位.秒)和Pool()参数的关系。

 

 

 

 

如果你以为到这里就结束了,那你就要错过最精彩的东西了:)

四、测试-异步结合多进程-爬虫程序
由于解析HTML也需要消耗一定的时间,而aiohttp和asyncio均未提供相关解析方法。所以可以在请求网页的时使用异步程序,在解析HTML使用多进程,两者配合使用,效率更高哦~!
【请求网页】:使用协程。
【解析HTML】:使用多进程。

 

from multiprocessing import Pool
import time
from lxml import etree
import aiohttp
import asyncio
urls = [
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488\',
    \'https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583\',
    # 省略后面8个url...
]
htmls = []
titles = []
sem = asyncio.Semaphore(10) # 信号量,控制协程数,防止爬的过快
\'\'\'
提交请求获取AAAI网页html
\'\'\'
async def get_html(url):
    with(await sem):
        # async with是异步上下文管理器
        async with aiohttp.ClientSession() as session:  # 获取session
            async with session.request(\'GET\', url) as resp:  # 提出请求
                html = await resp.read() # 直接获取到bytes
                htmls.append(html)
                print(\'异步获取%s下的html.\' % url)

\'\'\'
协程调用方,请求网页
\'\'\'
def main_get_html():
    loop = asyncio.get_event_loop()           # 获取事件循环
    tasks = [get_html(url) for url in urls]  # 把所有任务放到一个列表中
    loop.run_until_complete(asyncio.wait(tasks)) # 激活协程
    loop.close()  # 关闭事件循环
\'\'\'
使用多进程解析html
\'\'\'
def multi_parse_html(html,cnt):
    title = etree.HTML(html).xpath(\'//*[@id="title"]/text()\')
    titles.append(\'\'.join(title))
    print(\'第%d个html完成解析-title:%s\' % (cnt,\'\'.join(title)))
\'\'\'
多进程调用总函数,解析html
\'\'\'
def main_parse_html():
    p = Pool(4)
    i = 0
    for html in htmls:
        i += 1
        p.apply_async(multi_parse_html,args=(html,i))
    p.close()
    p.join()


if __name__ == \'__main__\':
    start = time.time()
    main_get_html()   # 调用方
    main_parse_html() # 解析html
    print(\'总耗时:%.5f秒\' % float(time.time()-start))

执行结果如下:

异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16380下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16674下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16583下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16911下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/17343下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16449下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16488下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16659下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16581下的html.
异步获取https://aaai.org/ocs/index.php/AAAI/AAAI18/paper/viewPaper/16112下的html.
第3个html完成解析-title:Algorithms for Trip-Vehicle Assignment in Ride-Sharing
第1个html完成解析-title:Tensorized Projection for High-Dimensional Binary Embedding
第2个html完成解析-title:TIMERS: Error-Bounded SVD Restart on Dynamic Networks
第4个html完成解析-title:Synthesis of Programs from Multimodal Datasets
第6个html完成解析-title:Dual Deep Neural Networks Cross-Modal Hashing
第7个html完成解析-title:Norm Conflict Resolution in Stochastic Domains
第8个html完成解析-title:Neural Link Prediction over Aligned Networks
第5个html完成解析-title:Mitigating Overexposure in Viral Marketing
第9个html完成解析-title:Video Summarization via Semantic Attended Networks
第10个html完成解析-title:Memory Management With Explicit Time in Resource-Bounded Agents

【参考文献】:
[1] aiohttp官方API文档
[2] 加速爬虫: 异步加载 Asyncio
[3] python:利用asyncio进行快速抓取
[4] 使用 aiohttp 和 asyncio 进行异步请求
[5] requests的content与text导致lxml的解析问题


推荐阅读
  • 在托管C++中开发应用程序时,遇到了如何声明和操作字符串数组的问题。本文详细探讨了字符串数组在托管C++中的应用与实现方法,包括声明、初始化、遍历和常见操作技巧,为开发者提供了实用的参考和指导。 ... [详细]
  • 分布式开源任务调度框架 TBSchedule 深度解析与应用实践
    本文深入解析了分布式开源任务调度框架 TBSchedule 的核心原理与应用场景,并通过实际案例详细介绍了其部署与使用方法。首先,从源码下载开始,详细阐述了 TBSchedule 的安装步骤和配置要点。接着,探讨了该框架在大规模分布式环境中的性能优化策略,以及如何通过灵活的任务调度机制提升系统效率。最后,结合具体实例,展示了 TBSchedule 在实际项目中的应用效果,为开发者提供了宝贵的实践经验。 ... [详细]
  • 本文探讨了利用Java实现WebSocket实时消息推送技术的方法。与传统的轮询、长连接或短连接等方案相比,WebSocket提供了一种更为高效和低延迟的双向通信机制。通过建立持久连接,服务器能够主动向客户端推送数据,从而实现真正的实时消息传递。此外,本文还介绍了WebSocket在实际应用中的优势和应用场景,并提供了详细的实现步骤和技术细节。 ... [详细]
  • HTML5绘图功能的全面支持与应用
    HTML5绘图功能的全面支持与应用 ... [详细]
  • 本文深入探讨了 MXOTDLL.dll 在 C# 环境中的应用与优化策略。针对近期公司从某生物技术供应商采购的指纹识别设备,该设备提供的 DLL 文件是用 C 语言编写的。为了更好地集成到现有的 C# 系统中,我们对原生的 C 语言 DLL 进行了封装,并利用 C# 的互操作性功能实现了高效调用。此外,文章还详细分析了在实际应用中可能遇到的性能瓶颈,并提出了一系列优化措施,以确保系统的稳定性和高效运行。 ... [详细]
  • 本文深入探讨了 hCalendar 微格式在事件与时间、地点相关活动标记中的应用。作为微格式系列文章的第四篇,前文已分别介绍了 rel 属性用于定义链接关系、XFN 微格式增强链接的人际关系描述以及 hCard 微格式对个人和组织信息的描述。本次将重点解析 hCalendar 如何通过结构化数据标记,提高事件信息的可读性和互操作性。 ... [详细]
  • 在过去,我曾使用过自建MySQL服务器中的MyISAM和InnoDB存储引擎(也曾尝试过Memory引擎)。今年初,我开始转向阿里云的关系型数据库服务,并深入研究了其高效的压缩存储引擎TokuDB。TokuDB在数据压缩和处理大规模数据集方面表现出色,显著提升了存储效率和查询性能。通过实际应用,我发现TokuDB不仅能够有效减少存储成本,还能显著提高数据处理速度,特别适用于高并发和大数据量的场景。 ... [详细]
  • 大家好,我是梅巴哥er。本文将深入探讨Redux框架中的第三个实战案例,具体实现每两秒自动点击按钮以触发颜色变化的功能。该案例中,一个关键点在于是否需要使用异步操作来处理定时任务,我们将详细分析其必要性和实现方式。通过这一实例,读者可以更好地理解Redux在实际项目中的应用及其异步处理机制。 ... [详细]
  • Java Web开发中的JSP:三大指令、九大隐式对象与动作标签详解
    在Java Web开发中,JSP(Java Server Pages)是一种重要的技术,用于构建动态网页。本文详细介绍了JSP的三大指令、九大隐式对象以及动作标签。三大指令包括页面指令、包含指令和标签库指令,它们分别用于设置页面属性、引入其他文件和定义自定义标签。九大隐式对象则涵盖了请求、响应、会话、应用上下文等关键组件,为开发者提供了便捷的操作接口。动作标签则通过预定义的动作来简化页面逻辑,提高开发效率。这些内容对于理解和掌握JSP技术具有重要意义。 ... [详细]
  • 通过在项目中引用 NuGet 包 `ExcelDataReader`,可以实现高效地读取和导入 Excel 文件中的数据。具体方法是在项目中执行 `Install-Package ExcelDataReader` 命令,然后通过定义一个 `LeadingIn` 方法并传入上传文件的路径来完成数据导入。该方法不仅简化了代码逻辑,还显著提升了数据处理的效率和可靠性。 ... [详细]
  • 在处理 GridView 中的行记录时,有时需要动态地添加或删除行,而无需对数据库中的实际数据进行任何更改。本文介绍了如何实现这一功能,确保操作仅限于前端展示层面,而不影响后端数据库的完整性。通过这种方法,用户可以在不修改数据库记录的情况下,灵活地管理 GridView 中的数据展示。 ... [详细]
  • Node.js 教程第五讲:深入解析 EventEmitter(事件监听与发射机制)
    本文将深入探讨 Node.js 中的 EventEmitter 模块,详细介绍其在事件监听与发射机制中的应用。内容涵盖事件驱动的基本概念、如何在 Node.js 中注册和触发自定义事件,以及 EventEmitter 的核心 API 和使用方法。通过本教程,读者将能够全面理解并熟练运用 EventEmitter 进行高效的事件处理。 ... [详细]
  • 本文深入探讨了ASP.NET中ViewState、Cookie和Session三种状态管理技术的区别与应用场景。ViewState主要用于保存页面控件的状态信息,确保在多次往返服务器过程中数据的一致性;Cookie则存储在客户端,适用于保存少量用户偏好设置等非敏感信息;而Session则在服务器端存储数据,适合处理需要跨页面保持的数据。文章详细分析了这三种技术的工作原理及其优缺点,并提供了实际应用中的最佳实践建议。 ... [详细]
  • 如何在Spark数据排序过程中有效避免内存溢出(OOM)问题
    本文深入探讨了在使用Spark进行数据排序时如何有效预防内存溢出(OOM)问题。通过具体的代码示例,详细阐述了优化策略和技术手段,为读者在实际工作中遇到类似问题提供了宝贵的参考和指导。 ... [详细]
  • 深入解析 Django 中用户模型的自定义方法与技巧 ... [详细]
author-avatar
陈鑫
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有