1. 程式人生 > >Python爬蟲-速度(2)

Python爬蟲-速度(2)

Python爬蟲-速度(2)

文章目錄


018.9.17

Python爬蟲-速度(1)
Python爬蟲-速度(3)

前言

我原有個習慣,就是每寫個什麼東西,都會在開頭記下日期。今天得空複查這篇內容的時候,發現居然赫然顯示著:018.9.17

十天就這麼過去了。

很難說我這10天裡到底做了什麼,收穫了什麼。因為我確實未發現自己在這時間裡有如何的長進。倒是再一次加深了對時間飛速的體會。

繼而說說這次的內容吧。由於程序執行緒協程涉及到的知識點很多,如果細講起來,夠寫好幾篇了。所以只好提供自己學習時用到的資料,包括書籍《Python Cook》,以及一些部落格文章。為了方便你我他,是全部附上了連結的。

併發與並行

我們先來說說什麼是併發,什麼又是並行。

併發:指程式有處理多個任務的能力
並行:指程式有同時處理多個任務的能力

二者之間的差別,就在同時二字。可以舉個例子:併發就是一群人從一個獨木橋上過,而並行則是一群人從多個獨木橋上過。
在這裡插入圖片描述
在這裡插入圖片描述

其實網上我看到許多人愛用“一個人吃十個饅頭與十個人吃十個饅頭”的例子,倒是很好的解釋了併發與並行的區別,然而乍一看,似乎併發沒啥用呀!所以慶幸自己想出一個過獨木橋的例子【不要臉.jpg】,這樣一來不但給了二者區別,還可以體現出併發的提速作用:一群人過獨木橋(所有人人可以都在橋上)肯定是比這群人挨個挨個的過獨木橋(橋上最多允許一個人存在)要快許多。

非同步與同步/阻塞和非阻塞

可以用一個在知乎看到的段子來理解:

老張愛喝茶,廢話不說,煮開水。
出場人物:老張。
道具:水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。

(1) 老張把水壺放到火上,立等水開。(同步阻塞)
【老張覺得自己有點傻】
(2) 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞)
【老張還是覺得自己有點傻,於是變高端了,買了把會響笛的那種水壺。水開之後,能大聲發出嘀~~~~的噪音。】

(3) 老張把響水壺放到火上,立等水開。(非同步阻塞)
【老張覺得這樣傻等意義不大。】
(4) 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。(非同步非阻塞)
【老張覺得自己聰明瞭。】

所謂同步非同步,只是對於水壺而言。
普通水壺,同步;響水壺,非同步。雖然都能幹活,但響水壺可以在自己完工之後,提示老張水開了。這是普通水壺所不能及的。同步只能讓呼叫者去輪詢自己(情況2中),造成老張效率的低下。

所謂阻塞非阻塞,僅僅對於老張而言。
立等的老張,阻塞;看電視的老張,非阻塞。情況(1)和情況(3)中老張就是阻塞的,媳婦喊他都不知道。雖然(3)中響水壺是非同步的,可對於立等的老張沒有太大的意義。所以一般非同步是配合非阻塞使用的,這樣才能發揮非同步的效用。

——來源網路,作者不明。

作者:愚抄
連結:https://www.zhihu.com/question/19732473/answer/23434554
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

多程序

由於主要是為爬蟲程式提速,所以過多的原理暫不講的。但需要記得的是:多程序是並行的

我們可以通過例項來觀察,首先來一個單程序單執行緒的爬蟲程式:

import requests
import time

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()
    results = map(get_one_html, urls)
    for result in results:
        print(result)
    end = time.time() - start
    print(end)

多次試驗,耗時在2.8~3.6s範圍。如果我們改寫為多程序呢?

可參見《Python Cook》中簡單的並行程式設計,連結:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p08_perform_simple_parallel_programming.html

import requests
import time
from concurrent.futures import ProcessPoolExecutor

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()

	# 利用程序池
    with ProcessPoolExecutor() as pool:
	    # 往程序池新增任務
        pool.map(get_one_html, urls)

    end = time.time() - start
    print(end)

多次試驗,耗時在1.2~1.7s範圍。

可以看到多程序的確可以實現程式提速,但需要的注意的是,只有在需要多次使用get_one_html()函式時,多程序的優勢才能體現;如果僅僅用兩三次,反而是單程序單執行緒執行速度快。這是因為系統在為實現多程序前需要一些準備工作,這個準備將耗費大量時間。

多執行緒

多執行緒是併發的。儘管Python中有GIL鎖,但針對I/O操作時,仍有提速效果。

可參見《Python Cook》中建立一個執行緒池,連結:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p07_creating_thread_pool.html

import requests
import time
from concurrent.futures import ThreadPoolExecutor

def get_one_html(url):
    response = requests.get(url=url, headers=HEADERS)
    return response.url

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]

    start = time.time()

	# 執行緒池
    with ThreadPoolExecutor() as pool:
        pool.map(get_one_html, urls)

    end = time.time() - start
    print(end)

多次試驗,耗時在0.86~1.0s範圍。

可以看到提速比多程序會好一些。

協程+非同步

關於這部分的知識,講起來都可以單獨撐開一個篇幅了,所以這裡我就“拿來主義”:
在這裡插入圖片描述
崔大神的一篇關於非同步提速,連結:https://mp.weixin.qq.com/s/jCc1jIHxU_p6bUlwijkwyQ
以及CSDN上某友的文章,連結:https://blog.csdn.net/sinat_26917383/article/details/79246632

import aiohttp
import time
import asyncio


async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=HEADERS) as response:
            assert response.status == 200

    return response.url

async def get_one_html(url):
    result = await get(url) # 由於asyncio不支援http請求,所以需要用aiohttp來封裝一個get
    return result

if __name__  == "__main__":
    HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) "
                             "AppleWebKit/537.36 (KHTML, like Gecko) "
                             "Chrome/65.0.3325.183 Safari/537.36 "
                             "Vivaldi/1.96.1147.64"}

    urls = [
        "http://www.baidu.com", "http://www.sina.com.cn",
        "http://www.163.com", "http://www.sohu.com",
        "http://www.csdn.net", "http://www.jobbole.com",
        "http://www.qq.com", "http://weixin.qq.com",
        "http://www.jb51.net", "https://m.qidian.com"
    ]
    # 構建tasks列表
    tasks = [asyncio.ensure_future(get_one_html(url)) for url in urls]

    start = time.time()

    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    end = time.time() - start
    print(end)

    for task in tasks:
        print(task.result())

多次試驗,耗時在1.4~1.8s範圍。儘管速度上不及多程序與多執行緒,但我們知道它節省了配置,如果請求的任務量繼續加大,做相應調整是可以超越前面兩種方法的。