如何讓你寫的爬蟲速度像坐火箭一樣快【并發(fā)請求】

開坑個新系列梯轻,主要面向新手梯刚,老司機(jī)可以忽略及皂。

這個系列內(nèi)的文章將會讓你知道如何做到讓你寫的爬蟲在運行的時候速度能像火箭一樣快圃郊!

很多初學(xué)爬蟲的朋友對于這方面的知識似乎是空白的价涝,甚至還有一些在爬蟲崗位上工作了一兩年的人也搞不清楚在不使用爬蟲框架的情況下,如何寫出一個速度足夠快的爬蟲持舆,而網(wǎng)上的文章大多是基于多進(jìn)程/Gevent來寫的色瘩,代碼看起來就極其復(fù)雜,甚至有些人抄來抄去連多進(jìn)程和多線程沒搞清楚逸寓,如果是一個想學(xué)習(xí)這方面知識的人看到了這樣的文章居兆,多半會一臉懵逼。

綜上所述竹伸,為了讓關(guān)注我公眾號的新手朋友們能快速掌握這些技巧泥栖,這個系列就這樣誕生了~

話不多說,我們正式開始勋篓。在提升爬蟲的速度這方面吧享,最基礎(chǔ)、最有效譬嚣、最直接的操作是什么呢钢颂?沒錯,就是并發(fā)請求孤荣,如果你的爬蟲整個邏輯是順序執(zhí)行的甸陌,請求的時候永遠(yuǎn)不會并發(fā)须揣,那么你就會遇到像他這樣的情況:《小白寫了個壁紙的爬蟲,能跑起來钱豁,但是感覺很慢耻卡,不知道怎么回事,請大佬指點》牲尺。

上面這是我昨天刷V2的時候看到的一個帖子卵酪,樓主的代碼內(nèi)容簡單概括一下就完全是順序執(zhí)行的,每下載一個圖片都需要等待當(dāng)前這個圖片下載完了才能繼續(xù)下載下一個谤碳,這樣子做當(dāng)然會非常慢了溃卡!這篇文章就拿他的代碼作為樣例,在原來的基礎(chǔ)上進(jìn)行一些調(diào)整蜒简,從而讓他寫的這個爬蟲的運行速度能從龜爬變成像坐火箭一樣快瘸羡!


首先,我們需要知道什么是并發(fā)搓茬,這里的并發(fā)指的是“并行發(fā)送請求”犹赖,意思就是一次性發(fā)出多個請求,從而達(dá)到節(jié)省時間的效果卷仑!那么并發(fā)和不并發(fā)的區(qū)別在哪呢峻村?簡單來說就是這樣子的:

把爬蟲比喻成工人,在不并發(fā)的情況下锡凝,一個工人一次只能做一件事情粘昨,所以必須要下載完一個圖片才能繼續(xù)下載下一個。

順序執(zhí)行的情況

而在并發(fā)的情況下窜锯,就有很多個工人一起在干活张肾,每個工人都被分配了一件事情做,所以可以同時下載多個圖片衬浑,速度自然就快了很多捌浩。

并發(fā)的情況

當(dāng)然,上面說的這個例子只是從一個宏觀的角度上來看并發(fā)工秩,實際在做的時候要讓你的爬蟲能并發(fā)請求的方式是分為多線程尸饺、多進(jìn)程、協(xié)程三種的助币,并不是每一種方式在運行時的效果都像上面說的這樣浪听,這里先不做深入探討,因為這不是本文的重點眉菱。我們現(xiàn)在只需要知道迹栓,只要能讓爬蟲并發(fā)請求,就能同時下載多個圖片俭缓,讓速度快得飛起克伊,這樣就夠了酥郭。


那么我們要用上面說的三種方式里的哪一種來實現(xiàn)并發(fā)請求呢?這還用問嗎愿吹?當(dāng)然是選擇代碼最簡單不从、改動最小,并且最容易看懂的協(xié)程袄绻颉椿息!在Python3.4之后Python就引入了一個叫做asyncio的庫,原生支持了異步IO坷衍,而在3.5之后Python又支持了asyncawait這兩個語法寝优,使得寫異步代碼可以像寫同步代碼一樣簡單易讀。

剛剛又提到了兩個詞枫耳,同步和異步乏矾,這兩個詞的含義其實就跟上面的并發(fā)差不多,同步代碼就是順序執(zhí)行的嘉涌,而異步則不是妻熊,這里同樣不做深入探討,先知道有這么個東西就行了仑最。

看到這里肯定會有人開始有疑問了,雖然前面說我們要用協(xié)程來實現(xiàn)并發(fā)請求帆喇,但是后面說的卻是什么Python支持原生異步警医,那么這個異步跟協(xié)程的關(guān)系又是什么呢?

其實很簡單坯钦,協(xié)程可以讓你寫異步代碼的時候能像寫同步代碼一樣簡單预皇,在Python3中寫協(xié)程代碼的核心語法就是asyncawait這兩個,舉個簡單的例子吧:

def func():
    print(1)
    time.sleep(10)
    print(2)

這是一段普通的函數(shù)婉刀,它屬于同步代碼吟温,里面的time.sleep是普通函數(shù),也屬于同步代碼突颊。

async def func():  # 調(diào)用協(xié)程函數(shù)的那個函數(shù)也需要是一個協(xié)程函數(shù)
    print(1)
    await asyncio.sleep(10)  # 調(diào)用協(xié)程函數(shù)的時候要在前面加await
    print(2)

而這是一個協(xié)程函數(shù)鲁豪,它屬于異步代碼,里面的asyncio.sleep是協(xié)程函數(shù)律秃,也屬于異步代碼爬橡。

它們的區(qū)別顯而易見,用協(xié)程來寫異步代碼棒动,除了需要換成異步的庫以外糙申,就只是多了個asyncawait而已船惨,是不是非常簡單柜裸?


那么我們在了解了怎么寫協(xié)程代碼之后缕陕,就能開始優(yōu)化那段慢成龜速的代碼了嗎?答案是否定的疙挺,那段代碼中使用了requests庫進(jìn)行網(wǎng)絡(luò)請求扛邑,而requests是一個同步庫,不能在異步環(huán)境下使用衔统;同樣鹿榜,文件操作用的openfile.write也是同步的,也不能在異步環(huán)境下使用锦爵。

所以在開始之前我們還需要了解兩個庫舱殿,分別是aiohttp和aiofiles,aiohttp是一個異步網(wǎng)絡(luò)請求庫险掀,而aiofiles是一個異步文件操作庫沪袭。(aiofiles是基于線程池實現(xiàn)的,并不是真正的原生異步樟氢,但問題不大冈绊,不影響使用)

切記,異步代碼不能與同步代碼混用埠啃,否則如果同步代碼耗時過長死宣,異步代碼就會被阻塞,失去異步的效果碴开。而網(wǎng)絡(luò)請求和文件操作是整個流程中最耗時的部分毅该,所以我們必須使用異步的庫來進(jìn)行操作!否則就白搞了潦牛!

好了眶掌,先來看看aiohttp的用法吧,官方文檔上的示例大致如下:

async with aiohttp.ClientSession() as session:
    async with session.get(url) as resp:
        result = await resp.text()

是不是覺得很麻煩巴碗,不像requests庫那么方便朴爬?還覺得兩層async with很丑?有沒有辦法讓它像requests庫一樣方便呢橡淆?

答案是有的召噩,有一個叫作aiohttp-requests的庫,它能讓上面的這段代碼變成這樣:

resp = await requests.get(url)
result = await resp.text()

清爽多了對吧明垢?我們等下就用它了蚣常!記得裝這個庫的前提是要先裝aiohttp哦!

然后我們來看看aiofiles的用法痊银,官方文檔上的示例如下:

async with aiofiles.open('filename', mode='r') as f:
    contents = await f.read()
print(contents)

嗯抵蚊,這個用起來就和用同步代碼操作文件差不多了,沒啥可挑剔的,直接用就完事了贞绳。

提示:aiohttp-requests默認(rèn)是創(chuàng)建并使用了session的谷醉,對于一些需要不保留Cookie進(jìn)行請求的場景需要自己實例化一個Requests類,并指定cookie_jar為aiohttp.DummyCookieJar冈闭。


了解完了要用的庫之后我們就可以開始對貼子中的代碼進(jìn)行魔改了俱尼,如果你用的不是Python3.5以上版本的話需要先準(zhǔn)備一下環(huán)境。除了版本號大于等于3.5的Python以外萎攒,你還需要安裝以下幾個庫:

  • aiohttp(異步網(wǎng)絡(luò)請求庫)
  • aiohttp-requests(讓aiohttp用起來更方便的庫)
  • aiofiles(異步文件操作庫)
  • pillow(其實就是PIL庫遇八,代碼中的圖片操作有用到)

執(zhí)行一下pip install aiohttp aiohttp-requests aiofiles pillow一次性裝完,如果存在多個不同版本的Python環(huán)境記得區(qū)分好耍休。


然后我們打開編輯器刃永,開始改代碼,首先調(diào)整一下導(dǎo)包的部分羊精,將里面的requests替換成aiohttp-requests斯够,像這樣:

image

然后搜索一下requests,看看哪些地方用到了它喧锦。

image

接著把所有搜到的部分都給改成異步請求的读规。

image

同時不要忘了將所有調(diào)用過requests.get的函數(shù)都變成協(xié)程函數(shù)。

image

然后我們把文件操作的部分也換成異步的燃少,使用aiofiles.open代替open束亏。

image

最主要的部分都換好了,接著我們將原先在if __name__ == '__main__':下的代碼移到一個新寫的協(xié)程函數(shù)run中阵具,并且將調(diào)用前面協(xié)程函數(shù)的部分都加上await枪汪。

image

再導(dǎo)入一下asyncio庫,然后在if __name__ == '__main__':下寫出這樣的代碼:

image

上面這個是Python3.7之后才能用的寫法怔昨,低于Python3.7要這樣寫:

image

現(xiàn)在我們就可以運行一下看看修改后的代碼能不能跑通了。

image

這里報了個錯宿稀,從錯誤堆棧中可以看出問題是出在response = await requests.get(url=url, headers=headers)這里的趁舀,原因是self.session._request方法沒有key為url的參數(shù)。這個問題很好解決祝沸,只需要將url=url變成url就好了(本來也就沒必要這么指定參數(shù)寫)矮烹。將代碼中所有用到requests.get并且存在url=url這種寫法的都做一下調(diào)整:

image

調(diào)整完之后再運行一次就正常了,效果和原先的代碼相同罩锐。

image

注意奉狈!僅僅是這樣并不會讓速度發(fā)生很大的變化!我們最后還需要將這一堆代碼中最耗時且是順序執(zhí)行涩惑、沒有并發(fā)請求的部分單獨放到一個協(xié)程函數(shù)中仁期,并且用asyncio.gather來并發(fā)調(diào)用(由于原本的邏輯較為混亂,這里除了并發(fā)請求以外還進(jìn)行了一些其他的微調(diào),主要是計數(shù)和文件路徑的部分跛蛋,無關(guān)緊要)熬的。

image

運行一下看看效果,剛運行起來一瞬間就刷了一排的下載完成赊级,跟修改之前比起來簡直是天差地別押框。

image

這就是并發(fā)請求的威力!我們僅僅是對他原本的代碼進(jìn)行了一些微調(diào)理逊,把最耗時的下載圖片部分簡單粗暴地使用asyncio.gather并發(fā)執(zhí)行了一下橡伞,速度就從龜爬變成了像坐火箭一樣快!(其實代碼中還有很多可以優(yōu)化的點晋被,這里就不一一拿出來講了)


最后給大家提個醒:

雖然并發(fā)請求非常牛逼兑徘,可以讓你的爬蟲變得飛快,但它也不是不存在任何問題的墨微!

如果你的并發(fā)請求數(shù)量過大(又稱并發(fā)數(shù)過高)道媚,你的爬蟲就相當(dāng)于是在對他人的服務(wù)器進(jìn)行Dos攻擊(拒絕服務(wù)攻擊)了!

舉個例子翘县,你在爬一個小網(wǎng)站的時候為了自己爬的速度更快最域,對并發(fā)請求的數(shù)量毫無限制,使得你的爬蟲一次性發(fā)出了幾百锈麸、上千個請求镀脂,但一般的小網(wǎng)站根本扛不住這么高的并發(fā)!幾乎會在一瞬間就被你的爬蟲給打爆掉忘伞!試想一下薄翅,如果你是站長,看到這樣的情形你會怎么想氓奈?

如果你不能理解這個例子所產(chǎn)生的效果是什么樣的翘魄,可以自己搭建一個Web服務(wù),只放一個簡單的頁面舀奶,然后開個幾百并發(fā)去請求這個頁面暑竟,這樣你就能切身地體會到別人是什么感受了。

所以記住育勺,一定要合理控制并發(fā)請求的數(shù)量但荤,不要對對方網(wǎng)站造成過大的壓力!你給別人留活路涧至,別人才會給你留活路腹躁!

最后再留個小作業(yè)吧,如何對這個修改后的代碼增加一道并發(fā)數(shù)的限制南蓬?在留言區(qū)給出你的答案纺非。(提示:可通過搜索引擎查找【aiohttp并發(fā)連接數(shù)限制】和【python 列表切割】相關(guān)的內(nèi)容)


這個時代各種東西變化太快哑了,而網(wǎng)絡(luò)上的垃圾信息又很多,你需要有一個良好的知識獲取渠道铐炫,很多時候早就是一種優(yōu)勢垒手,還不趕緊關(guān)注我的公眾號并置頂/星標(biāo)一波~

發(fā)送消息“爬蟲速度提升之并發(fā)請求”到我的公眾號【小周碼字】即可獲得本文代碼下載地址~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市倒信,隨后出現(xiàn)的幾起案子科贬,更是在濱河造成了極大的恐慌,老刑警劉巖鳖悠,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榜掌,死亡現(xiàn)場離奇詭異,居然都是意外死亡乘综,警方通過查閱死者的電腦和手機(jī)憎账,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卡辰,“玉大人胞皱,你說我怎么就攤上這事【怕瑁” “怎么了反砌?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長萌朱。 經(jīng)常有香客問我宴树,道長,這世上最難降的妖魔是什么晶疼? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任酒贬,我火速辦了婚禮,結(jié)果婚禮上翠霍,老公的妹妹穿的比我還像新娘锭吨。我一直安慰自己,他們只是感情好寒匙,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布耐齐。 她就那樣靜靜地躺著,像睡著了一般蒋情。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耸携,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天棵癣,我揣著相機(jī)與錄音,去河邊找鬼夺衍。 笑死狈谊,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播河劝,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼壁榕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了赎瞎?” 一聲冷哼從身側(cè)響起牌里,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎务甥,沒想到半個月后牡辽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡敞临,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年态辛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挺尿。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡奏黑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出编矾,到底是詐尸還是另有隱情熟史,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布洽沟,位于F島的核電站以故,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏裆操。R本人自食惡果不足惜怒详,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望踪区。 院中可真熱鬧昆烁,春花似錦、人聲如沸缎岗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽传泊。三九已至鼠渺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間眷细,已是汗流浹背拦盹。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留溪椎,地道東北人普舆。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓恬口,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沼侣。 傳聞我的和親對象是個殘疾皇子祖能,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容