python爬虫基础Ⅳ——多协程:爬取食物热量



基础爬虫部分Ⅳ

(1) 协程是什么

我们前面爬取的数据都不算大,如果我们想要爬取的是成千上万条的数据,那么就会遇到一个问题:因为程序是一行一行依次执行的缘故,要等待很久,我们才能拿到想要的数据。

既然一个爬虫爬取大量数据要爬很久,那我们能不能让多个爬虫一起爬取?

如果我们把同步(例如先煮完饭饭熟了才能开始炒菜)与异步(又如饭没熟那就先去炒菜)的概念迁移到网络爬虫的场景中,那我们之前学的爬虫方式都是同步的。

爬虫每发起一个请求,都要等服务器返回响应后,才会执行下一步。而很多时候,由于网络不稳定,加上服务器自身也需要响应的时间,导致爬虫会浪费大量时间在等待上。这也是爬取大量数据时,爬虫的速度会比较慢的原因。

有没有方法可以采取异步的爬虫方式,让多个爬虫在执行任务时保持相对独立,彼此不受干扰,这样不就可以免去等待时间?显然这样爬虫的效率和速度都会提高。

要实现异步的爬虫方式的话,需要用到多协程


(2) gevent库

怎么异步爬虫呢?用gevent库(需安装),能让我们在Python中实现多协程。

现在爬取8个网站(包括百度、新浪、搜狐、腾讯、网易、爱奇艺、天猫、凤凰)来看看同步与异步的速度上的差距:

# -*- coding:utf-8 -*-
import requests,time

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/']
#把8个网站封装成列表


for url in url_list:
    r = requests.get(url)
    print(url,r.status_code) #请求8个网站,打印网址和抓取请求的状态码

end = time.time()
#记录程序结束时间
print(end-start)
#end-start是结束时间减去开始时间,就是最终所花时间。

在本地运行结果是:

https://www.baidu.com/ 200
https://www.sina.com.cn/ 200
http://www.sohu.com/ 200
https://www.qq.com/ 200
https://www.163.com/ 200
http://www.iqiyi.com/ 200
https://www.tmall.com/ 200
http://www.ifeng.com/ 200
2.2718516063690186

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


下面看看多协程爬取:

# -*- coding:utf-8 -*-
from gevent import monkey
monkey.patch_all()
import gevent,time,requests

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/']

def crawler(url):
    r = requests.get(url)
    print(url,time.time()-start,r.status_code)

tasks_list = []

for url in url_list:
    task = gevent.spawn(crawler,url)
    tasks_list.append(task)

gevent.joinall(tasks_list)
end = time.time()
print(end-start)

在本地运行结果是:

https://www.baidu.com/ 0.2156848907470703 200
http://www.iqiyi.com/ 0.2506403923034668 200
http://www.sohu.com/ 0.2510659694671631 200
https://www.163.com/ 0.30031681060791016 200
https://www.qq.com/ 0.347592830657959 200
https://www.tmall.com/ 0.375349760055542 200
https://www.sina.com.cn/ 0.5546746253967285 200
http://www.ifeng.com/ 0.6207404136657715 200
0.6208701133728027

通过对比同步和异步爬取最终所花的时间,用多协程异步的爬取方式,确实比同步的爬虫方式速度更快。其实,这里爬取的数据量还比较小,不能直接体现出更大的速度差异。如果爬的是大量的数据,运用多协程会有更显著的速度优势。

而且每个请求完成的时间并不是按着url在列表里顺序来的。比如在我运行这个代码的时候,最先爬取到的是百度,接着是爱奇艺而不是新浪。


(3) 使用多协程

一步步讲(╹▽╹)

1. 把程序设置为多协作式运行
from gevent import monkey
#从gevent库里导入monkey模块。

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

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

gevent库里导入了monkey模块,这个模块能将程序转换成可异步的程序。monkey.patch_all(),它的作用其实就像你的电脑有时会弹出“是否要用补丁修补漏洞或更新”一样。它能给程序打上补丁,让程序变成是异步模式,而不是同步模式。它也叫“猴子补丁”。

注意:在导入其他库和模块前,先把monkey模块导入进来,并运行monkey.patch_all()。


2. 定义爬取函数
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/']
#把8个网站封装成列表。

def crawler(url):
#定义一个crawler()函数。
    r = requests.get(url)
    #用requests.get()函数爬取网站。
    print(url,time.time()-start,r.status_code)
    #打印网址、请求运行时间、状态码。

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


3. 用gevent.spawn()创建任务
tasks_list = [ ]
#创建空的任务列表。

for url in url_list:
#遍历url_list。
    task = gevent.spawn(crawler,url)
    #用gevent.spawn()函数创建任务。
    tasks_list.append(task)
    #往任务列表添加任务。
gevent.joinall(tasks_list)
#调用gevent库里的joinall方法,能启动执行任务列表里所有的任务,就是让爬虫开始爬取网站。
end = time.time()
#记录程序结束时间。
print(end-start)
#打印程序最终所需时间。

上面第六行代码task = gevent.spawn(crawler,url),因为gevent只能处理gevent的任务对象不能直接调用普通函数,所以需要借助gevent.spawn()创建任务对象

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

这行代码gevent.joinall(tasks_list)就是执行tasks_list这个任务列表里的所有任务,开始爬取。

最后再打印时间。


(4) queue模块和协程配合

那如果我们要爬的不是8个网站,而是1000个网站,用我们刚刚学的gevent语法,我们可以用gevent.spawn()创建1000个爬取任务,再用gevent.joinall()执行这1000个任务。但是这样子的恶意请求,会拖垮网站的服务器!

那我们能不能只创建成5个任务,但每个任务爬取200个网站呢?像下面这样写:

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

start = time.time()
url_list = [ 
    #假设有1000个网址 
]

def crawler(url_list): #定义一个crawler()函数。参数为url列表
    for url in url_list:
        r = requests.get(url)
        print(url,time.time()-start,r.status_code)

tasks_list = [ ]
#创建空的任务列表。
for i in range(5):
    task = gevent.spawn(crawler,url_list[i*200:(i+1)*200])
    #用gevent.spawn()函数创建5个任务。
    tasks_list.append(task)
    #往任务列表添加任务。

gevent.joinall(tasks_list)
end = time.time()
print(end-start)

遗憾地说,就算用gevent.spawn()创建了5个分别执行爬取200个网站的任务,这5个任务之间虽然是异步执行的,但是每个任务(爬取200个网站)内部又是同步的。

这意味着:如果有一个任务在执行的过程中,它要爬取的一个网站一直在等待响应,哪怕其他任务都完成了200个网站的爬取,它也还是不能完成200个网站的爬取。


可以使用队列来解决,用queue模块来存储任务,让任务都变成一条整齐的队列,这样,协程就可以从队列里把任务提取出来执行,直到队列空了,任务也就处理完了。

怎么用queue模块和协程配合,依旧以抓取8个网站为例,看注释:

from gevent import monkey
monkey.patch_all()
import gevent,time,requests
from gevent.queue import Queue #从gevent库里导入队列
#因为gevent库里就带有queue,所以我们用【from gevent.queue import Queue】就能把queue模块导入。

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:
    work.put_nowait(url) #用put_nowait()函数可以把网址都放进队列里。

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) #打印网址、队列长度、抓取请求的状态码。

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)

在本地运行结果:

https://www.baidu.com/ 6 200
http://www.sohu.com/ 5 200
https://www.sina.com.cn/ 4 200
https://www.163.com/ 3 200
https://www.qq.com/ 2 200
http://www.iqiyi.com/ 1 200
http://www.ifeng.com/ 0 200
https://www.tmall.com/ 0 200
1.2389874458312988

网址后面的数字指的是队列里还剩的任务数,比如第一个网址后面的数字6,就是此时队列里还剩6个抓取其他网址的任务。

我们相当于创建了两只可以异步爬取的爬虫(前边没用队列爬取8个网站那里相当于创建了八个可以异步爬取的爬虫,每个爬虫爬取一个网站)。它们会从队列里取走网址,执行爬取任务。一旦一个网址被一只爬虫取走,另一只爬虫就取不到了,它会取走下一个网址。直至所有网址都被取走,队列为空时,爬虫就停止工作。


(5) 实例:爬取食物热量

可以练练用多协程爬取薄荷网的食物热量,网址在这http://www.boohee.com/food/

爬取前5个分类中,每个分类前2页食物的【食物名、热量、详情链接】,数据储存到本地。

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
from gevent import monkey
monkey.patch_all() #让程序变成异步模式。
import gevent,requests, openpyxl, time
from gevent.queue import Queue
from bs4 import BeautifulSoup

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
def getfood(url): #获取url页面的食物信息
    foodlist = []
    res = requests.get(url,headers=headers)
    bs4 = BeautifulSoup(res.text, 'html.parser')
    items = bs4.find_all('li', class_='clearfix')
    for item in items:
        detail = item.find('div', class_='text-box')
        name = detail.find('a')['title']
        href = 'http://www.boohee.com'+detail.find('a')['href']
        heat = detail.find('p').text
        # print(name,href,heat)
        foodlist.append([name, heat, href])
    return foodlist

def savefood(items): #储存食物信息
    for item in items:
        sheet.append(item)

#存入excel
wb = openpyxl.Workbook()
sheet = wb.active
sheet.title = 'food heat'
sheet['A1'] = '食物'
sheet['B1'] = '热量'
sheet['C1'] = '详情链接'

work = Queue() #构造url队列
url = 'http://www.boohee.com/food/group/{type}?page={page}'
for group in range(1,6): #前五个分类
    for page in range(1,3): #前两页
        work.put_nowait(url.format(type=group, page=page))

def crawler():
    while not work.empty():
        list1 = getfood(work.get_nowait())
        savefood(list1)

task_list = []
for i in range(5): #创建5个爬虫
    task = gevent.spawn(crawler)#用gevent.spawn()函数创建执行crawler()函数的任务。
    task_list.append(task)#往任务列表添加任务

workstart = time.time()
gevent.joinall(task_list)
#用gevent.joinall方法,启动协程,执行任务列表里的所有任务,让爬虫开始爬取网站。
workend = time.time()
print('操作完成,用时:'+str(workend-workstart))

wb.save('food heat.xlsx')



————————每个人都在抱怨生活不易,可是都在默默为生活打拼————————

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值