概述
如何加速 python 爬虫?多进程/多线程/协程
在完成基本的爬虫功能以后,亟需考虑和解决的就是爬虫效率问题。爬虫的重要过程有发送请求、等待响应、解析 html、将目标数据写入到文件等操作。其中等待响应和写文件的过程,都是需要“等待”的,也就是会阻塞。阻塞的意思就是,cpu 处理到某些环节时,它需要等待相关的动作完成后它才会继续工作,只要动作没完成它就可以耗着不干活。如果阻塞的时间过长,整个代码的运行效率就非常低下。
那么解决这种问题有哪些方式呢?想要回答这个问题,就得先分析下计算机是如何运行代码的。
进程和线程
角色扮演如下:
-
计算机:公司
-
进程:项目
-
线程:员工
-
内存:数据等
以 python 为例,我们写了个普通的代码文件。计算机/公司运行代码的时候,就启动了一个进程,相当于成功立了个项目。项目/进程可以有对应的员工/线程、设备和仓库等资源供其运行使用。项目/进程必须至少有一个员工/线程来干活儿。
如果计算机/公司有较为雄厚的实力和资源,也可以为这个项目/进程再多招点员工/线程,这个时候也就是多线程,员工/线程之间能贡献很多资源。
还有一种方式就是,计算机/公司是个集团公司,除了在北京分公司立了这个项目/进程以外,还在其他多个分公司一起立了项目/进程,相应的也需要保证在各个分公司都要有员工/线程和相关资源,办公设备啥的也都得跟着配一套,这个时候就是多进程。
总结来讲,进程是任务运行的最小资源单位,至少包括一个线程来工作,相关的数据等也要内存资源等。而线程是实际干活的,cpu 有多少个核就最多可以有多少个线程一起工作。所以,开多线程要看 cpu 的情况,开多进程就还要考虑内存等,都需要根据计算机性能来确定,不是越多越好。
值得注意的一点是,python 中的多线程是假的,根源在于 python 的 GIL (全局解释器锁)。GIL 会限制多个线程一起工作,保证每次只有一个线程在实际工作。所以,在 python 中开了多线程之后,代码运行效率不会得到什么提升。python 中常见的是多进程,但是多进程很耗费数据资源,所以也要酌情使用。
了解到进程和线程基本的特点之后,就可以继续讨论如何加速爬虫了。加速方式可以有 3 种,也就是多进程、多线程和协程。python 将多进程和多线程的调用封装在了一起,在 concurrent.futures 中可以调用,接口也都非常相似,便于使用。
多进程
# 创建进程池,将任务提交给进程池,让进程池去完成工作
from concurrent.futures import ProcessPoolExecutor
def fn(name):
for i in range(1000):
print(name, i)
return
if __name__ == "__main__":
with ProcessPoolExecutor(50) as t:
for i in range(100):
t.submit(fn, name=f"进程{i}")
print("多线程运行结束。")
# output 截选
...
进程17 964
进程19 865
进程17 965
进程19 866
进程17 966
进程19 867
进程17 967
进程19 868
进程17 968
进程19 869
...
多线程
# 创建线程池,将任务提交给线程池,让线程池去完成工作
from concurrent.futures import ThreadPoolExecutor
def fn(name):
for i in range(1000):
print(name, i)
return
if __name__ == "__main__":
with ThreadPoolExecutor(50) as t:
for i in range(100):
t.submit(fn, name=f"线程{i}")
print("多线程运行结束。")
# output 截选
...
线程78 938
线程96 985
线程96 986
线程96 987
线程96 988
线程96 989
线程96 990
线程96 991
线程96 992
线程96 993
线程92 849
线程89 878
线程89 879
线程93 866
...
协程
协程的概念和前面的进程和线程都有些不一样,英文叫 coroutine,从名字看就体现了它的特性。为了体现对比性,先强调下我们只讨论单线程的情况下,协程如何提高代码运行效率。
前面提到,多进程相当于开多个分公司,资源开销很大,多线程相当于招多个员工来做。而协程的想法有些“不人道”,它既不开分公司,也不多招人,它通过压榨那唯一的员工/单线程的“剩余价值”来提高整体的效率,也就是针对本文开头提到的“阻塞”情况来对症下药。
简单来讲,协程就是让单线程 cpu “不休息”来加速,比如厨房做菜需要以下步骤:
- 腌制鸡腿操作需要 10 分钟,需要等待 120 分钟才入味;
- 热馒头操作需要 2 分钟,需要等待 15 分钟才热透;
- 蒸米饭操作需要 5 分钟,需要等待 30 分钟饭才熟;
- 炒菜需要更多关注,但是中间也可以有 2 分钟间隙去摆一下餐具。
- ……
有做饭经验的人,不会在等待 120 分钟鸡腿入味的时候什么不做就干等着,然后两小时以后开始热馒头,然后再干等 15 分钟,以此类推……这种方式叫“同步”,我的理解就是各个任务或运算共用一套时间表,而且必须严格按照时间表和先后顺序来执行,鸡腿没腌完我就不热馒头。
但实际上,不止做饭,日常很多时间安排都可以适当地“交叠”,一天下来能做更多的事情。协程就是这种思想,它尽量不让 cpu 空等着,而是充分利用各种具体运算的间隙来交叠着工作,A 运算阻塞了就做点 B 运算,B 阻塞了就做 C 反正不闲着。这种方式叫“异步”,就是各个任务没有那么多讲究,全看 cpu 如何安排,cpu 获得了安排任务顺序及交叠情况的自由,就能考虑“大局”,尽快地把多个任务完成,达成整体的效率提升。
协程的具体实现上,稍微比前面的多进程和多线程麻烦一点。具体到爬虫上,还需要安装几个其他的模块才能实现,比如 asyncio、aiohttp 和 aiofiles 等。前面提到的请求页面等待响应、写文件等都是会“阻塞”的,那么这些相关的代码都需要明显明确地叠个“异步”的标签和 buff,声明自己是个“异步”对象,并且在实际调用的时候也要体现出来支持协程。
# 一个将普通函数包装成异步支持协程的对象,例子中并不能加速
import asyncio
# async “冠名”,声明这个 fn 函数支持异步
async def fn():
for i in range(1000):
print(i)
return
if __name__ == "__main__":
# 下面一行,不会执行 fn 函数,而是返回一个异步对象 g
g = fn()
# 通过这种方式来运行异步函数 fn,python 版本要 3.7 以后,否则可能报错
asyncio.run(g)
print(g)
# 下面例子中在验证异步操作协程加速整体代码运行速度
# 需要注意 sleep 部分,需要将同步操作改为异步操作,爬虫中 requests 及 io 部分同理。
import time, asyncio
async def func1():
print("你好啊,我叫小明1")
# 下面的 time.sleep 是同步操作,虽然 func1 是异步,但是遇到同步操作就会中断异步,也就是无法协程
# 所以,应该将上面 sleep 的同步操作也改为异步实现的 sleep,如下
# time.sleep(3)
await asyncio.sleep(3)
print("你好啊,我叫小红1")
return
async def func2():
print("你好啊,我叫小明2")
# time.sleep(5)
await asyncio.sleep(5)
print("你好啊,我叫小红2")
return
async def func3():
print("你好啊,我叫小明3")
# time.sleep(6)
await asyncio.sleep(6)
print("你好啊,我叫小红3")
return
if __name__ == "__main__":
f1 = func1()
f2 = func2()
f3 = func3()
tasks = [f1, f2, f3]
t1 = time.time()
asyncio.run(asyncio.wait(tasks))
t2 = time.time()
print(t2 - t1)
参考:
系列视频
最后
以上就是谨慎魔镜为你收集整理的如何加速 python 爬虫?多进程/多线程/协程的全部内容,希望文章能够帮你解决如何加速 python 爬虫?多进程/多线程/协程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复