實戰(zhàn):爬取簡書之多線程爬取(一)

在上上篇我們編寫了一個簡單的程序框架來爬取簡書的文章信息,10分鐘左右爬取了 1萬 5千條數(shù)據(jù)。

現(xiàn)在,讓我們先來做一個簡單的算術(shù)題:

假設(shè)簡書有活躍用戶一千萬人(不知道簡書有多少活躍用戶,我只能往小了算)

平均每人寫了 15篇文章,那么一共有一億五千萬篇文章

我們10分鐘爬取了 1萬 5千篇,湊個整算 2萬

那么爬取一億五千萬條數(shù)據(jù)需要

150000000 / 20000 = 10 * 7500 = 75000 min = 1250 h = 52 d

w(?Д?)w 52天!!!,如果按照前面的腳本來爬要爬整整 52天,那時候黃花菜都涼了呀。

這些數(shù)據(jù)的時間跨度如此大,如果要做數(shù)據(jù)分析的進(jìn)行對比的話就會產(chǎn)生較大的誤差。

所以,我們必須得提高爬取速度!!!

這時候就輪到今天得主角登場了,

噔 噔 噔 蹬------》多線程

一、多線程簡介

簡單來講,多線程就相當(dāng)于你原來開一個窗口爬取,現(xiàn)在開了10個窗口來爬取。

不計較數(shù)據(jù)的重復(fù)的話,現(xiàn)在的速度應(yīng)該是之前的10倍,也就是說原來要52天才能爬完的數(shù)據(jù)現(xiàn)在只要5.2天了。

不過多線程和上面的例子還是有一些區(qū)別的

多線程是在一個窗口里同時運(yùn)行十個線程,而上面的例子是同時打開十個窗口。

如果將數(shù)據(jù)比作貨物的話,原來一個線程就相當(dāng)于一個人在搬,十個線程就相當(dāng)于十個人在搬

二、多線程的簡單使用

threading是 python的標(biāo)準(zhǔn)庫,可以直接導(dǎo)入使用

Thread類是 threading庫的重點,我們使用要使用多線程都要通過這個類來使用

Thread一共有兩種使用方法,第一種是直接傳一個回調(diào)函數(shù)給 Thread類,這個回調(diào)函數(shù)可以有參數(shù),但必須返回 None也就是不能有返回值。

第二種方法是繼承 Thread類,然后重載 Thread類的 __init__()run()方法,第二種方法可以在實例初始化的時候向 run方法傳遞參數(shù)。

這兩種方式可以互換,但是推薦使用第二種方法,因為這樣更利于代碼的組織,代碼的可讀性也更強(qiáng)。

下面我們就用代碼來演示一下,Thread類的使用方法:
第一種:

Thread的原型是:

threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

這里 group必須是 None,將回調(diào)函數(shù)傳給 target,name是線程的名字默認(rèn)是 ‘Thread-N’ N是一個數(shù)字。

args是要傳遞給回調(diào)函數(shù)的位置參數(shù),kwargs是傳遞給回調(diào)函數(shù)的關(guān)鍵詞參數(shù)。

第一種使用方法如下:

先定義一個函數(shù),然后將函數(shù)和它所需的參數(shù)作為 Thread類的初始化參數(shù)得到一個 Thread實例,這個實例就是一個未開始的線程。

要啟動這個線程,只需調(diào)用 start() 方法,然后調(diào)用 join()方法阻塞主線程。

為什么要調(diào)用 join()方法呢?

因為我們實例化的線程和主線程(也就是我們代碼所在的線程)是分開的,那就有一個完成先后的問題。

如果我們實例化的線程先完成,那問題不大,但是要是主線程先完成了,那么正在運(yùn)行的其他子線程會全部強(qiáng)行被停止

所以調(diào)用 join()方法阻塞主線程來保證所有的子線程全部完成,下面看代碼示例:

在這個示例中我們用十個線程來訪問我的首頁 100次,并用 time庫測試所用時間。

#-*- coding: utf-8 -*
import threading
import requests
import time


# 定義回調(diào)函數(shù)
def getPage(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
    }
    for i in range(10):
        r = requests.get(url, headers=headers)
        print(r)



url = 'http://www.lxweimin.com/u/472a595d244c'

# 用來存放線程的列表
threads = []

# 記錄開始時間
start = time.time()

# 添加十個線程到線程列表中
for i in range(10):
    # 向線程列表傳遞參數(shù),通過 kwargs向回調(diào)函數(shù)傳遞參數(shù)
    t = threading.Thread(target=getPage, name=f'Thread {i}', kwargs={'url':url})
    # 通過 args向回調(diào)函數(shù)傳遞參數(shù), 注意 url后的逗號,
    # args必須是一個元組
    # t = threading.Thread(target=getPage, name=f'Thread {i}', args=(url,))
    threads.append(t)

# 開啟所有線程
for t in threads:
    t.start()

# 阻塞主線程,直到所有線程全部完成
for t in threads:
    t.join()

# 記錄結(jié)束時間
end = time.time()

print(f'多線程共用時 {end - start} 秒')

十個線程訪問一百次大概是 8.13秒

用普通的方法訪問一百次的話,大概需要 60秒

前者平均每次訪問耗時 0.08秒,而后者平均每次訪問耗時 0.6秒,多線程差不多是普通方法的 8倍

第二種:

在使用之前我們得先定義一個 Thread類的子類:

# 定義一個子類并重載 __init__()方法和 run()方法
class testThread(threading.Thread):
    def __init__(self, thread_name):
        # 初始化時調(diào)用基類的初始化函數(shù) 初始化基類
        threading.Thread.__init__(self)
        # 將參數(shù)賦值作為 self的屬性 這樣就可以將參數(shù)通過 self傳遞給 run方法
        self.thread_name = thread_name


    # 要在多線程里運(yùn)行的函數(shù)
    def run(self):
        url = 'http://www.lxweimin.com/u/472a595d244c'
        headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36'
        }
        for i in range(10):
            r = requests.get(url, headers=headers)
            print(r, ' '+self.thread_name)

后面的使用方法就和第一種方法差不多了,只是傳遞的參數(shù)發(fā)生了一點變化:

# 主頁鏈接
url = 'http://www.lxweimin.com/u/472a595d244c'

# 用來存放線程的列表
threads = []

# 記錄開始時間
start = time.time()

# 添加十個線程到線程列表中
for i in range(10):
    # 像使用普通類一樣
    t = testThread(url)
    threads.append(t)

# 開啟所有線程
for t in threads:
    t.start()

# 阻塞主線程,直到所有線程全部完成
for t in threads:
    t.join()

# 記錄結(jié)束時間
end = time.time()

print(f'多線程共用時 {end - start} 秒')

這兩種方法運(yùn)行時間沒有太大差別,但是第二種方可讀性更強(qiáng),所以推薦大家盡量使用第二種方法

三、使用多線程需要注意的問題

凡事都有兩面性,雖然使用多線程速度更快,但是多線程也會帶來一些問題,我們來看下面這個例子:

定義一個函數(shù),這個函數(shù)會在控制臺中打印 Hello World一次

現(xiàn)在我們用十個進(jìn)程來同時執(zhí)行它,看看輸出的結(jié)果:

#-*- coding: utf-8 -*
import threading


def print_hello_world():
    print('Hello World')


threads = []
for i in range(10):
    t = threading.Thread(target=print_hello_world)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

現(xiàn)在 pycharm里運(yùn)行一遍,和正常情況一樣,每一句 hello world單獨(dú)占一行,沒什么問題。

但是,現(xiàn)在我們用 idel再運(yùn)行一遍這段代碼,你會發(fā)現(xiàn):

原本應(yīng)該分成十行輸出的 hello world現(xiàn)在變成了一行

更加糟糕的是 hello world竟然變成了 world hello (#°Д°)

為什么會這樣呢?

這是因為 pycharm的控制臺是線程安全的,而 idel則沒有做線程保護(hù)

所以當(dāng)多個線程同時訪問 idel的控制臺時,就會出現(xiàn)爭搶的現(xiàn)象

比如前一個線程剛打印完 hello,這時后面的線程就根本不管前面地線程還沒打印完,直接上去就開始打印

這時就會出現(xiàn)單詞順序不對的問題,就像上面的例子一樣

那什么時候會出現(xiàn)這樣的問題呢?

其實問題的根源是多個線程同時訪問了同一個對象,造成爭搶,然后就會出問題

比如多個線程同時操作一個文件、變量、列表等等

為了防止這樣的問題產(chǎn)生,我們會給每一個可能被多個線程訪問的資源加一個資源鎖,這樣同時就只能有一個線程訪問了。

不過這樣速度就會變慢一些,而且整體速度與資源鎖的數(shù)量成反比。

所以要適當(dāng)?shù)氖褂觅Y源鎖,不要濫用,不然速度會變得很慢。

這里的資源鎖指的就是 threading.Lock() ,它的一個實例就是一個資源鎖對象

使用方法如下:

lock.acquire()
print('Hello World')
lock.release()

lock.acquire()申請上鎖,lock.release() 解鎖,在兩者之間的代碼就是線程安全的

現(xiàn)在,我們已經(jīng)能夠簡單地使用多線程了

那么,下一篇我們就把 v1.0 版的簡書爬蟲升級到 v2.0版的多線程版簡書爬蟲。

最后,覺得不錯的話,記得關(guān)注、點贊、評論哦(? ω ?)

上一篇:實戰(zhàn):爬取簡書之搭建程序框架
下一篇:實戰(zhàn):簡書爬取之多線程爬取(二)速度提升何止10倍!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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