Python爬虫第九课:异步、协程与队列

一、协程是什么

情景:
今天是周末,囤了一周的美剧没看,你想在早上出门吃早点的时候把电脑打开下载好几部美剧的新出剧集,你是一个接着一个下载,还是说让他们同时下载呢?

我们一般都是同时开始下载所有的新出剧集,哪一部先下载好了,就先看哪一部;没有下载完的剧集就接着下载。

用计算机语言表示:
在一个任务未完成时,就可以执行其他多个任务,彼此不受影响(在看第一集下载好的美剧时,其他美剧继续保持下载状态,彼此之间不受影响),叫异步

  • 既然有异步;
  • 那也就有同步:同步就是一个任务结束才能启动下一个(类比你看完一集,才能去看下一集美剧)。

显然,异步执行任务会比同步更加节省时间,因为它能减少不必要的等待。如果你需要对时间做优化,异步是一个很值得考虑的方案。
在这里插入图片描述
每当我们爬取一个网页时,都要经过一系列的过程:

  • 发送请求
  • 服务器响应
  • 返回网页
  • 获取数据

因此当我们在同步爬取时,需要等待前一个网页爬取完再爬取下一个网页。而很多时候,由于网络不稳定,加上服务器自身也需要响应的时间,导致爬虫会浪费大量时间在等待上。这也是爬取大量数据时,爬虫的速度会比较慢的原因。
在这里插入图片描述
我们可以采取异步的爬虫方式,让多个爬取网页的任务互相独立, 互不打扰,免去等待时间。显然这样爬虫的效率和速度都会提高。那么,怎样采取异步的方式呢?

二、回顾计算机历史

每台计算机都靠着CPU(中央处理器)干活。

在过去,单核CPU的计算机在处理多任务时,会出现一个问题:每个任务都要抢占CPU,执行完了一个任务才开启下一个任务。CPU毕竟只有一个,这会让计算机处理的效率很低。
在这里插入图片描述
为了解决这样的问题,一种非抢占式的异步技术被创造了出来,这种方式叫多协程。

它的原理是:一个任务在执行过程中,如果遇到等待,就先去执行其他的任务,当等待结束,再回来继续之前的那个任务。在计算机的世界,这种任务来回切换得非常快速,看上去就像多个任务在被同时执行一样。

这就好比你可以边看美剧边吃零食一样,同一时间内即有了视觉上的享受,也有了味觉上的满足。多协程能够再同一段时间内做多件事情。

所以,要实现异步的爬虫方式的话,需要用到多协程。在它的帮助下,我们能实现前面提到的“让多个爬虫替我们干活”。

三、怎么使用多协程

我们先用之前同步的爬虫方式爬取这8个网站,并统计一下所用的时间,然后等下再和gevent异步爬取做一个对比。

扫描二维码关注公众号,回复: 11129491 查看本文章
import requests, time

# 记录程序开始时间
start = time.time()

# 将8个网址封装成列表
url_list = [
    'https://www.baidu.com/',
    'https://www.sina.com.cn/',
    'http://www.sohu.com/',
    'https://www.qq.com/',
    'https://www.163.com/',
    'http://www.iqiyi.com/',
    'https://www.tmall.com/',
    'http://www.ifeng.com/']

# 遍历url_list
for url in url_list:
    requests.get(url)

# 计算程序运行时间
time_taken = time.time()-start
print(time_taken)

# 输出结果:
13.047908067703247

程序运行后,你会看到同步的爬虫方式,是依次爬取网站,并等待服务器响应(状态码为200表示正常响应)后,才爬取下一个网站。比如第一个先爬取了百度的网址,等服务器响应后,再去爬取新浪的网址,以此类推,直至全部爬取完毕。程序运行了13秒多。

如果运用多协程来运行程序又会怎样呢?需要运用到第三方库gevent。

  • Windows系统:pip install gevent
  • Mac系统:pip3 install gevent
# 从geven库导入monkey模块
from gevent import monkey

# monkey.path_all()能把程序变成协作式运行,帮助程序实现异步
monkey.patch_all()

# 导入gevent,requests,time
import gevent
import requests
import time

# 记录程序开始时间
start = time.time()

# 将8个网址封装成列表
url_list = [
    'https://www.baidu.com/',
    'https://www.sina.com.cn/',
    'http://www.sohu.com/',
    'https://www.qq.com/',
    'https://www.163.com/',
    'http://www.iqiyi.com/',
    'https://www.tmall.com/',
    'http://www.ifeng.com/']


# 定义一个crawler函数
def crawler(url):
    response = requests.get(url)
    print(url, time.time() - start, response.status_code)


# 创建空的任务列表
task_list = []

# 遍历url_list
for url in url_list:
    # 使用gevent.spawn创建任务
    task = gevent.spawn(crawler, url)
    task_list.append(task)

# 执行任务列表中的所有任务,让爬虫爬取网站
gevent.joinall(task_list)

# 记录程序结束时间
end = time.time()
print(end - start)

# 输出结果:
https://www.163.com/ 0.7550866603851318 200
https://www.baidu.com/ 0.8124599456787109 200
http://www.ifeng.com/ 1.165616750717163 200
http://www.sohu.com/ 1.745849847793579 200
https://www.qq.com/ 1.8639347553253174 200
https://www.tmall.com/ 2.0335497856140137 200
https://www.sina.com.cn/ 2.3294148445129395 200
http://www.iqiyi.com/ 2.5574638843536377 200
2.5576677322387695

程序运行后,输出了网址、每个请求运行的时间、状态码和爬取8个网站最终所用时间。

通过每个请求运行的时间,我们能知道:爬虫用了异步的方式抓取了8个网站,因为每个请求完成的时间并不是按着顺序来的。比如在我测试运行这个代码的时候,最先爬取到的网站是网易,接着是百度,并不是百度、新浪……的顺序。

每个请求完成时间之间的间隔都非常短,你可以看作这些请求几乎是“同时”发起的。

通过对比同步和异步爬取最终所花的时间,用多协程异步的爬取方式,确实比同步的爬虫方式速度更快。

其实,我们案例爬取的数据量还比较小,不能直接体现出更大的速度差异。如果爬的是大量的数据,运用多协程会有更显著的速度优势。
在这里插入图片描述

from gevent import monkey

从gevent库里导入了monkey模块,这个模块能将程序转换成可异步的程序。

mokney.path_all()

monkey.patch_all(),它的作用就是让程序变成是异步模式。

def crawler(url):
	response = request. get(url)
	print(url,time.time()-start,response.status_code)

我们定义了一个crawler函数,只要调用这个函数,它就会执行【用requests.get()爬取网站】和【打印网址、请求运行时间、状态码】这两个任务。

task = gevent.spawn(crawler,url)

因为gevent只能处理gevent的任务对象,不能直接调用普通函数,所以需要借助gevent.spawn()来创建任务对象。

这里需要注意一点:gevent.spawn()的参数需为要调用的函数名及该函数的参数。比如,gevent.spawn(crawler,url)就是创建一个执行crawler函数的任务,参数为crawler函数名和它自身的参数url。
在这里插入图片描述

gevent.joinall(task_list)

调用gevent库里的joinall方法,能启动执行所有的任务。gevent.joinall(tasks_list)就是执行tasks_list这个任务列表里的所有任务,开始爬取。

在这里插入图片描述
在这里插入图片描述

四、多协程与队列

如果我们要爬的不是8个网站,而是1000个网站,我们可以怎么做?

用我们刚刚学的gevent语法,我们可以用gevent.spawn()创建1000个爬取任务,再用gevent.joinall()执行这1000个任务。

但这种方法会有问题:执行1000个任务,就是一下子发起1000次请求,这样子的恶意请求,会拖垮网站的服务器。

在这里插入图片描述

在gevent库中,有一个模块可以实现这种银行排队办理业务类似的功能——queue模块。

当我们用多协程来爬虫,需要创建大量任务时,我们可以借助queue模块。

queue翻译成中文是队列的意思。我们可以用queue模块来存储任务,让任务都变成一条整齐的队列,就像银行窗口的排号做法。因为queue其实是一种有序的数据结构,可以用来存取数据。

这样,协程就可以从队列里把任务提取出来执行,直到队列空了,任务也就处理完了。就像银行窗口的工作人员会根据排号系统里的排号,处理客人的业务,如果已经没有新的排号,就意味着客户的业务都已办理完毕。
在这里插入图片描述

from gevent import monkey

monkey.patch_all()
import requests, time,gevent
from gevent.queue import Queue

# 程序开始运行的时间
start = time.time()

# 将8个网址封装成列表
url_list = ['https://www.baidu.com/',
            'https://www.sina.com.cn/',
            'http://www.sohu.com/',
            'https://www.qq.com/',
            'https://www.163.com/',
            'http://www.iqiyi.com/',
            'https://www.tmall.com/',
            'http://www.ifeng.com/']

# 创建队列对象,并赋值给work
work = Queue()

# 遍历url列表
for url in url_list:
    # 用put_nowait()函数将url添加进队列中
    work.put_nowait(url)


# 创建crawler函数
def crawler():
    # 当队列不为空时,循环执行下面的程序
    while not work.empty():
        # 从队列中将网址取出
        url = work.get_nowait()
        # 用requests.get抓取网址
        reponse = requests.get(url)
        # 打印网址、队列长度、抓取请求的状态码。
        print(url, work.qsize(), reponse.status_code)

# 创建空的任务列表
task_list = []

# 相当于创建了2个爬虫
for x in range(2):
    # 用gevent.spawn()函数创建执行crawler()函数的任务
    task = gevent.spawn(crawler)
    # 往任务列表添加任务
    task_list.append(task)
# #用gevent.joinall方法,执行任务列表里的所有任务,就是让爬虫开始爬取网站
gevent.joinall(task_list)

end = time.time()

print(end-start)

分别来看一下,各个部分代码的含义:

from gevent import monkey

#从gevent库里导入monkey模块

import gevent

import time

import requests

from gevent.queue import Queue

#从gevent库里导入queue模块

因为gevent库里就带有queue,所以我们用【from gevent.queue import Queue】就能把queue模块导入。

第2部分,是如何创建队列,以及怎么把任务存储进队列里。

monkey.patch_all()

#monkey.patch_all()能把程序变成协作式运行,就是可以帮助程序实现异步。

start = time.time()



url_list = ['https://www.baidu.com/',

'https://www.sina.com.cn/',

'http://www.sohu.com/',

'https://www.qq.com/',

'https://www.163.com/',

'http://www.iqiyi.com/',

'https://www.tmall.com/',

'http://www.ifeng.com/']



work = Queue()

#创建队列对象,并赋值给work

for url in url_list:

#遍历url_list

    work.put_nowait(url)

    #用put_nowait()函数可以把网址都放进队列里

用Queue()能创建queue对象,相当于创建了一个不限任何存储数量的空队列。如果我们往Queue()中传入参数,比如Queue(10),则表示这个队列只能存储10个任务。

创建了queue对象后,我们就能调用这个对象的put_nowait方法,把我们的每个网址都存储进我们刚刚建立好的空队列里。

work.put_nowait(url)这行代码就是把遍历的8个网站,都存储进队列里。

第3部分,是定义爬取函数,和如何从队列里提取出刚刚存储进去的网址。

def crawler():

    while not work.empty():

    #当队列不是空的时候,就执行下面的程序

        url = work.get_nowait()

        #用get_nowait()函数可以把队列里的网址都取出

        r = requests.get(url)

        #用requests.get()函数抓取网址

        print(url,work.qsize(),r.status_code)

        #打印网址、队列长度、抓取请求的状态码

这里定义的crawler函数,多了三个之前没见过的代码:

  1. while not work.empty()
  2. url = work.get_nowait()
  3. work.qsize()

这三个代码涉及到queue对象的三个方法:

  • empty方法,是用来判断队列是不是空了的;
  • get_nowait方法,是用来从队列里提取数据的;
  • qsize方法,是用来判断队列里还剩多少数量的。

在这里插入图片描述
在这里插入图片描述
接下来就是用爬虫通过多协程爬取网站:

tasks_list  = [ ]

#创建空的任务列表

for x in range(2):

#相当于创建了2个爬虫

    task = gevent.spawn(crawler)

    #用gevent.spawn()函数创建执行crawler()函数的任务

    tasks_list.append(task)

    #往任务列表添加任务。

gevent.joinall(tasks_list)

#用gevent.joinall方法,执行任务列表里的所有任务,就是让爬虫开始爬取网站

end = time.time()

print(end-start)

在这里插入图片描述
这里创建了两只可以异步爬取的爬虫。

它们会从队列里取走网址,执行爬取任务。一旦一个网址被一只爬虫取走,另一只爬虫就取不到了,另一只爬虫就会取走下一个网址。直至所有网址都被取走,队列为空时,爬虫就停止工作。

五、总结

多协程,是一种非抢占式的异步方式。使用多协程的话,就能让多个爬取任务用异步的方式交替执行。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、练习

  1. 利用多协程和队列,来爬取豆瓣图书Top250(书名,作者,评分)并存储csv 豆瓣图书:https://book.douban.com/top250?start=0。
from gevent import monkey

monkey.patch_all()
import requests
import csv
import time
import gevent
from bs4 import BeautifulSoup
from gevent.queue import Queue

# 程序开始运行的时间
start = time.time()

# 创建csv文件,并以写入模式打开
file = open('db_article_top250.csv', 'w', newline='')
file_writer = csv.writer(file)
# 写入表头
file_writer.writerow(['标题', '作者','评价信息', '书籍简介', 'url'])

headers = {
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'}
url = 'https://book.douban.com/top250'
# 豆瓣url变化规律,起始页为0,间隔25
page_size = 25
page_start = 0

# 创建队列对象
work = Queue()

for i in range(10):
    # 请求参数
    params = {'start': page_start + i * page_size}
    # 将请求参数放入队列中
    work.put_nowait(params)


# 定义函数
def crawler():
    while not work.empty():
        # 从队列中依次取出请求参数
        param = work.get_nowait()
        # 使用requests获取数据
        response = requests.get(url, headers=headers, params=param)
        # 转为bs对象
        bs_response = BeautifulSoup(response.text, 'html.parser')
        # 找到书籍信息列表
        list_books = bs_response.find_all('tr',class_='item')
        # 遍历书籍信息列表
        for book in list_books:
            title = book.find(class_='pl2').find('a').text.replace(' ','').replace('\n','')
            author = book.find('p',class_='pl').text.replace(' ','').replace('\n','')
            rating = book.find('div',class_='star clearfix').text.replace(' ','').replace('\n','')
            url_article = book.find(class_='pl2').find('a')['href']
            # 书籍简介有部分是空值,无法返回数据,采取异常捕捉的方式,让程序正常运行
            try:
                info = book.find('p', class_='quote').text.replace(' ', '').replace('\n', '')
            except:
                info = None
            # 打印,以便查出问题
            print(title, rating, info, url_article)
            # 将书籍信息写入csv文件中
            file_writer.writerow([title, author, rating, info, url_article])


# 创建空的任务列表
task_list = []
# 创建3只爬虫
for x in range(3):
    # 创建gevent任务对象
    task = gevent.spawn(crawler)
    # 将任务逐个存入任务列表中
    task_list.append(task)

# 执行任务列表
gevent.joinall(task_list)
# 结束时间
end = time.time()
print(end - start)
# 关闭文件
file.close()

# 程序运行时间:3.62秒
  1. 健身要知道食物热量,让我们来找到这些食物的热量指标吧。
from gevent import monkey
from bs4 import BeautifulSoup
from gevent.queue import Queue
import gevent
import requests
from openpyxl import load_workbook, Workbook, worksheet
import time


# 定义写入数据的函数
def save_data(li):
    try:
        # 读取文件
        wb = load_workbook('food_result.xlsx')
    except:
        # 若文件不存在,则创建工作簿
        wb = Workbook()
    finally:
        try:
            ws = wb[li[0][0]]
        except:
            # 若工作表不存在,则创建一个当前类别命名的工作表
            ws = wb.create_sheet(li[0][0], index=0)
            ws.append(['类别', '食物名称', '热量', '链接'])
        finally:
            for x in li:
                # 按行写入数据
                ws.append(x)
    wb.save('result.xlsx')
    wb.close()


# 让程序变成异步模式
monkey.patch_all()

# 创建队列
work = Queue()

# 程序开始时间
start = time.time()

# 根据网址规律,设定url和请求参数
for i in range(1, 4):
    url = 'https://food.hiyd.com/list-{}-html?'.format(i)
    for x in range(1, 4):
        param = 'page={}'.format(x)
        # 合成url
        url_big = url + param
        # 将url放入队列中
        work.put_nowait(url_big)

# 最后一个种类的的编号与之前的规律不一致,故需重新设定
for y in range(1, 4):
    url_small = 'https://food.hiyd.com/list-132-html?page={}'.format(y)
    # 将这部分的url也放入队列中
    work.put_nowait(url_small)
# 打印队列,是一个列表
print(work)


# 定义爬取函数
def crawler(job):
    # 请求头
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'}
    # 当队列不为空时,循环执行
    while not work.empty():
        # 从队列中,提取url
        url_hi = work.get_nowait()
        # 请求数据
        response = requests.get(url_hi, headers=headers)
        # 转为bs对象
        bs_response = BeautifulSoup(response.text, 'html.parser')
        main_food = bs_response.find('div', class_='box-hd').text.replace(' ', '').replace('\n', '')
        list_food = bs_response.find('div', class_='box-bd').find_all('li')
        # 创建空列表用于存储结果
        result_list = []
        # 遍历食物列表
        for food in list_food:
            name = food.find('h3').text.replace(' ', '').replace('\n', '')
            carolie = food.find('p').text.replace(' ', '').replace('\n', '')
            url_food = 'https:' + food.find('a')['href']
            print([main_food, name, carolie, url_food])
            result_list.append([main_food, name, carolie, url_food])
        save_data(result_list)


# 创建空任务列表
task_list = []
# 创建3个爬虫
for y in range(3):
    # 创建gevent对象
    task = gevent.spawn(crawler(work))
    task_list.append(task)

# 执行任务列表
gevent.joinall(task_list)
# 任务结束时间
end = time.time()
print(end - start)
from gevent import monkey

# 将程序变成多协程运行
monkey.patch_all()
import requests
import gevent
from gevent.queue import Queue
from bs4 import BeautifulSoup

# 创建队列
work = Queue()

url_1 = 'http://www.boohee.com/food/group/{type}?page={page}'

for i in range(1, 4):
    for j in range(1, 4):
        real_url = url_1.format(type=i, page=j)
        # 将url放入队列中
        work.put_nowait(real_url)


# 设定爬取函数
def crawler():
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'}
    # 当队列不为空时,循环执行
    while not work.empty():
        # 从队列中取出url
        url = work.get_nowait()
        response = requests.get(url, headers=headers)
        # 创建文件,并以追加模式打开
        with open('bh_food.txt', 'a+') as myfile:
            # 将爬取的网页源代码存入文件中
            myfile.write(response.text)

# 创建任务列表
task_list = []
# 创建3只爬虫
for x in range(3):
    # 创建gevent对象
    task = gevent.spawn(crawler)
    # 将任务存入任务列表
    task_list.append(task)

# 执行任务列表
gevent.joinall(task_list)

file = open('bh_food.txt','r')
file_reader = file.read()

bs_response = BeautifulSoup(file_reader,'html.parser')
category = bs_response.find('div',class_='widget-food-list pull-right').find('h3').text.replace(' ','').replace('\n','')
list_food = bs_response.find_all('li',class_='item clearfix')
for food in list_food:
    name = food.find('h4').text.replace(' ','').replace('\n','')
    carolie = food.find('p').text.replace(' ','').replace('\n','')
    url_food = 'http://www.boohee.com'+food.find('a')['href']
    print(category,name,carolie,url_food)
    
发布了35 篇原创文章 · 获赞 4 · 访问量 5290

猜你喜欢

转载自blog.csdn.net/fightingoyo/article/details/104711229