基于生產(chǎn)者消費者模式的多線程爬蟲案例

Python雖然在爬蟲領(lǐng)域占用十分重要的地位瘫絮,但這時涨冀,經(jīng)常有人會嘲諷到:Python這么慢的語言用來寫爬蟲一點都不香,Java檀何、Go......哪個不比Python快坝恪!

雖然Python性能確實不好频鉴,但改善優(yōu)化這方面栓辜,其實也是比較簡單的。接下來筆者將使用一個常見的設(shè)計模式:生產(chǎn)者消費者模式垛孔,編寫一個簡書博客文章的爬蟲藕甩。

思路

首先,我們需要先了解一下生產(chǎn)者消費者模式周荐。

生產(chǎn)者消費者模式是通過一個容器來解決生產(chǎn)者和消費者的強(qiáng)耦合問題的狭莱。簡單來講,就是使用一個線程安全的容器進(jìn)行多線程的分工操作概作。也就是說腋妙,生產(chǎn)者消費者模式一定是一個異步模式。

關(guān)于線程安全的容器讯榕,在Python中第一個想到的就是隊列骤素,當(dāng)然,使用redis的數(shù)據(jù)結(jié)構(gòu)當(dāng)作該容器也是可以的愚屁。

然后济竹,先確定生產(chǎn)者以及消費者。生產(chǎn)者在該爬蟲中的任務(wù)即請求頁面并解析頁面霎槐,生成url鏈接送浊,放入到容器中;消費者則是對容器中的鏈接進(jìn)行二次請求丘跌,提取博客內(nèi)容以及保存袭景。

本文中將使用redis作為容器唁桩,并使用queue作為頁碼隊列。

依賴庫

import os
import re
import random
import threading
import requests
import redis
import fake_useragent
from queue import Queue
from bs4 import BeautifulSoup
  • os模塊主要用于進(jìn)行文件路徑操作與文件保存
  • re模塊在本文中主要用于替換數(shù)據(jù)而不是解析頁面浴讯,當(dāng)然朵夏,也可以用來解析頁面
  • random模塊為隨機(jī)模塊
  • threading模塊即多線程模塊
  • requests模塊依然用于進(jìn)行頁面請求
  • redis模塊用于操作redis數(shù)據(jù)庫
  • fake_useragent模塊用于自動生成user-agent蔼啦,也可以直接復(fù)制瀏覽器的user-agent
  • queue模塊即阻塞隊列
  • bs4模塊用于解析頁面榆纽,提取數(shù)據(jù),也可使用xpath語法或者正則

生產(chǎn)者類

首先捏肢,生產(chǎn)者顧名思義奈籽,是需要生產(chǎn)數(shù)據(jù),也就是爬蟲中的頁面請求部分鸵赫。

class Producer(threading.Thread):
    """生產(chǎn)者類"""
    def __init__(self, cli, queue):
        super().__init__()
        self.cli = cli
        self.queue = queue

    def get_html(self):
        """
        獲取頁面源代碼
        :return: response.text/None
        """
        pass

    def get_url(self, text):
        """
        生產(chǎn)數(shù)據(jù)(url)
        :param text: 頁面源代碼
        :return: None
        """
        pass
        
    def run(self):
        """
        生產(chǎn)者線程執(zhí)行函數(shù)
        :return: None
        """
        pass

上述代碼為該生產(chǎn)者類的一個簡單框架衣屏,既然是一個多線程爬蟲,自然要繼承threading.Thread這個類辩棒,再init方法里面也要通過super實現(xiàn)父類的init方法狼忱。

在init方法里有兩個屬性,分別代表了1個redis對象和一個隊列對象一睁。redis對象很明顯钻弄,是用來對redis進(jìn)行讀寫的,而這個對列對象者吁,則是1個頁碼池窘俺,出于線程安全考慮,使用了隊列复凳。

get_html這個方法比較簡單瘤泪,首先在頁碼池里面獲取1個頁碼,然后使用requests進(jìn)行http請求育八,返回請求頁面的源代碼对途,如果池中沒有頁碼了,則返回None髓棋。

get_url這個方法需要在頁面源代碼中提取我們需要的信息实檀,即文章鏈接。獲取到的文章鏈接將保存在redis中的集合對象中仲锄,以免出現(xiàn)重復(fù)文章劲妙。

run方法是線程的執(zhí)行方法,也就是該生產(chǎn)者類的”發(fā)動機(jī)“儒喊,詳細(xì)代碼如下镣奋。

def run(self):
        """
        生產(chǎn)者線程執(zhí)行函數(shù)
        :return: None
        """
        while True:
            # redis集合中的數(shù)據(jù)大于1000時結(jié)束循環(huán)(沒有實際意義,起到控制程序工作量的作用)
            if self.cli.scard('jianshu:start_urls') > 1000:
                break
            else:
                res = self.get_html()
                if res is None:
                    continue
                else:
                    self.get_url(res)
        print(f'name:{threading.current_thread().name} is over!!!')

消費者類

消費者怀愧,自然要消費數(shù)據(jù)侨颈,當(dāng)redis中開始出現(xiàn)文章鏈接時余赢,消費者類就要大顯身手了。

class Consumer(threading.Thread):
    """消費者類"""
    def __init__(self, cli):
        super().__init__()
        self.cli = cli

    def get_data(self):
        """
        二次請求鏈接
        :return: response.text
        """
        pass

    def analysis(self, text):
        """
        獲取頁面中的需要保存的html
        :param text: 頁面源代碼
        :return: section
        """
        pass

    def write_in(self, section):
        """
        將獲取到的數(shù)據(jù)保存到本地
        :param section: section標(biāo)簽中的內(nèi)容
        :return: None
        """
        pass
        
    def run(self):
        """
        消費者線程執(zhí)行函數(shù)
        :return: None
        """
        pass

在消費者類中哈垢,init方法不需要再傳入頁碼了妻柒,消費者類的工作基本上都圍繞在redis與頁面解析上。

get_data方法類似生產(chǎn)者類中的get_html方法耘分,即請求鏈接举塔,返回頁面源代碼。

analysis方法則是需要進(jìn)行頁面解析求泰,即獲取文章頁面的html內(nèi)容央渣。因為markdown是完全兼容html格式的,所以將獲取到的html的文章內(nèi)容部分保存為markdown渴频,是可以還原簡書的文章格式的芽丹。通過解析可看出,只需要保存< section >標(biāo)簽內(nèi)的內(nèi)容即可卜朗。即該方法返回< section >的soup對象拔第。

write_in方法則是將獲取到的html文本保存為markdown的格式,雖然描述起來簡單场钉,但這一步是最麻煩的蚊俺。因為,我們需要去修改文章內(nèi)的圖片鏈接惹悄,將圖片下載到本地春叫,每篇文章都需要1個獨立文件夾,再將圖片鏈接替換為相對路徑即可實現(xiàn)文章的保存泣港。

關(guān)于run方法暂殖,如下所示。

def run(self):
        """
        消費者線程執(zhí)行函數(shù)
        :return: None
        """
        while True:
            text = self.get_data()
            if text is None:
                continue
            else:
                section = self.analysis(text)
                self.write_in(section)
                # 如果redis集合中沒有數(shù)據(jù)則退出循環(huán)
                if self.cli.scard('jianshu:start_urls') == 0:
                    break
        print('大功告成5鄙础G好俊!')

需要注意的是坡氯,創(chuàng)建線程時晨横,盡量要保證生產(chǎn)的線程多一點,否則上述代碼的消費者if語句會直接退出程序箫柳,這個if語句也只是用來控制程序的手形,可隨意增改。

結(jié)語

上述兩個類的具體實現(xiàn)可以自由發(fā)揮悯恍,其實說到底库糠,爬蟲程序的主觀性實在是太大了。涮毫。瞬欧。關(guān)鍵一點在于贷屎,不要讓別人的代碼影響了你的思路,優(yōu)秀的爬蟲工程師往往都能第一時間想到解決問題的思路艘虎,然后一步步去解決思路里的麻煩唉侄。

爬蟲程序并沒有特么難的地方,往往都是一些”麻煩“在不斷的困擾你野建,有時候勸退你的往往都是麻煩属划,這時候只要耐心就好了。

關(guān)于該爬蟲的具體實現(xiàn)贬墩,可參考下面的Github鏈接榴嗅。

https://github.com/macxin123/spider/blob/master/jianshu/threading_jianshu.py

由于最近工作比較忙,所以鴿了很久陶舞。。绪励。上述的爬蟲也已經(jīng)完成很久了肿孵,所以在可用性上也許已經(jīng)崩了,不過還是那句話疏魏,爬蟲最重要的還是思路以及愿不愿意去解決麻煩停做。

爬蟲界的最大的麻煩也許就是驗證碼了,下篇文章筆者將寫一篇關(guān)于驗證碼與爬蟲的案例大莫,敬請期待蛉腌!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市只厘,隨后出現(xiàn)的幾起案子烙丛,更是在濱河造成了極大的恐慌,老刑警劉巖羔味,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件河咽,死亡現(xiàn)場離奇詭異,居然都是意外死亡赋元,警方通過查閱死者的電腦和手機(jī)忘蟹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搁凸,“玉大人媚值,你說我怎么就攤上這事』ぬ牵” “怎么了褥芒?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長椅文。 經(jīng)常有香客問我喂很,道長惜颇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任少辣,我火速辦了婚禮凌摄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘漓帅。我一直安慰自己锨亏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布忙干。 她就那樣靜靜地躺著器予,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捐迫。 梳的紋絲不亂的頭發(fā)上乾翔,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機(jī)與錄音施戴,去河邊找鬼反浓。 笑死,一個胖子當(dāng)著我的面吹牛赞哗,可吹牛的內(nèi)容都是我干的雷则。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼肪笋,長吁一口氣:“原來是場噩夢啊……” “哼月劈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起藤乙,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤猜揪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后湾盒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湿右,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年罚勾,在試婚紗的時候發(fā)現(xiàn)自己被綠了毅人。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡尖殃,死狀恐怖丈莺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情送丰,我是刑警寧澤缔俄,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響俐载,放射性物質(zhì)發(fā)生泄漏蟹略。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一遏佣、第九天 我趴在偏房一處隱蔽的房頂上張望挖炬。 院中可真熱鬧,春花似錦状婶、人聲如沸意敛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽草姻。三九已至,卻和暖如春稍刀,著一層夾襖步出監(jiān)牢的瞬間撩独,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工掉丽, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留跌榔,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓捶障,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纲刀。 傳聞我的和親對象是個殘疾皇子项炼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351