Python爬蟲:單線程、多線程和協程的爬蟲性能對比

2021-02-14 Python綠色通道

專欄作者:小小明

非常擅長解決各類複雜數據處理的邏輯,各類結構化與非結構化數據互轉,字符串解析匹配等等。

至今已經幫助很多數據從業者解決工作中的實際問題,如果你在數據處理上遇到什麼困難,歡迎評論區與我交流。

大家好,我是小小明。

今天我要給大家分享的是如何爬取豆瓣上深圳近期即將上映的電影影訊,並分別用普通的單線程、多線程和協程來爬取,從而對比單線程、多線程和協程在網絡爬蟲中的性能。

具體要爬的網址是:https://movie.douban.com/cinema/later/shenzhen/

除了要爬入口頁以外還需爬取每個電影的詳情頁,具體要爬取的結構信息如下:

爬取測試

下面我演示使用xpath解析數據。

入口頁數據讀取:

import requests
from lxml import etree
import pandas as pd
import re

main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
r

結果:

<Response [200]>

檢查一下所需數據的xpath:

可以看到每個電影信息都位於id為showing-soon下面的div裡面,再分別分析內部的電影名稱、url和想看人數所處的位置,於是可以寫出如下代碼:

html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
    #  imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    # date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])
main_df

結果:

然後再選擇一個詳情頁的url進行測試,我選擇了熊出沒·狂野大陸這部電影,因為文本數據相對最複雜,也最具備代表性:

url = main_df.at[17, "url"]
url

結果:

'https://movie.douban.com/subject/34825886/'

分析詳情頁結構:

文本信息都在這個位置中,下面我們直接提取這個div下面的所有文本節點:

r = requests.get(url, headers=headers)
html = etree.HTML(r.text)
movie_infos = html.xpath("//div[@id='info']//text()")
print(movie_infos)

結果:

['\n        ', '導演', ': ', '丁亮', '\n        ', '編劇', ': ', '徐芸', ' / ', '崔鐵志', ' / ', '張宇', '\n        ', '主演', ': ', '張偉', ' / ', '張秉君', ' / ', '譚笑', '\n        ', '類型:', ' ', '喜劇', ' / ', '科幻', ' / ', '動畫', '\n        \n        ', '製片國家/地區:', ' 中國大陸', '\n        ', '語言:', ' 漢語普通話', '\n        ', '上映日期:', ' ', '2021-02-12(中國大陸)', ' / ', '2020-08-01(上海電影節)', '\n        ', '片長:', ' ', '100分鐘', '\n        ', '又名:', ' 熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life', '\n        ', 'IMDb連結:', ' ', 'tt11654032', '\n\n']

為了閱讀方便,拼接一下:

movie_info_txt = "".join(movie_infos)
print(movie_info_txt)

結果:

        導演: 丁亮
        編劇: 徐芸 / 崔鐵志 / 張宇
        主演: 張偉 / 張秉君 / 譚笑
        類型: 喜劇 / 科幻 / 動畫
        
        製片國家/地區: 中國大陸
        語言: 漢語普通話
        上映日期: 2021-02-12(中國大陸) / 2020-08-01(上海電影節)
        片長: 100分鐘
        又名: 熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life
        IMDb連結: tt11654032

接下來就簡單了:

row = {}
for line in re.split("[\n ]*\n[\n ]*", movie_info_txt):
    line = line.strip()
    arr = line.split(": ", maxsplit=1)
    if len(arr) != 2:
        continue
    k, v = arr
    row[k] = v
row

結果:

{'導演': '丁亮',
 '編劇': '徐芸 / 崔鐵志 / 張宇',
 '主演': '張偉 / 張秉君 / 譚笑',
 '類型': '喜劇 / 科幻 / 動畫',
 '製片國家/地區': '中國大陸',
 '語言': '漢語普通話',
 '上映日期': '2021-02-12(中國大陸) / 2020-08-01(上海電影節)',
 '片長': '100分鐘',
 '又名': '熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life',
 'IMDb連結': 'tt11654032'}

可以看到成功的切割出了每一項。

下面根據上面的測試基礎,我們完善整體的爬蟲代碼:

單線程爬蟲
import requests
from lxml import etree
import pandas as pd
import re

main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
    imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    print(url)
#     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    r = requests.get(url, headers=headers)
    html = etree.HTML(r.text)
    row = {}
    row["電影名稱"] = name
    for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
        line = line.strip()
        arr = line.split(": ", maxsplit=1)
        if len(arr) != 2:
            continue
        k, v = arr
        row[k] = v
    row["想看人數"] = int(like_num[:like_num.find("人")])
#     row["url"] = url
#     row["圖片地址"] = imgurl
#     print(row)
    result.append(row)
df = pd.DataFrame(result)
df.sort_values("想看人數", ascending=False, inplace=True)
df.to_csv("shenzhen_movie.csv", index=False)

結果:

https://movie.douban.com/subject/26752564/
https://movie.douban.com/subject/35172699/
https://movie.douban.com/subject/34992142/
https://movie.douban.com/subject/30349667/
https://movie.douban.com/subject/30283209/
https://movie.douban.com/subject/33457717/
https://movie.douban.com/subject/30487738/
https://movie.douban.com/subject/35068230/
https://movie.douban.com/subject/27039358/
https://movie.douban.com/subject/30205667/
https://movie.douban.com/subject/30476403/
https://movie.douban.com/subject/30154423/
https://movie.douban.com/subject/27619748/
https://movie.douban.com/subject/26826330/
https://movie.douban.com/subject/26935283/
https://movie.douban.com/subject/34841067/
https://movie.douban.com/subject/34880302/
https://movie.douban.com/subject/34825886/
https://movie.douban.com/subject/34779692/
https://movie.douban.com/subject/35154209/

爬到的文件:

整體耗時:

42.5秒。

多線程爬蟲

單線程的爬取耗時還是挺長的,下面看看使用多線程的爬取效率:

import requests
from lxml import etree
import pandas as pd
import re
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED


def fetch_content(url):
    print(url)
    headers = {
        "Accept-Encoding": "Gzip",  # 使用gzip壓縮傳輸數據讓訪問更快
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
    }
    r = requests.get(url, headers=headers)
    return r.text


url = "https://movie.douban.com/cinema/later/shenzhen/"
init_page = fetch_content(url)
html = etree.HTML(init_page)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
#     imgurl, = e.xpath(".//img/@src")
    name, = e.xpath(".//div[@class='intro']/h3/a/text()")
    url, = e.xpath(".//div[@class='intro']/h3/a/@href")
#     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
    like_num, = e.xpath(
        ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
    result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])

max_workers = main_df.shape[0]
with ThreadPoolExecutor(max_workers=max_workers) as executor:
    future_tasks = [executor.submit(fetch_content, url) for url in main_df.url]
    wait(future_tasks, return_when=ALL_COMPLETED)
    pages = [future.result() for future in future_tasks]

result = []
for url, html_text in zip(main_df.url, pages):
    html = etree.HTML(html_text)
    row = {}
    for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
        line = line.strip()
        arr = line.split(": ", maxsplit=1)
        if len(arr) != 2:
            continue
        k, v = arr
        row[k] = v
    row["url"] = url
    result.append(row)
detail_df = pd.DataFrame(result)
df = main_df.merge(detail_df, on="url")
df.drop(columns=["url"], inplace=True)
df.sort_values("想看人數", ascending=False, inplace=True)
df.to_csv("shenzhen_movie2.csv", index=False)
df

結果:

耗時8秒。

由於每個子頁面都是單獨的線程爬取,每個線程幾乎都是同時在工作,所以最終耗時僅取決於爬取最慢的子頁面。

協程異步爬蟲

由於我在jupyter中運行,為了使協程能夠直接在jupyter中直接運行,所以我在代碼中增加了下面兩行代碼,在普通編輯器裡面可以去掉:

import nest_asyncio
nest_asyncio.apply()

這個問題是因為jupyter所依賴的高版本Tornado存在bug,將Tornado退回到低版本也可以解決這個問題。

下面我使用協程來完成這個需求的爬取:

import aiohttp
from lxml import etree
import pandas as pd
import re
import asyncio
import nest_asyncio
nest_asyncio.apply()


async def fetch_content(url):
    print(url)
    header = {
        "Accept-Encoding": "Gzip",  # 使用gzip壓縮傳輸數據讓訪問更快
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
    }
    async with aiohttp.ClientSession(
        headers=header, connector=aiohttp.TCPConnector(ssl=False)
    ) as session:
        async with session.get(url) as response:
            return await response.text()


async def main():
    url = "https://movie.douban.com/cinema/later/shenzhen/"
    init_page = await fetch_content(url)
    html = etree.HTML(init_page)
    all_movies = html.xpath("//div[@id='showing-soon']/div")
    result = []
    for e in all_movies:
        #         imgurl, = e.xpath(".//img/@src")
        name, = e.xpath(".//div[@class='intro']/h3/a/text()")
        url, = e.xpath(".//div[@class='intro']/h3/a/@href")
    #     date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
        like_num, = e.xpath(
            ".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
        result.append((name, int(like_num[:like_num.find("人")]), url))
    main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])

    tasks = [fetch_content(url) for url in main_df.url]
    pages = await asyncio.gather(*tasks)

    result = []
    for url, html_text in zip(main_df.url, pages):
        html = etree.HTML(html_text)
        row = {}
        for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
            line = line.strip()
            arr = line.split(": ", maxsplit=1)
            if len(arr) != 2:
                continue
            k, v = arr
            row[k] = v
        row["url"] = url
        result.append(row)
    detail_df = pd.DataFrame(result)
    df = main_df.merge(detail_df, on="url")
    df.drop(columns=["url"], inplace=True)
    df.sort_values("想看人數", ascending=False, inplace=True)
    return df

df = asyncio.run(main())
df.to_csv("shenzhen_movie3.csv", index=False)
df

結果:

耗時僅7秒,相對比多線程更快一點。

由於request庫不支持協程,所以我使用了支持協程的aiohttp進行頁面抓取。當然實際爬取的耗時還取絕於當時的網絡,但整體來說,協程爬取會比多線程爬蟲稍微快一些。

回顧

今天我向你演示了,單線程爬蟲、多線程爬蟲和協程爬蟲。可以看到一般情況下協程爬蟲速度最快,多線程爬蟲略慢一點,單線程爬蟲則必須上一個頁面爬取完成才能繼續爬取。

但協程爬蟲相對來說並不是那麼好編寫,數據抓取無法使用request庫,只能使用aiohttp。所以在實際編寫爬蟲時,我們一般都會使用多線程爬蟲來提速,但必須注意的是網站都有ip訪問頻率限制,爬的過快可能會被封ip,所以一般我們在多線程提速的同時使用代理ip來並發的爬取數據。

彩蛋:xpath+pandas解析表格並提取url

我們在深圳影訊的底部能夠看到一個[查看全部即將上映的影片] (https://movie.douban.com/coming)的按鈕,點進去能夠看到一張完整近期上映電影的列表,發現這個列表是個table標籤的數據:

那就簡單了,解析table我們可能壓根就不需要用xpath,直接用pandas即可,但片名中包含的url地址還需解析,所以我採用xpath+pandas來解析這個網頁,看看我的代碼吧:

import pandas as pd
import requests
from lxml import etree

headers = {
    "Accept-Encoding": "Gzip",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get("https://movie.douban.com/coming", headers=headers)
html = etree.HTML(r.text)
table_tag = html.xpath("//table")[0]
df, = pd.read_html(etree.tostring(table_tag))
urls = table_tag.xpath(".//td[2]/a/@href")
df["url"] = urls
df

結果

這樣就能到了主頁面的完整數據,再簡單的處理一下即可。

結語

感謝各位讀者,有什麼想法和收穫歡迎留言評論噢!

相關焦點

  • 看幾段爬蟲代碼,詳解Python多線程、多進程、協程
    很多時候我們寫了一個爬蟲,實現了需求後會發現了很多值得改進的地方,其中很重要的一點就是爬取速度。本文就通過代碼講解如何使用多進程、多線程、協程來提升爬取速度。注意:我們不深入介紹理論和原理,一切都在代碼中。首先我們寫一個簡化的爬蟲,對各個功能細分,有意識進行函數式編程。
  • Python爬蟲從入門到精通(3): BeautifulSoup用法總結及多線程爬蟲爬取糗事百科
    我們還會利用requests庫和BeauitfulSoup來爬取糗事百科上的段子, 並對比下單線程爬蟲和多線程爬蟲的爬取效率。什麼是BeautifulSoup及如何安裝BeautifulSoup是一個解析HTML或XML文件的第三方庫。
  • Python協程:概念及其用法
    ——蒙田《蒙田隨筆全集》上篇《Python 多線程雞年不雞肋》論述了關於python多線程是否是雞肋的問題,得到了一些網友的認可,當然也有一些不同意見,表示協程比多線程不知強多少,在協程面前多線程算是雞肋。好吧,對此我也表示贊同,然而上篇我論述的觀點不在於多線程與協程的比較,而是在於IO密集型程序中,多線程尚有用武之地。
  • Web爬蟲:多線程、異步與動態代理初步
    進入正題,使用Python3簡單實現一個單機版多線程/異步+多代理的爬蟲,沒有分布式、不談高效率,先跑起來再說,腦補開始。。。1.4 多線程多線程是實現任務並發的方式之一。在Python中實現多線程的方案比較多,最常見的是隊列和線程類que = queue.Queue()def worker():    while not que.empty():        do(que.get())threads = []nloops = 256for i in range(nloops):    t = threading.Thread(target=worker
  • Python 進程、線程和協程實戰指北
    前言前些日子寫過幾篇關於線程和進程的文章,概要介紹了Python內置的線程模塊(threading)和進程模塊(multiprocessing)的使用方法,側重點是線程間同步和進程間同步。隨後,陸續收到了不少讀者的私信,諮詢進程、線程和協程的使用方法,進程、線程和協程分別適用於何種應用場景,以及混合使用進程、線程和協程的技巧。
  • Python爬蟲:一些常用的爬蟲技巧總結
    也差不多一年多了,python應用最多的場景還是web快速開發、爬蟲、自動化運維:寫過簡單網站、寫過自動發帖腳本、寫過收發郵件腳本、寫過簡單驗證碼識別腳本。,於是對爬蟲一律拒絕請求。 compresseddata = f.read() compressedstream = StringIO.StringIO(compresseddata)gzipper = gzip.GzipFile(fileobj=compressedstream) print gzipper.read()8、多線程並發抓取
  • Python 爬蟲:8 個常用的爬蟲技巧總結!
    用python也差不多一年多了,python應用最多的場景還是web快速開發、爬蟲、自動化運維:寫過簡單網站、寫過自動發帖腳本、寫過收發郵件腳本
  • python爬蟲16 | 你,快去試試用多進程的方式重新去爬取豆瓣上的電影
    我們在之前的文章談到了高效爬蟲在 python 中多線程下的
  • Python 爬蟲面試題 170 道
    常見的設計模式的使用深淺拷貝的區別線程、進程、協程的使用了解 Python 中的元編程和反射常考的數據結構和算法29. (1)怎樣將字符串轉換為小寫 (2)單引號、雙引號、三引號的區別?123.用 Python 實現一個二分查找的函數124.python 單例模式的實現方法125.使用 Python 實現一個斐波那契數列126.找出列表中的重複數字127.找出列表中的單個數字
  • Python視頻教程網課編程零基礎入門數據分析網絡爬蟲全套Python...
    ,然後再根據自 己的需求和規劃選擇學習其他方向課程,學完後一定要多實踐 總目錄 零基礎全能篇(4套課程) 實用編程技巧進價(1套課程) 數據分析與挖掘(8套課程) 辦公自動化(3套課程) 機器學習與人工智慧(7套課程) 開發實戰篇(4套課程) 量化投資(2套課程) 網絡爬蟲(
  • Python多線程實戰
    多線程可以共享全局變量,多進程不能。多線程中,所有子線程的進程號相同;多進程中,不同的子進程進程號不同。進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位.
  • Python 協程模塊 asyncio 使用指南
    在上次的《5 分鐘入門 Python 協程》的 Chat 中和大家簡單的普及了下
  • Python 爬蟲面試題 170 道:2019 版
    常見的設計模式的使用深淺拷貝的區別線程、進程、協程的使用了解 Python 中的元編程和反射常考的數據結構和算法109.列舉 5 個 Python 中的異常類型以及其含義110.copy 和 deepcopy 的區別是什麼?111.代碼中經常遇到的*args, **kwargs 含義及用法。112.Python 中會有函數或成員變量包含單下劃線前綴和結尾,和雙下劃線前綴結尾,區別是什麼?
  • 這樣學 Python 多線程與進程(一)
    眾所周知,Python 中的多線程是一個假的多線程
  • 簡單講解價值1K的Python爬蟲外包案例
    這裡就不多說明了。 往期推薦 本篇文章就使用三種爬蟲模式爬取相關數據 1、常規爬取數據 2、多線程爬取數據 3、scrapy框架爬取數據 基本開發環境
  • 如何爬取全網1200本Python書|爬蟲實戰篇
    上次代碼沒有寫完,正好周末有時間把代碼全部完成並且存入了資料庫中,今天就給大家一步步分析一下是我是如何爬取數據,清洗數據和繞過反爬蟲的一些策略和點滴記錄。1)2).我用的是多線程爬取,把所有的url都扔到一個隊列裡面,然後設置幾個線程去隊列裡面不斷的爬取,然後循環往復,直到隊列裡的url全部處理完畢3).數據存儲的時候,有兩種思路:1).一般大型的網站都有反爬蟲策略,雖然我們這次爬的數量只有1000本書,但是一樣會碰到反爬蟲問題
  • Python開發簡單爬蟲【學習資料總結】
    一、簡單爬蟲架構 四、網頁解析器和BeautifulSoup 網頁解析器從HTML網頁字符串中提取出價值數據和新URL對象。
  • Python爬蟲知識點梳理
    爬蟲涉及的技術包括但不限於熟練一門程式語言(這裡以 Python 為例) HTML 知識、HTTP 協議的基本知識、正則表達式、資料庫知識,常用抓包工具的使用、爬蟲框架的使用、涉及到大規模爬蟲,還需要了解分布式的概念、消息隊列、常用的數據結構和算法、緩存,甚至還包括機器學習的應用,大規模的系統背後都是靠很多技術來支撐的。
  • Unity3D協程——線程(Thread)和協程(Coroutine)
    很多小夥伴會分不清線程和協程的區別,這也是很多初級程序面試經常遇到的題目,在此特出一個系列,專門講解Unity協程。
  • Python爬蟲從入門到精通只需要三個月
    為什麼要學習python爬蟲?隨著了解爬行動物學習的人越來越多,就業需求也越來越需要這一塊的工作人員。在一方面,網際網路可以得到越來越多的數據。在另一方面,就像Python程式語言提供了越來越多的優秀的工具,允許爬蟲簡單,使用方便。我們使用爬蟲可以得到很多數據值。