在开发爬虫的时候,对于老工程师的工作节奏一般是先实现爬虫的抓取逻辑,然后就要提升爬取的效率了。
众所周知想提升效率就要涉猎到并发编程姿势啦,所以今天我们抛砖引玉,不去过多的计较太原理和抽象的东西,而是用 Python 自带的并发标准库和第三方库来看看怎么优雅的实现并发编程和提升爬取效率。
我们写来个简单的爬虫:
# -*- coding=utf-8 -*-
import time
import requests
from bs4 import BeautifulSoup
t1 = time.time()
urls = [
''.format(i)
for i in range(0, 226, 25)
]
def job(url):
r = requests.get(url)
content = r.text
soup = BeautifulSoup(content, 'html.parser')
item = soup.select(".item")
for i in item:
print(i.select(".title")[0].text)
for url in urls:
job(url)
# 耗时: 2.0312600135803223
print("耗时:", time.time() - t1)
这段代码的作用是使用 requests 和 BeautifulSoup 第三方库对豆瓣top250的电影名称进行的获取,非常简单,相信老司机的你们不需要我过多的解释。
然后我们统计一下耗时:2.0312600135803223
然后我们来思考,现在各种大牛口中的多线程,多进程和协程,我们在开发中是如何实现的。
▍多线程
虽然不过多的说明原理,但是我相信你们心中还是好奇的,那我就引用一段维基百科的说明:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程,但轻量进程更多指内核线程,而把用户线程称为线程 ——维基百科
其实线程很简单,我们写了一些代码,肯定想要执行看看结果吧?操作系统执行你的代码,执行的单位就是线程。
了解了线程,我们来看一下多线程的例子:
# -*- coding=utf-8 -*-
import requests
import time
import threading
from bs4 import BeautifulSoup
t1 = time.time()
urls = [
''.format(i)
for i in range(0, 226, 25)
]
def job(url):
r = requests.get(url)
content = r.text
soup = BeautifulSoup(content, 'html.parser')
item = soup.select(".item")
for i in item:
print(i.select(".title")[0].text)
threads = [threading.Thread(target=job, args=(url, )) for url in urls]
for i in threads:
i.start()
i.join()
print("耗时:", time.time() - t1)
我们看一下耗时:1.9976181983947754
这时同学一看,mmp 没快到哪去,你个骗子!稍安勿躁,这个锅老师肯定是不背的,其实这都怪万恶的 GIL。那么问题来了,什么是 GIL 呢?
▍GIL
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做设计。
尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。
引用 cookbook 里面的这段话,我相信已经完全解释清楚了问题,那我们就没有办法了么?
▍协程
这时候,协程就进入到了我们的视角了,协程叫轻量级线程也叫绿色线程,其实协程也是线程,只不过协程可以在用户态自由切换任务,提高了执行效率。 对于 Python 很早就支持了协程,python3 还新增了 async/await 这个关键字,内置了对异步 IO 的支持。第三方库 aiohttp 是基于 asyncio 开发的异步 HTTP Client 非常的好用,我们就使用它,来进行协程的开发。
# -*- coding=utf-8 -*-
import time
import aiohttp
import asyncio
from bs4 import BeautifulSoup
t1 = time.time()
urls = [''.format(i) for i in range(0, 226, 25)]
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def job(url):
async with aiohttp.ClientSession() as session:
content = await fetch(session, url)
soup = BeautifulSoup(content, 'html.parser')
item = soup.select(".item")
for i in item:
print(i.select(".title")[0].text)
loop = asyncio.get_event_loop()
tasks = [job(url) for url in urls]
loop.run_until_complete(asyncio.wait(tasks))
print("耗时:", time.time()-t1)
我们看一下耗时:1.4776520729064941
确实节省了时间,跟我们的预期一致。老师也松了一口气。
▍多进程
然后对于并发编程还有一个利器,那就是——进程。进程是一个资源单位,进程本身不是基本运行单位,而是线程的容器,为线程提供资源。
多进程能很好的避开Python GIL 的问题,每个进程都拥有独立的GIL,互不干扰,可实现真正的并行执行,更好的利用多核CPU的资源。所以一般开发并发编程首选多进程,这也是为啥放到最后压轴的原因。
我们看一下实现的代码
# -*- coding=utf-8 -*-
import time
import requests
import multiprocessing
from multiprocessing import Pool
from bs4 import BeautifulSoup
MAX_WORKER_NUM = multiprocessing.cpu_count()
t1 = time.time()
urls = [''.format(
i) for i in range(0, 226, 25)]
def job(url):
r = requests.get(url)
content = r.text
soup = BeautifulSoup(content, 'html.parser')
item = soup.select(".item")
for i in item:
print(i.select(".title")[0].text)
p = Pool(MAX_WORKER_NUM)
for url in urls:
p.apply_async(job, args=(url,))
p.close()
p.join()
print("耗时:", time.time()-t1)
耗时: 0.960860013961792
看到木有,耗时0.9秒,比最原始的版本几乎快了一倍多,这还是任务较少的情况下,如果任务较多情况会更加的明显。
你们以为这就完了么?既然大家都这么熟悉的份上,我再告诉大家一个效率更高的并发编程的方法,那就是 多进程+协程 的模式,我相信看了上面的分享,爱动手的你们一定可以自己动手写出来,写出来以后请在评论区给我留言哦。
学 Python ≠ 100G 视频资料
学 Python ≠ 傻瓜式的在线填空题
只有多写实操项目,才能学的高效。只有建立自己的知识体系,才能学的踏实。
这是我们一直在强调的学习方法,试过的人都学成了,没听进去的人都吃亏了。于是这一次,我们把学习方法做进了课程体系里。
学习顺序按照「学习-测评-实操」进行,强制你多写代码多练习,让你学的高效。
课程按照「建立知识模型-用法必知必会-上手项目案例-系统学习脑图」的知识体系,逐个模块系统递进,既有完整实操项目从零到上线,又有系统脑图的体系化,让你学的踏实。
这是只在「Python 后端工程师培养计划」才有的学习体验。