Python爬蟲-進階篇之多線程爬蟲

1、多線程描述

?? 多線程是為了同步完成多項任務,通過提高資源使用效率來提高系統(tǒng)的效率。線程是在同一個時間需要完成多項任務的時候?qū)崿F(xiàn)的。
?? 最簡單的比喻多線程就像火車的每一節(jié)車廂,而進程則是火車。車廂離開火車是無法跑動的,同理,火車也可以有多節(jié)車廂。
?? 多線程的出現(xiàn)就是為了提高效率,但同時也會帶來一些問題。

2、threading模塊

?? threading模塊是Python中專門提供用來做多線程編程的模塊,threading模塊中最常用的類是Thread。

?? A)單線程示例代碼如下:

# 單線程
import time

def coding():
    for x in range(3):
        print('%s正在寫代碼'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在畫圖'%x)
        time.sleep(1)

def single_thread():
    coding()
    drawing()

if __name__ == '__main__':
    single_thread()

?? 運行結果:

0正在寫代碼
1正在寫代碼
2正在寫代碼
0正在畫圖
1正在畫圖
2正在畫圖

?? B)多線程示例代碼如下:

# 多線程
import threading
import time

def coding():
    for x in range(3):
        print('%s正在寫代碼'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在畫圖'%x)
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)
    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()

?? 運行結果:

0正在寫代碼
0正在畫圖
1正在寫代碼
1正在畫圖
2正在寫代碼
2正在畫圖

3、多線程相關知識點

?? A)查看線程數(shù):使用threading.enumerate()函數(shù)查看當前線程的數(shù)量。

import threading
import time

def coding():
    for x in range(3):
        print('%s正在寫代碼'%x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在畫圖'%x)
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)

    t1.start()
    t2.start()
    print(threading.enumerate())

if __name__ == '__main__':
    multi_thread()

?? 運行結果:

0正在寫代碼
[<_MainThread(MainThread, started 31204)>, <Thread(Thread-1, started 31316)>, <Thread(Thread-2, started 31052)>]
0正在畫圖
1正在寫代碼
1正在畫圖
2正在寫代碼
2正在畫圖

?? B)查看當前線程的名字:使用threading.current_thread()函數(shù)查看當前線程的信息。

import threading
import time

def coding():
    for x in range(3):
        print('%s正在寫代碼'%threading.current_thread())
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在畫圖'%threading.current_thread())
        time.sleep(1)

def multi_thread():
    t1 = threading.Thread(target = coding)
    t2 = threading.Thread(target = drawing)

    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()

?? 運行結果:

<Thread(Thread-1, started 31824)>正在寫代碼
<Thread(Thread-2, started 28336)>正在畫圖
<Thread(Thread-2, started 28336)>正在畫圖
<Thread(Thread-1, started 31824)>正在寫代碼
<Thread(Thread-1, started 31824)>正在寫代碼
<Thread(Thread-2, started 28336)>正在畫圖

?? C)繼承自threading.Thread
?? 為了讓線程代碼更好的封裝,可以使用threading模塊下的Thread類,繼承自這個類,然后實現(xiàn)run方法,線程就會自動運行run方法中的代碼,示例代碼如下:

import threading
import time

class CodingThread(threading.Thread):
   def run(self):
       for x in range(3):
           print('%s正在寫代碼'%threading.current_thread())
           time.sleep(1)

class DrawingThread(threading.Thread):
   def run(self):
       for x in range(3):
           print('%s正在畫圖'%threading.current_thread())
           time.sleep(1)

def multi_thread():
   t1 = CodingThread()
   t2 = DrawingThread()

   t1.start()
   t2.start()

if __name__ == '__main__':
   multi_thread()

?? 運行結果:

<CodingThread(Thread-1, started 16572)>正在寫代碼
<DrawingThread(Thread-2, started 21720)>正在畫圖
<CodingThread(Thread-1, started 16572)>正在寫代碼
<DrawingThread(Thread-2, started 21720)>正在畫圖
<DrawingThread(Thread-2, started 21720)>正在畫圖
<CodingThread(Thread-1, started 16572)>正在寫代碼

?? D)多線程共享全局變量的問題
?? 多線程都是在同一個進程中運行的,因此在進程中的全局變量所有線程都是可共享的,這就造成了一個問題,因為線程執(zhí)行的順序是無序的,有可能會造成數(shù)據(jù)錯誤,示例代碼如下:

import threading
value = 0

def add_value():
    global value
    for i in range(1000000):
        value += 1
    print('value: %d'%value)

def main():
    for i in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()

?? 運行結果:

value: 1205450
value: 1253480

?? 以上代碼運行的結果應該是value: 1000000、value: 2000000,但因為多線程運行的不確定性,因此最后的結果可能是隨機的,解決該問題需要用到多線程的鎖機制。

?? E)鎖機制
?? 為了解決以上使用共享全局變量的問題,threading提供了一個Lock類,這個類可以在某個縣城訪問某個變量的時候加鎖,其他線程就不能進來,直到當前線程處理完成后,把鎖釋放了,其他線程才可以進來處理,示例代碼如下:

import threading
value = 0
gLock = threading.Lock()
def add_value():
    global value
    gLock.acquire()
    for i in range(1000000):
        value += 1
    gLock.release()
    print('value: %d'%value)

def main():
    for i in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()

?? 運行結果:

value: 1000000
value: 2000000

?? F)Lock版本生產(chǎn)者和消費者模式
?? 生產(chǎn)者和消費者是多線程開發(fā)中經(jīng)常見到的一種模式,生產(chǎn)者的線程專門用來生產(chǎn)一些數(shù)據(jù),然后存放到一個中間的變量中,消費者再從這個中間的變量中取出數(shù)據(jù)進行消費,但是因為要使用中間變量,中間變量經(jīng)常是一些全局變量,因此需要使用鎖來保證數(shù)據(jù)完整性,如下是使用threading.Lock鎖實現(xiàn)的“生產(chǎn)者與消費者模式”的一個例子:

import threading
import random
import time

gMoney = 1000
gLock = threading.Lock()
# 記錄生產(chǎn)者生產(chǎn)數(shù)據(jù)的次數(shù),達到10次就不再生產(chǎn)了
gTotalTimes = 5
gTimes = 0

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gLock.acquire()
            # 如果已經(jīng)達到10次了,就不再生產(chǎn)了
            if gTimes >= gTotalTimes:
                gLock.release()
                break
            gMoney += money
            print('{}當前存入{}元錢, 剩余{}元錢'.format(threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gLock.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 500)
            gLock.acquire()
            if gMoney > money:
                gMoney -= money
                print('{}當前取出{}元錢,剩余{}元錢'.format(threading.current_thread(), money, gMoney))
                time.sleep(0.5)
            else:
                # 如果錢不夠了,有可能是已經(jīng)超過了次數(shù),這時候就判斷一下
                if gTimes >= gTotalTimes:
                    gLock.release()
                    break
                print('{}當前想取{}元錢,剩余{}元錢,余額不足!'.format(threading.current_thread(), money, gMoney))
            gLock.release()

def main():
    for i in range(5):
        Consumer(name='消費者線程{}'.format(i)).start()

    for i in range(5):
        Producer(name='生產(chǎn)者線程{}'.format(i)).start()

if __name__ == '__main__':
    main()

?? 運行結果:

<Consumer(消費者線程0, started 40748)>當前取出475元錢,剩余525元錢
<Consumer(消費者線程1, started 36784)>當前取出183元錢,剩余342元錢
<Consumer(消費者線程2, started 36072)>當前想取366元錢,剩余342元錢,余額不足!
<Consumer(消費者線程2, started 36072)>當前取出148元錢,剩余194元錢
<Consumer(消費者線程4, started 37552)>當前想取226元錢,剩余194元錢,余額不足!
<Producer(生產(chǎn)者線程0, started 37884)>當前存入603元錢, 剩余797元錢
<Producer(生產(chǎn)者線程1, started 40416)>當前存入883元錢, 剩余1680元錢
<Producer(生產(chǎn)者線程2, started 40936)>當前存入705元錢, 剩余2385元錢
<Producer(生產(chǎn)者線程3, started 27848)>當前存入337元錢, 剩余2722元錢
<Producer(生產(chǎn)者線程4, started 35024)>當前存入195元錢, 剩余2917元錢
<Consumer(消費者線程0, started 40748)>當前取出116元錢,剩余2801元錢
<Consumer(消費者線程1, started 36784)>當前取出398元錢,剩余2403元錢
<Consumer(消費者線程3, started 37104)>當前取出121元錢,剩余2282元錢
<Consumer(消費者線程2, started 36072)>當前取出108元錢,剩余2174元錢
<Consumer(消費者線程4, started 37552)>當前取出269元錢,剩余1905元錢
<Consumer(消費者線程0, started 40748)>當前取出209元錢,剩余1696元錢
<Consumer(消費者線程1, started 36784)>當前取出125元錢,剩余1571元錢
<Consumer(消費者線程3, started 37104)>當前取出356元錢,剩余1215元錢
<Consumer(消費者線程2, started 36072)>當前取出358元錢,剩余857元錢
<Consumer(消費者線程4, started 37552)>當前取出302元錢,剩余555元錢
<Consumer(消費者線程0, started 40748)>當前取出193元錢,剩余362元錢
<Consumer(消費者線程1, started 36784)>當前取出133元錢,剩余229元錢
<Consumer(消費者線程3, started 37104)>當前取出188元錢,剩余41元錢

?? G)Condition版本生產(chǎn)者和消費者模式
?? Lock版本的生產(chǎn)者與消費者模式可以正常的運行,但是存在一個不足,在消費者中,總是通過while True死循環(huán)并且上鎖的方式去判斷錢夠不夠,上鎖是一個很耗費CPU資源的行為,因此這種方式不是最好的,還有一種更好的方式便是threading.Condition來實現(xiàn)。threading.Condition可以在沒有數(shù)據(jù)的時候處于阻塞等待狀態(tài),一旦有合適的數(shù)據(jù)了,還可以使用notify相關的函數(shù)來通知其他處于等待狀態(tài)的線程,這樣就可以不用做一些無用的上鎖和解鎖的操作,可以提高程序的性能。
?? 首先對threading.Condition相關的函數(shù)做個介紹,threading.Condition類似threading.Lock,可以在修改全局數(shù)據(jù)的時候進行上鎖,也可以在修改完畢后進行解鎖,如下是一些常用的函數(shù):
?? ?? G.1)acquire:上鎖
?? ?? G.2)release:解鎖
?? ?? G.3)wait:將當前線程處于等待狀態(tài),并且會釋放鎖,可以被其他線程使用notifynotify_all函數(shù)喚醒,被喚醒后會繼續(xù)等待上鎖,上鎖后繼續(xù)執(zhí)行下面的代碼。
?? ?? G.4)notify:通知某個正在等待的線程,默認是第1個等待的線程。
?? ?? G.5)notify_all:通知所有正在等待的線程,notifynotify_all不會釋放鎖,并且需要在release之間調(diào)用。
如下示例代碼是Condition版的生產(chǎn)者與消費者模式的一個例子:

import threading
import random
import time

gMoney = 1000
gCondition = threading.Condition()
# 記錄生產(chǎn)者生產(chǎn)數(shù)據(jù)的次數(shù),達到10次就不再生產(chǎn)了
gTotalTimes = 5
gTimes = 0
class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gCondition.acquire()
            # 如果已經(jīng)達到5次了,就不再生產(chǎn)了
            if gTimes >= gTotalTimes:
                gCondition.release()
                break
            gMoney += money
            print('{}當前存入{}元錢, 剩余{}元錢'.format(threading.current_thread(), money, gMoney))
            gTimes += 1
            gCondition.notify_all()
            gCondition.release()
            time.sleep(0.5)

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        while True:
            money = random.randint(100, 500)
            gCondition.acquire()
            while gMoney < money:
                if gTimes >= gTotalTimes:
                    gCondition.release()
                    return
                print('{}準備消費{}元錢,剩余{}元錢,余額不足!'.format(threading.current_thread(), money, gMoney))
                gCondition.wait()
            gMoney -= money
            print('{}當前想取{}元錢,剩余{}元錢'.format(threading.current_thread(), money, gMoney))
            gCondition.release()
            time.sleep(0.5)

def main():
    for i in range(5):
        Consumer(name='消費者線程{}'.format(i)).start()

    for i in range(5):
        Producer(name='生產(chǎn)者線程{}'.format(i)).start()

if __name__ == '__main__':
    main()

?? 運行結果:

<Consumer(消費者線程0, started 21960)>當前想取334元錢,剩余666元錢
<Consumer(消費者線程1, started 36664)>當前想取233元錢,剩余433元錢
<Consumer(消費者線程2, started 45600)>準備消費495元錢,剩余433元錢,余額不足!
<Consumer(消費者線程3, started 37768)>當前想取232元錢,剩余201元錢
<Consumer(消費者線程4, started 28312)>準備消費208元錢,剩余201元錢,余額不足!
<Producer(生產(chǎn)者線程0, started 40376)>當前存入517元錢, 剩余718元錢
<Consumer(消費者線程2, started 45600)>當前想取495元錢,剩余223元錢
<Consumer(消費者線程4, started 28312)>當前想取208元錢,剩余15元錢
<Producer(生產(chǎn)者線程1, started 42908)>當前存入931元錢, 剩余946元錢
<Producer(生產(chǎn)者線程2, started 44140)>當前存入566元錢, 剩余1512元錢
<Producer(生產(chǎn)者線程3, started 46256)>當前存入556元錢, 剩余2068元錢
<Producer(生產(chǎn)者線程4, started 43412)>當前存入976元錢, 剩余3044元錢
<Consumer(消費者線程0, started 21960)>當前想取192元錢,剩余2852元錢
<Consumer(消費者線程1, started 36664)>當前想取333元錢,剩余2519元錢
<Consumer(消費者線程3, started 37768)>當前想取284元錢,剩余2235元錢
<Consumer(消費者線程4, started 28312)>當前想取186元錢,剩余2049元錢
<Consumer(消費者線程2, started 45600)>當前想取256元錢,剩余1793元錢
<Consumer(消費者線程1, started 36664)>當前想取432元錢,剩余1361元錢
<Consumer(消費者線程0, started 21960)>當前想取145元錢,剩余1216元錢
<Consumer(消費者線程3, started 37768)>當前想取489元錢,剩余727元錢
<Consumer(消費者線程2, started 45600)>當前想取267元錢,剩余460元錢
<Consumer(消費者線程0, started 21960)>當前想取212元錢,剩余248元錢
<Consumer(消費者線程3, started 37768)>當前想取232元錢,剩余16元錢

?? H)Queue線程安全隊列
?? 在線程中,訪問一些全局變量,加鎖是一個經(jīng)常的過程,如果你想把一些數(shù)據(jù)存儲到某個隊列中,那么Python內(nèi)置了一個線程安全的模塊叫做queue模塊。
?? Python中的queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先進先出)隊列Queue、LIFO(后進先出)隊列LifoQueue。
?? 這些隊列都實現(xiàn)了鎖原語(可以理解為原子操作,即要么不做,要么都做完),能夠在多線程中直接使用,可以使用隊列來實現(xiàn)線程間的同步,相關函數(shù)如下:
?? a)初始化Queue(maxsize):創(chuàng)建一個先進先出的隊列
?? b)qsize():返回隊列的大小
?? c)empty():判斷隊列是否為空
?? d)full():判斷隊列是否滿了
?? e)get():從隊列中取出一個數(shù)據(jù)
?? f)put():將一個數(shù)據(jù)放到隊列中

from queue import Queue
import time
import threading
def set_value(q):
    index = 0
    while True:
        q.put(index)
        index += 1
        time.sleep(3)

def get_value(q):
    while True:
        print(q.get())

def main():
    q =Queue(4)
    t1 = threading.Thread(target=set_value, args=[q])
    t2 = threading.Thread(target=get_value, args=[q])

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

?? I)GIL全局解釋器鎖
?? Python自帶的解釋器是CPython,CPython解釋器的多線程實際上是一個假的多線程(在多核CPU中,只能利用一核,不能利用多核),同一時刻只有一個線程在執(zhí)行,為了保證同一時刻只有一個線程在執(zhí)行,在CPython解釋器中有一個GIL(全局解釋器鎖),這個解釋器鎖是有必要的,因為CPython解釋器的內(nèi)存管理不是線程安全的,當然除了CPython解釋器,還有其他的解釋器,有些解釋器是沒有GIL鎖的,如下:
?? a)Jython:用Java實現(xiàn)的Python解釋器,不存在GIL鎖,更多詳情見:https://zh.wikipedia.org/wiki/Jython
?? b)IronPython:用.Net實現(xiàn)的Python解釋器,不存在GIL鎖,更多詳情見:https://zh.wikipedia.org/wiki/IronPython
?? c)PyPy:用Python實現(xiàn)的Python解釋器,存在GIL鎖,更多詳情見:https://zh.wikipedia.org/wiki/PyPy。
?? GIL雖然是一個假的多線程,但是在處理一些IO操作(比如文件讀寫和網(wǎng)絡請求)還是可以在很大程度上提高效率的,在IO操作上建議使用多線程提高效率,在一些CPU計算操作上不建議使用多線程,而建議使用多進程。

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

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

  • 有些時候,比如下載圖片,因為下載圖片是一個耗時的操作。如果采用之前那種同步的方式下載。那效率肯會特別慢。這時候我們...
    朝南而行_閱讀 803評論 1 4
  • 線程 操作系統(tǒng)線程理論 線程概念的引入背景 進程 之前我們已經(jīng)了解了操作系統(tǒng)中進程的概念,程序并不能單獨運行,只有...
    go以恒閱讀 1,791評論 0 6
  • 多線程爬蟲 在爬取信息是,如果需要爬取大量資源,按順序一個個來可能會耗費大量時間,這是我們可以通過多線程來完成工作...
    喧嘩與搏殺閱讀 936評論 0 3
  • 大學的時候面試,時常被問到線程和進程的區(qū)別。時至今日,碰到爬蟲中正好也要運用,再拿出來梳理一波。 首先明確一...
    興華的mark閱讀 655評論 0 3
  • 憋的太難受 很想就地解決 又到處遇見路人 小腹傳來陣陣疼痛 頭頂生煙 滿頭大汗 我左右觀望 神意識傳來滴噠滴噠的聲...
    朱火火閱讀 603評論 3 25

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