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')



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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章