多線程爬蟲與異步爬蟲的性能測試

如何提升爬蟲的性能

如果你使用過爬蟲框架scrapy,那么你多多少少會驚異于她的并發(fā)和高效。
在scrapy中,你可以通過在settings中設置線程數(shù)來輕松定制一個多線程爬蟲。這得益于scrappy的底層twisted異步框架。
異步在爬蟲開發(fā)中經(jīng)常突顯奇效,因為他可以是單個鏈接爬蟲不堵塞。
不阻塞可以理解為:在A線程等待response的時候,B線程可以發(fā)起requests,或者C線程可以進行數(shù)據(jù)處理。
要單個爬蟲線程不阻塞,python可以使用到的庫有:

  • threading
  • gevent
  • asyncio

一個常規(guī)的阻塞爬蟲

下面的代碼實現(xiàn)了一個獲取 貓眼電影top100 的爬蟲,網(wǎng)站反爬較弱,帶上UA即可。
我們給爬蟲寫一個裝飾器,記錄其爬取時間。

import requests
import time
from lxml import etree
from threading import Thread
from functools import cmp_to_key


# 給輸出結(jié)果排序
def sortRule(x, y):
    for i in x.keys():
        c1 = int(i)
    for i in y.keys():
        c2 = int(i)
    if c1 > c2:
        return 1
    elif c1 < c2:
        return -1
    else:
        return 0


# 計算時間的裝飾器
def caltime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        print("costtime: ", time.time() - start)

    return wrapper


# 獲取頁面
def getPage(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
        # 'Cookie': '__mta=141898381.1589978369143.1590927122695.1590927124319.9; uuid_n_v=v1; uuid=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; _csrf=f36a7050eb60429b197a902b4f1d66317db95bde0879648c8bff0e8237e937de; Hm_lvt_703e94591e87be68cc8da0da7cbd0be2=1589978364; mojo-uuid=8b4dad0e1f472f08ffd3f3f67b75f2ab; _lxsdk_cuid=17232188c2f0-022085e6f29b1b-30657c06-13c680-17232188c30c8; _lxsdk=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; mojo-session-id={"id":"afcd899e03fe72ca70e34368fe483d15","time":1590927095603}; __mta=141898381.1589978369143.1590063115667.1590927111235.7; mojo-trace-id=10; Hm_lpvt_703e94591e87be68cc8da0da7cbd0be2=1590927124; _lxsdk_s=1726aa4fd86-ba9-904-221%7C%7C15',
    }
    try:
        resp = requests.get(url=url, headers=headers)
        if resp.status_code == 200:
            return resp.text
        return None
    except Exception as e:
        print(e)
        return None


# 獲取單個頁面數(shù)據(jù)
def parsePage(page):
    if not page:
        yield
    data = etree.HTML(page).xpath('.//dl/dd')
    for d in data:
        rank = d.xpath("./i/text()")[0]
        title = d.xpath(".//p[@class='name']/a/text()")[0]
        yield {
            rank: title
        }


# 調(diào)度
def schedule(url, f):
    page = getPage(url)
    for data in parsePage(page):
        f.append(data)


# 數(shù)據(jù)展示
def show(f):
    f.sort(key=cmp_to_key(sortRule))
    for x in f:
        print(x)


@caltime
def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    f = []
    for url in urls:
        schedule(url, f)
    show(f)


if __name__ == '__main__':
    main()

成功爬取完top100平均花費2.8s左右。
這個爬蟲程序總共有10個小的爬蟲線程,每個爬蟲線程爬取10條數(shù)據(jù)。當前面的線程未成功收到response時,后面所有的線程都阻塞了。
這也是這個爬蟲程序低效的原因。因為線程之間有明確的先后順序,后面的線程無法越過前面的線程發(fā)送請求。


threading打破線程的優(yōu)先級?

接下來我們使用多線程打破這種優(yōu)先順序。修改main函數(shù)

def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    threads = []
    f = []
    for url in urls:
        # schedule(url, f)
        t = Thread(target=schedule, args=(url, f))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    show(f)

記得導入threading庫

from threading import Thread

點擊運行,發(fā)現(xiàn)時間縮短為0.4s,性能的提升還是很客觀的。
threading的作用在于開啟了多個線程,每個線程同時競爭GIL,當拿到GIL發(fā)出requests后。該線程又立即釋放GIL。進入等待Response的狀態(tài)。
釋放掉的GIL又馬上被其他線程獲取...如此以來,每個線程都是平等的,無先后之分??雌饋砭秃孟裢瑫r進行著(實際并不是,因為GIL的原因)。
所以效率大大提升了。


gevent異步協(xié)程搞一波?

gevent是一個優(yōu)先的異步網(wǎng)絡庫,可以輕松支持高并發(fā)的網(wǎng)絡訪問。我們現(xiàn)在試著把阻塞的爬蟲加上gevent試試

@caltime
def main():
    threads = []
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    f = []
    for url in urls:
        threads.append(gevent.spawn(schedule, url, f))
    gevent.joinall(threads)
    show(f)

同樣這里也要導入gevent庫

import gevent
from gevent import monkey
monkey.patch_all()

點擊運行,平均時間在0.45上左右,和多線程差不多。


新版異步庫ascyncio搞一波?

ascyncion是python前不久剛推出的基于協(xié)程的異步庫,號稱最有野心的庫。要使ascyncio支持我們的程序,必須對getPage做點修改:
因為requests是不支持異步的,所以我們這里使用aiohttp庫替換requests,并用它來實現(xiàn)getPage函數(shù)。

# 異步requests
async def getPage(url):
    headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'}
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers = headers) as resp:
            return await resp.text()

main函數(shù)也需要修改

@caltime
def main():
    urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
    loop = asyncio.get_event_loop()
    f = []
    threads = []
    for url in urls:
        threads.append(schedule(url,f))
    loop.run_until_complete(asyncio.wait(threads))
    show(f)

記得導入相關(guān)庫

import asyncio
import aiohttp

點擊運行,平均時間在0.35左右,性能稍優(yōu)于多線程和gevent一點。


結(jié)語

對于爬蟲技術(shù),其實有些比較新的東西是值得去了解一下的。比如:

  • 提升并發(fā)方面:asyncio, aiohttp
  • 動態(tài)渲染:pyppeteer(puppeteer的python版,支持異步)
  • 驗證碼破解:機器學習,模型訓練

還有一些數(shù)據(jù)解析方面的工具性能大概如下:

  • re > lxml > bs4
  • 但是即便是同一種解析方法,不同工具實現(xiàn)的,性能也不一樣。比如同樣是xpath,lxml的性能略好于parsel(scrapy團隊開發(fā)的數(shù)據(jù)解析工具,支持css,re,xpath)的。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容