從抓取豆瓣電影聊高性能爬蟲思路

簡(jiǎn)書不維護(hù)了,歡迎關(guān)注我的知乎:波羅學(xué)的個(gè)人主頁

知乎原文

本篇文章將以抓取豆瓣電影信息為例來一步步介紹開發(fā)一個(gè)高性能爬蟲的常見思路。

尋找數(shù)據(jù)地址

爬蟲的第一步,首先我們要找到獲取數(shù)據(jù)的地址??梢韵鹊蕉拱觌娪笆醉?https://movie.douban.com/ 去看看。

頂部導(dǎo)航為提供了很多種類型的入口,其中和電影有關(guān)的有:排行榜、選電影和分類。為了便于后續(xù)更精細(xì)的分析,這里選擇進(jìn)入分類頁面,它的地址為https://movie.douban.com/tag/。通過瀏覽的開發(fā)工具,我們最終能確認(rèn)數(shù)據(jù)來源是的 https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0 接口。

注意:如果有朋友熟悉前端并裝有vue瀏覽器插件,就會(huì)發(fā)現(xiàn)豆瓣電影站點(diǎn)是vue開發(fā)的。這些基本web開發(fā)技能對(duì)于我們平時(shí)開發(fā)爬蟲都是很有幫助的。

爬取首頁數(shù)據(jù)

用瀏覽器打開上面的接口地址,我們就會(huì)發(fā)現(xiàn)它的返回?cái)?shù)據(jù)為json格式。利用python的requests和json庫,就可以把數(shù)據(jù)獲取下來了。

這里我們只獲取電影的標(biāo)題、導(dǎo)演、評(píng)分和演員四個(gè)字段,代碼如下:

import json
import requests

def crawl(url):
    response = requests.get(url)
    if response.status_code != 200:
        raise Exception('http status code is {}'.format(response.status_code))

    data = response.json()['data']

    items = []
    for v in  data:
        items.append({
            'title': v['title'],
            'drectors': v['directors'],
            'rate': v['rate'],
            'casts': v['casts']
        })

    return items

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0'
    for item in crawl(url):
        print(item)

if __name__ == "__main__":
    main()

代碼執(zhí)行得到如下這些數(shù)據(jù):

{'title': '綠皮書', 'drectors': ['彼得·法雷里'], 'rate': '8.9', 'casts': ['維果·莫騰森', '馬赫沙拉·阿里', '琳達(dá)·卡德里尼', '塞巴斯蒂安·馬尼斯科', '迪米特·D·馬里諾夫']}
{'title': '驚奇隊(duì)長(zhǎng)', 'drectors': ['安娜·波頓', '瑞安·弗雷克'], 'rate': '7.0', 'casts': ['布麗·拉爾森', '裘德·洛', '塞繆爾·杰克遜', '本·門德爾森', '安妮特·貝寧']}
 ...
{'title': '這個(gè)殺手不太冷', 'drectors': ['呂克·貝松'], 'rate': '9.4', 'casts': ['讓·雷諾', '娜塔莉·波特曼', '加里·奧德曼', '丹尼·愛羅', '彼得·阿佩爾']}
{'title': '新喜劇之王', 'drectors': ['周星馳', '邱禮濤', '黃驍鵬', '肖鶴'], 'rate': '5.8', 'casts': ['王寶強(qiáng)', '鄂靖文', '張全蛋', '景如洋', '張琪']}

仔細(xì)觀察,我們會(huì)發(fā)現(xiàn)僅僅抓到了20條數(shù)據(jù)。電影數(shù)據(jù)才這么點(diǎn),這是不可能的,這是因?yàn)檎>W(wǎng)站展示信息都會(huì)采用分頁方式。再來看下電影的分類頁面,我們把滾動(dòng)條拉到底部就會(huì)發(fā)現(xiàn)底部有個(gè) "加載更多" 的提示按鈕。點(diǎn)擊之后,會(huì)加載出更多的電影。

分頁抓取

對(duì)于各位來說,分頁應(yīng)該是很好理解的。就像書本一樣,包含信息多了自然就需要分頁,網(wǎng)站也是如此。不過站點(diǎn)根據(jù)場(chǎng)景不同,分頁規(guī)則也會(huì)有些不同。下面來具體說說:

先說說分頁的參數(shù),通常會(huì)涉及三個(gè)參數(shù),分別是:

  • 具體頁碼,url中的常見名稱有 page、p、n 等,起始頁碼通常為1,有些情況為0;
  • 每頁數(shù)量,url中的常見名稱有 limit、size、pagesize(page_size pageSize)等;
  • 起始位置,url中的常見名稱有start、offset等,主要說明從什么位置開始獲取數(shù)據(jù),;

分頁主要通過這三種參數(shù)的兩種組合實(shí)現(xiàn),哪兩種組合?繼續(xù)往下看:

  • 具體頁碼 + 每頁數(shù)量,這種規(guī)則主要用在分頁器的情況下,而且返回?cái)?shù)據(jù)需包含總條數(shù);
  • 起始位置 + 每頁數(shù)量,這種規(guī)則主要用在下拉場(chǎng)景,豆瓣的例子就是用下拉來分頁,這種情況下的url返回?cái)?shù)據(jù)可不包含總數(shù),前端以下頁是否還有數(shù)據(jù)就可判定分頁是否完成。

介紹完了常見的兩種分頁規(guī)則,來看看我們的的url:

https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0

該頁面通過下拉方式實(shí)現(xiàn)翻頁,那么我們就會(huì)想url中是否有起始位置信息。果然在找到了start參數(shù),此處為0。然后點(diǎn)擊下拉,通過瀏覽器開發(fā)工具監(jiān)控得到了新的url,如下:

https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=20

start的值變成了20,這說明起始位置參數(shù)就是start。依照分頁的規(guī)則,我們把main函數(shù)修改下,加個(gè)while循環(huán)就可以獲取全部電影數(shù)據(jù)了,代碼如下:

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}'

    start = 0
    total = 0
    while True:
        items = crawl(url.format(start))
        if len(items) <= 0:
            break

        for item in items:
            print(item)

        start += 20
        total += len(items)
        print('已抓取了{(lán)}條電影信息'.format(total))

    print('共抓取了{(lán)}條電影信息'.format(total))

到這里工作基本完成!把print改為入庫操作把抓取的數(shù)據(jù)入庫,一個(gè)爬蟲就真正完成了。

進(jìn)一步優(yōu)化

不知大家注意到?jīng)]有,這里的請(qǐng)求每次只能獲取20條數(shù)據(jù),這必然到導(dǎo)致數(shù)據(jù)請(qǐng)求次數(shù)增加。這有什么問題嗎?三個(gè)問題:

  • 網(wǎng)絡(luò)資源浪費(fèi)嚴(yán)重;
  • 獲取數(shù)據(jù)速度太慢;
  • 容易觸發(fā)發(fā)爬機(jī)制;

那有沒有辦法使請(qǐng)求返回?cái)?shù)據(jù)量增加?當(dāng)然是有的。

前面說過分頁規(guī)則有兩個(gè),分別是 具體頁碼 + 每頁大小 和 起始位置 + 每頁大小。這兩種規(guī)則都和每頁大小,即每頁數(shù)量有關(guān)。我們知道上面的接口默認(rèn)每頁大小為20。根據(jù)前面介紹的分頁規(guī)則,我們分別嘗試在url加上limit和size參數(shù)。驗(yàn)證后發(fā)現(xiàn),limit可用來改變每次請(qǐng)求獲取數(shù)量。修改一下代碼,在url上增加參數(shù)limit,使其等于100:

def main():
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}&limit=100'

只是增加了一個(gè)limit參數(shù)就可以幫助我們大大減少接口請(qǐng)求次數(shù),提高數(shù)據(jù)獲取速度。要說明一下,不是每次我們都有這樣好的運(yùn)氣,有時(shí)候每頁數(shù)量是固定的,我們沒有辦法修改,這點(diǎn)我們需要知道。

高性能爬蟲

經(jīng)過上面的優(yōu)化,我們的爬蟲性能已經(jīng)有了一定提升,但是好像還是很慢。執(zhí)行它并觀察打印信息,我們會(huì)發(fā)現(xiàn)每個(gè)請(qǐng)求之間的延遲很大,必須等待上一個(gè)請(qǐng)求響應(yīng)并處理完成,才能繼續(xù)發(fā)出下一個(gè)請(qǐng)求。如果大家有網(wǎng)絡(luò)監(jiān)控工具,你會(huì)發(fā)現(xiàn)此時(shí)網(wǎng)絡(luò)帶寬的利用率很低。因?yàn)榇蟛糠值臅r(shí)間都被IO請(qǐng)求阻塞了。有什么辦法可以解決這個(gè)問題?那么必然要提的就是并發(fā)編程。

并發(fā)編程是個(gè)很大的話題,涉及多線程、多進(jìn)程以及異步io等,這篇文章的重點(diǎn)不在此。這里使用python的asyncio來幫助我們提升高爬蟲性能。我們來看實(shí)現(xiàn)代碼吧。

此處要說明一個(gè)問題,因?yàn)槎拱暧孟吕姆绞将@取數(shù)據(jù),正如上面介紹的那樣,這是一種不需要提供數(shù)據(jù)總數(shù)的就可以分頁的方式。但是這種方式會(huì)導(dǎo)致我就沒有辦法事先根據(jù)limit和total確定請(qǐng)求的總數(shù),在請(qǐng)求總數(shù)未知的情況下,我們的請(qǐng)求只能順序執(zhí)行。所以這里我們?yōu)榱税咐軌蚶^續(xù),假設(shè)獲取數(shù)據(jù)最多1萬條,代碼如下:

import json
import asyncio
import aiohttp

async def crawl(url):
    data = None
    async with aiohttp.ClientSession() as s:
        async with s.get(url) as r:
            if r.status != 200:
                raise Exception('http status code is {}'.format(r.status))
            data = json.loads(await r.text())['data']

    items = []
    for v in  data:
        items.append({
            'title': v['title'],
            'drectors': v['directors'],
            'rate': v['rate'],
            'casts': v['casts']
        })

    return items

async def main():
    limit = 100
    url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start={}&limit={}'
    start = 0
    total = 10000

    crawl_total = 0
    tasks = [crawl(url.format(start + i * limit, limit)) for i in range(total // limit)]
    for r in asyncio.as_completed(tasks):
        items = await r
        for item in items:
            print(item)
        crawl_total += len(items)

    print('共抓取了{(lán)}條電影信息'.format(crawl_total))

if __name__ == "__main__":
    ioloop = asyncio.get_event_loop()
    ioloop.run_until_complete(main())

最終結(jié)果顯示獲取了9900條,感覺是豆瓣限制了翻頁的數(shù)量,最多只能獲取9900條數(shù)據(jù)。

最終的代碼使用了asyncio的異步并發(fā)編程來實(shí)現(xiàn)爬蟲性能的提高,而且還用到了aiohttp這個(gè)庫來實(shí)現(xiàn)http的異步請(qǐng)求。跳躍有點(diǎn)大,有一種學(xué)會(huì)了1+1就可以去做微積分題目的感覺。淡然,這里也使用多進(jìn)程或多線程來重寫。

總結(jié)

本文從提高爬蟲抓取速度與減少資源消耗兩個(gè)角度介紹了開發(fā)一個(gè)高性能爬蟲的一些技巧:

  • 有效利用分頁減少網(wǎng)絡(luò)請(qǐng)求減少資源消耗;
  • 并發(fā)編程實(shí)現(xiàn)帶寬高效利用提高爬蟲速度;

最后,大家如果有興趣可以去看看tornado文檔中實(shí)現(xiàn)的一個(gè)高并發(fā)爬蟲。如果不懂異步io的話,或許會(huì)覺得這個(gè)代碼很詭異。
tornado高性能爬蟲

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • width: 65%;border: 1px solid #ddd;outline: 1300px solid #...
    邵勝奧閱讀 5,148評(píng)論 0 1
  • 前言 爬蟲就是請(qǐng)求網(wǎng)站并提取數(shù)據(jù)的自動(dòng)化程序,其中請(qǐng)求,提取,自動(dòng)化是爬蟲的關(guān)鍵。Python作為一款出色的膠水語...
    王奧OX閱讀 3,636評(píng)論 1 8
  • 上網(wǎng)原理 1、爬蟲概念 爬蟲是什麼? 蜘蛛,蛆,代碼中,就是寫了一段代碼,代碼的功能從互聯(lián)網(wǎng)中提取數(shù)據(jù) 互聯(lián)網(wǎng): ...
    riverstation閱讀 8,621評(píng)論 1 2
  • 基礎(chǔ)知識(shí) HTTP協(xié)議 我們?yōu)g覽網(wǎng)頁的瀏覽器和手機(jī)應(yīng)用客戶端與服務(wù)器通信幾乎都是基于HTTP協(xié)議,而爬蟲可以看作是...
    腩啵兔子閱讀 1,664評(píng)論 0 17
  • 如上圖所示,屏幕正中間有個(gè)元素A,隨著屏幕寬度的增加,始終需要滿足以下條件: A元素垂直居中于屏幕中央; A元素距...
    獨(dú)愛一樂拉面閱讀 649評(píng)論 0 1

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