爬蟲進(jìn)階之多線程爬蟲

有些時候,比如下載圖片,因為下載圖片是一個耗時的操作。如果采用之前那種同步的方式下載。那效率肯會特別慢。這時候我們就可以考慮使用多線程的方式來下載圖片。

多線程介紹:

多線程是為了同步完成多項任務(wù),通過提高資源使用效率來提高系統(tǒng)的效率。線程是在同一時間需要完成多項任務(wù)的時候?qū)崿F(xiàn)的。
最簡單的比喻多線程就像火車的每一節(jié)車廂,而進(jìn)程則是火車。車廂離開火車是無法跑動的,同理火車也可以有多節(jié)車廂。多線程的出現(xiàn)就是為了提高效率。同時它的出現(xiàn)也帶來了一些問題。更多介紹請參考:https://baike.baidu.com/item/多線程/1190404?fr=aladdin

threading模塊介紹:
threading模塊是python中專門提供用來做多線程編程的模塊。threading模塊中最常用的類是Thread。以下看一個簡單的多線程程序:

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 single_thread():
    coding()
    drawing()

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

    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()

查看線程數(shù):

使用threading.enumerate()函數(shù)便可以看到當(dāng)前線程的數(shù)量。

查看當(dāng)前線程的名字:

使用threading.current_thread()可以看到當(dāng)前線程的信息。

繼承自threading.Thread類:

為了讓線程代碼更好的封裝??梢允褂?code>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()

多線程共享全局變量的問題:

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

import threading

tickets = 0

def get_ticket():
    global tickets
    for x in range(1000000):
        tickets += 1
    print('tickets:%d'%tickets)

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

if __name__ == '__main__':
    main()

以上結(jié)果正常來講應(yīng)該是6,但是因為多線程運行的不確定性。因此最后的結(jié)果可能是隨機的。

鎖機制:

為了解決以上使用共享全局變量的問題。threading提供了一個Lock類,這個類可以在某個線程訪問某個變量的時候加鎖,其他線程此時就不能進(jìn)來,直到當(dāng)前線程處理完后,把鎖釋放了,其他線程才能進(jìn)來處理。示例代碼如下:

import threading

VALUE = 0

gLock = threading.Lock()

def add_value():
    global VALUE
    gLock.acquire()
    for x in range(1000000):
        VALUE += 1
    gLock.release()
    print('value:%d'%VALUE)

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

if __name__ == '__main__':
    main()

Lock版本生產(chǎn)者和消費者模式:

生產(chǎn)者消費者模式是多線程開發(fā)中經(jīng)常見到的一種模式。生產(chǎn)者的線程專門用來生產(chǎn)一些數(shù)據(jù),然后存放到一個中間的變量中。消費者再從這個中間的變量中取出數(shù)據(jù)進(jìn)行消費。但是因為要使用中間變量,中間變量經(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ù),達(dá)到10次就不再生產(chǎn)
gTimes = 0

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

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

def main():
    for x in range(5):
        Consumer(name='消費者線程%d'%x).start()

    for x in range(5):
        Producer(name='生產(chǎn)者線程%d'%x).start()

if __name__ == '__main__':
    main()

Condition版的生產(chǎn)者與消費者模式:

Lock版本的生產(chǎn)者與消費者模式可以正常的運行。但是存在一個不足,在消費者中,總是通過while True死循環(huán)并且上鎖的方式去判斷錢夠不夠。上鎖是一個很耗費CPU資源的行為。因此這種方式不是最好的。還有一種更好的方式便是使用threading.Condition來實現(xiàn)。threading.Condition可以在沒有數(shù)據(jù)的時候處于阻塞等待狀態(tài)。一旦有合適的數(shù)據(jù)了,還可以使用notify相關(guān)的函數(shù)來通知其他處于等待狀態(tài)的線程。這樣就可以不用做一些無用的上鎖和解鎖的操作??梢蕴岣叱绦虻男阅堋J紫葘?code>threading.Condition相關(guān)的函數(shù)做個介紹,threading.Condition類似threading.Lock,可以在修改全局?jǐn)?shù)據(jù)的時候進(jìn)行上鎖,也可以在修改完畢后進(jìn)行解鎖。以下將一些常用的函數(shù)做個簡單的介紹:

  1. acquire:上鎖。
  2. release:解鎖。
  3. wait:將當(dāng)前線程處于等待狀態(tài),并且會釋放鎖。可以被其他線程使用notifynotify_all函數(shù)喚醒。被喚醒后會繼續(xù)等待上鎖,上鎖后繼續(xù)執(zhí)行下面的代碼。
  4. notify:通知某個正在等待的線程,默認(rèn)是第1個等待的線程。
  5. notify_all:通知所有正在等待的線程。notifynotify_all不會釋放鎖。并且需要在release之前調(diào)用。

Condition版的生產(chǎn)者與消費者模式代碼如下:

import threading
import random
import time

gMoney = 1000
gCondition = threading.Condition()
gTimes = 0
gTotalTimes = 5

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gCondition
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gCondition.acquire()
            if gTimes >= gTotalTimes:
                gCondition.release()
                print('當(dāng)前生產(chǎn)者總共生產(chǎn)了%s次'%gTimes)
                break
            gMoney += money
            print('%s當(dāng)前存入%s元錢,剩余%s元錢' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gCondition.notify_all()
            gCondition.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gCondition
        while True:
            money = random.randint(100, 500)
            gCondition.acquire()
            # 這里要給個while循環(huán)判斷,因為等輪到這個線程的時候
            # 條件有可能又不滿足了
            while gMoney < money:
                if gTimes >= gTotalTimes:
                    gCondition.release()
                    return
                print('%s準(zhǔn)備取%s元錢,剩余%s元錢,不足!'%(threading.current_thread(),money,gMoney))
                gCondition.wait()
            gMoney -= money
            print('%s當(dāng)前取出%s元錢,剩余%s元錢' % (threading.current_thread(), money, gMoney))
            time.sleep(0.5)
            gCondition.release()

def main():
    for x in range(5):
        Consumer(name='消費者線程%d'%x).start()

    for x in range(2):
        Producer(name='生產(chǎn)者線程%d'%x).start()

if __name__ == '__main__':
    main()

Queue線程安全隊列:

在線程中,訪問一些全局變量,加鎖是一個經(jīng)常的過程。如果你是想把一些數(shù)據(jù)存儲到某個隊列中,那么Python內(nèi)置了一個線程安全的模塊叫做queue模塊。Python中的queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先進(jìn)先出)隊列Queue,LIFO(后入先出)隊列LifoQueue。這些隊列都實現(xiàn)了鎖原語(可以理解為原子操作,即要么不做,要么都做完),能夠在多線程中直接使用??梢允褂藐犃衼韺崿F(xiàn)線程間的同步。相關(guān)的函數(shù)如下:

  1. 初始化Queue(maxsize):創(chuàng)建一個先進(jìn)先出的隊列。
  2. qsize():返回隊列的大小。
  3. empty():判斷隊列是否為空。
  4. full():判斷隊列是否滿了。
  5. get():從隊列中取最后一個數(shù)據(jù)。
  6. put():將一個數(shù)據(jù)放到隊列中。

使用生產(chǎn)者與消費者模式多線程下載表情包:

import threading
import requests
from lxml import etree
from urllib import request
import os
import re
from queue import Queue

class Producer(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(Producer, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            self.parse_page(url)

    def parse_page(self,url):
        response = requests.get(url,headers=self.headers)
        text = response.text
        html = etree.HTML(text)
        imgs = html.xpath("http://div[@class='page-content text-center']//a//img")
        for img in imgs:
            if img.get('class') == 'gif':
                continue
            img_url = img.xpath(".//@data-original")[0]
            suffix = os.path.splitext(img_url)[1]
            alt = img.xpath(".//@alt")[0]
            alt = re.sub(r'[,。??,/\\·]','',alt)
            img_name = alt + suffix
            self.img_queue.put((img_url,img_name))

class Consumer(threading.Thread):
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(Consumer, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.img_queue.empty():
                if self.page_queue.empty():
                    return
            img = self.img_queue.get(block=True)
            url,filename = img
            request.urlretrieve(url,'images/'+filename)
            print(filename+'  下載完成!')

def main():
    page_queue = Queue(100)
    img_queue = Queue(500)
    for x in range(1,101):
        url = "http://www.doutula.com/photo/list/?page=%d" % x
        page_queue.put(url)

    for x in range(5):
        t = Producer(page_queue,img_queue)
        t.start()

    for x in range(5):
        t = Consumer(page_queue,img_queue)
        t.start()

if __name__ == '__main__':
    main()

GIL全局解釋器鎖:

Python自帶的解釋器是CPython。CPython解釋器的多線程實際上是一個假的多線程(在多核CPU中,只能利用一核,不能利用多核)。同一時刻只有一個線程在執(zhí)行,為了保證同一時刻只有一個線程在執(zhí)行,在CPython解釋器中有一個東西叫做GIL(Global Intepreter Lock),叫做全局解釋器鎖。這個解釋器鎖是有必要的。因為CPython解釋器的內(nèi)存管理不是線程安全的。當(dāng)然除了CPython解釋器,還有其他的解釋器,有些解釋器是沒有GIL鎖的,見下面:

  1. Jython:用Java實現(xiàn)的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/Jython
  2. IronPython:用.net實現(xiàn)的Python解釋器。不存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/IronPython
  3. PyPy:用Python實現(xiàn)的Python解釋器。存在GIL鎖。更多詳情請見:https://zh.wikipedia.org/wiki/PyPy
    GIL雖然是一個假的多線程。但是在處理一些IO操作(比如文件讀寫和網(wǎng)絡(luò)請求)還是可以在很大程度上提高效率的。在IO操作上建議使用多線程提高效率。在一些CPU計算操作上不建議使用多線程,而建議使用多進(jìn)程。

多線程下載百思不得姐段子作業(yè):

import requests
from lxml import etree
import threading
from queue import Queue
import csv

class BSSpider(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,joke_queue,*args,**kwargs):
        super(BSSpider, self).__init__(*args,**kwargs)
        self.base_domain = 'http://www.budejie.com'
        self.page_queue = page_queue
        self.joke_queue = joke_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            response = requests.get(url, headers=self.headers)
            text = response.text
            html = etree.HTML(text)
            descs = html.xpath("http://div[@class='j-r-list-c-desc']")
            for desc in descs:
                jokes = desc.xpath(".//text()")
                joke = "\n".join(jokes).strip()
                link = self.base_domain+desc.xpath(".//a/@href")[0]
                self.joke_queue.put((joke,link))
            print('='*30+"第%s頁下載完成!"%url.split('/')[-1]+"="*30)

class BSWriter(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }

    def __init__(self, joke_queue, writer,gLock, *args, **kwargs):
        super(BSWriter, self).__init__(*args, **kwargs)
        self.joke_queue = joke_queue
        self.writer = writer
        self.lock = gLock

    def run(self):
        while True:
            try:
                joke_info = self.joke_queue.get(timeout=40)
                joke,link = joke_info
                self.lock.acquire()
                self.writer.writerow((joke,link))
                self.lock.release()
                print('保存一條')
            except:
                break

def main():
    page_queue = Queue(10)
    joke_queue = Queue(500)
    gLock = threading.Lock()
    fp = open('bsbdj.csv', 'a',newline='', encoding='utf-8')
    writer = csv.writer(fp)
    writer.writerow(('content', 'link'))

    for x in range(1,11):
        url = 'http://www.budejie.com/text/%d' % x
        page_queue.put(url)

    for x in range(5):
        t = BSSpider(page_queue,joke_queue)
        t.start()

    for x in range(5):
        t = BSWriter(joke_queue,writer,gLock)
        t.start()

if __name__ == '__main__':
    main()

上一篇:數(shù)據(jù)存儲之使用MongoDB數(shù)據(jù)庫存儲數(shù)據(jù)
下一篇:爬蟲進(jìn)階之動態(tài)網(wǎng)頁(Ajax)數(shù)據(jù)抓取

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

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

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