第八天——多任务异步爬虫
一、异步爬虫概述
爬虫的本质:就是客户端向服务器请求批量获取响应数据。如果有多个待爬取的url的话,只用一个线程切采用串行的方式执行,那么只能等待一个爬虫任务完成之后才能继续下一个爬虫任务,这样的话效率是非常低的。
所以,在我们心中其实很自然的就可以想到使用异步机制来提高爬虫的速度。通过构建进程池或者线程池来完成异步爬虫,即使用多进程或多线程来处理多个请求,当别的进程或线程发生阻塞时,自动切换到另一进程或线程执行下一个爬虫任务。
高性能异步爬虫的目的:在爬虫中使用异步实现高性能的数据爬取操作。
注意:python中无法实现真正的多线程,因为其并不能充分利用多核CPU资源。如果想要充分利用CPU资源的话大部分情况下都是使用的多进程(multiprocessing)
import requests
import time
t1 = time.time()
urls = ['https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4949_s.jpg',
'https://scpic.chinaz.net/Files/pic/pic9/202201/hpic4950_s.jpg',
'https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4948_s.jpg',
'https://scpic2.chinaz.net/Files/pic/pic9/202201/apic37939_s.jpg',
'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37931_s.jpg',
'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37934_s.jpg',
'https://scpic.chinaz.net/Files/pic/pic9/202201/apic37936_s.jpg']
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'}
def get_content(url):
print('正在爬取', url)
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.content
def parse_content(content):
print('响应数据长度为为:',len(content))
for url in urls:
content = get_content(url)
parse_content(content)
t2 = time.time()
print('程序总用时为:', t2-t1)
二、异步爬虫的方式
2.1 多线程、多进程(不推荐)
-
优点:可以为相关阻塞的操作开启线程或者进程,阻塞操作就可以异步执行。
-
缺点:不能够无限制的开启多进程或者多线程
爬虫程序模拟(单线程):
import time t1 = time.time() lst = [] urls = [1, 2, 3, 4, 5, 6, 7, 8, 9] result = [] def GetResponse(url): global result time.sleep(2) r = url * url result.append(r) if __name__ == '__main__': for url in urls: GetResponse(url) t2 = time.time() print(result) print('消耗时间:', t2-t1)
爬虫程序模拟(异步,多进程使用):
import time import multiprocessing t1 = time.time() lst = [] urls = [1, 2, 3, 4, 5, 6, 7, 8, 9] result = [] def GetResponse(url, queue): time.sleep(2) r = url * url queue.put(r) if __name__ == '__main__': jobs = [] queues = [] for url in urls: queue = multiprocessing.Queue() queues.append(queue) p = multiprocessing.Process(target=GetResponse, args=(url, queue)) jobs.append(p) p.start() for i, t in enumerate(jobs): t.join() result.append(queues[i].get()) t2 = time.time() print(result) print('消耗时间:', t2-t1)
基于管道通信的多进程爬虫实现:
import requests import time import multiprocessing from multiprocessing import Queue t1 = time.time() urls = ['https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4949_s.jpg', 'https://scpic.chinaz.net/Files/pic/pic9/202201/hpic4950_s.jpg', 'https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4948_s.jpg', 'https://scpic2.chinaz.net/Files/pic/pic9/202201/apic37939_s.jpg', 'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37931_s.jpg', 'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37934_s.jpg', 'https://scpic.chinaz.net/Files/pic/pic9/202201/apic37936_s.jpg'] headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'} result = [] # 保存最终结果 def get_content(url, queue): global result print('正在爬取', url) result.append(url) queue.put(url) # 将当前url添加到队列中 response = requests.get(url, headers=headers) if response.status_code == 200: return response.content def parse_content(content): print('响应数据长度为为:',len(content)) if __name__ == '__main__': queues = [] jobs = [] for url in urls: queue = Queue() queues.append(queue) p = multiprocessing.Process(target=get_content, args=(url, queue)) jobs.append(p) p.start() for i, t in enumerate(jobs): t.join() result.append(queues[i].get()) t2 = time.time() print(result) print('程序总用时为:', t2-t1)
2.2 线程池、进程池(适当使用)
- 优点:可以降低系统对进程或者线程创建和销毁的频率,从而很好的降低系统的开销
- 缺点:池中线程或进程的数量是有上限的。
模拟爬虫操作:
import time
from multiprocessing.dummy import Pool
t1 = time.time()
lst = []
urls = [1, 2, 3, 4, 5, 6, 7, 8, 9]
result = []
def GetResponse(url):
time.sleep(2)
r = url * url
result.append(r)
if __name__ == '__main__':
pools = Pool(9)
pools.map(GetResponse, urls)
t2 = time.time()
print(result)
print('占用时长:', t2-t1)
2.3 单线程+异步协程(推荐)
协程概念是从python3.4开始,从3.6的时候进行简化封装,在本课程中的协程是基于python3.6之后
- event_loop:事件循环,相当于一个无限循环,可以把一些函数注册到这个事件循环上,当满足某些条件的时候,函数就会被循环执行。
- coroutine:协程对象,可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async关键字来定义一个方法,这个方法在调用时并不会立即被执行,而是返回一个协程对象
- task:任务,是对协程对象的进一步封装,包含了任务的各个状态。
- future:代表将来执行或还没有执行的任务,实际上和task没有本质区别。
- async:定义一个协程。
- await:用于挂起阻塞方法的执行
2.3.1 task的使用
import time
import asyncio
t1 = time.time()
lst = []
urls = [1, 2, 3, 4, 5, 6, 7, 8, 9]
result = []
async def GetResponse(url):
await asyncio.sleep(3)
r = url * url
print(r)
result.append(r)
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建loop对象
task = loop.create_task(GetResponse(9)) # 创建任务对象
print(task)
loop.run_until_complete(task) # 运行直到任务结束
t2 = time.time()
print(task)
print(result)
print('占用时长:', t2-t1)
2.3.2 future的使用
import time
import asyncio
t1 = time.time()
lst = []
urls = [1, 2, 3, 4, 5, 6, 7, 8, 9]
result = []
async def GetResponse(url):
await asyncio.sleep(3) # 注意阻塞操作的时候需要进行await手动挂起
r = url * url
print(r)
result.append(r)
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建loop对象
task = asyncio.ensure_future(GetResponse(9)) # 创建任务对象
print(task)
loop.run_until_complete(task) # 运行直到任务结束
t2 = time.time()
print(task)
print(result)
print('占用时长:', t2-t1)
2.3.3 绑定回调
import time
import asyncio
t1 = time.time()
lst = []
urls = [1, 2, 3, 4, 5, 6, 7, 8, 9]
result = []
async def GetResponse(url):
await asyncio.sleep(3) # 注意阻塞操作的时候需要进行await手动挂起
r = url * url
result.append(r)
return r
def callback_func(task):
# result返回的就是任务对象中封装的协程对象对应的函数的返回值
print(task.result())
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建loop对象
task = asyncio.ensure_future(GetResponse(9)) # 创建任务对象
print(task)
# 将回调函数绑定到任务对象中
task.add_done_callback(callback_func) # 当任务执行成功之后就会回调到回调函数中,默认将任务对象传递到回调函数中
loop.run_until_complete(task) # 运行直到任务结束
t2 = time.time()
print(task)
print(result)
print('占用时长:', t2-t1)
三、多任务异步协程实现
import time
import asyncio
t1 = time.time()
lst = []
urls = [1, 2, 3, 4, 5, 6, 7, 8, 9]
result = []
CONCURRENCY = 5 # 定义最大协程数(常量)
semaphore = asyncio.Semaphore(CONCURRENCY) # 控制并发量
async def GetResponse(url):
await asyncio.sleep(3) # 注意阻塞操作的时候需要进行await手动挂起,如果不挂起,就不会等待阻塞操作结束直接运行后面的代码,可能导致数据紊乱
r = url * url
result.append(r)
return r
def callback_func(task):
# result返回的就是任务对象中封装的协程对象对应的函数的返回值
print(task.result())
if __name__ == '__main__':
loop = asyncio.get_event_loop() # 创建loop对象
tasks = []
for url in urls:
task = asyncio.ensure_future(GetResponse(url)) # 创建任务对象
tasks.append(task)
# 将回调函数绑定到任务对象中
loop.run_until_complete(asyncio.wait(tasks)) # 运行直到任务结束,注意多任务的格式,需要将任务列表封装到wait中
t2 = time.time()
print(result)
print('占用时长:', t2-t1)
四、多任务异步爬虫实现
在爬虫开发中,requests模块是一个同步模块,所以不能使用于异步任务中。为了实现真正的异步,需要我们引入aiohttp模块来替换requests模块来实现网络请求操作。
安装
pip install aiohttp
使用ClientSession来发送
import asyncio
import aiohttp
import time
t1 = time.time()
urls = ['https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4949_s.jpg',
'https://scpic.chinaz.net/Files/pic/pic9/202201/hpic4950_s.jpg',
'https://scpic1.chinaz.net/Files/pic/pic9/202201/hpic4948_s.jpg',
'https://scpic2.chinaz.net/Files/pic/pic9/202201/apic37939_s.jpg',
'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37931_s.jpg',
'https://scpic3.chinaz.net/Files/pic/pic9/202201/apic37934_s.jpg',
'https://scpic.chinaz.net/Files/pic/pic9/202201/apic37936_s.jpg']
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'}
result = [] # 保存最终结果
async def get_content(url):
async with aiohttp.ClientSession() as session:
# 注意代理使用的是proxy='http://ip:port'
async with await session.get(url, headers=headers)as response:
global result
print('正在爬取', url)
result.append(url)
if response.status == 200:
res = response.content
return res
if __name__ == '__main__':
tasks = []
for url in urls:
c = get_content(url)
task = asyncio.ensure_future(c)
tasks.append(task)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
t2 = time.time()
print(result)
print('程序总用时为:', t2-t1)