分布式爬蟲筆記(一)- 非框架實現(xiàn)的Crawlspider

不久前寫過一篇使用Scrapy框架寫的Crawlspider爬蟲筆記(五)- 關(guān)于Scrapy 全站遍歷Crawlspider翠语,本次我再次沿用上次的網(wǎng)站實現(xiàn)全站爬蟲韵卤,希望目標(biāo)網(wǎng)址的小伙伴原諒我~~~
目標(biāo)站點:www.cuiqingcai.com
代碼已經(jīng)上存到github下載

代碼已經(jīng)有詳細(xì)的注釋,這里附上流程圖和部分代碼解析~~

主要用到的庫和技術(shù)

import urllib2 
from collections import deque  # deque是為了高效實現(xiàn)插入和刪除操作的雙向列表
import httplib
from lxml import etree
from pybloom import BloomFilter
  • urllib2:偽造請求
  • deque:雙向列表着撩,存儲待下載的URL
  • httplib:生成md5值
  • BloomFilter:用于URL過濾
  • etree:獲取頁面诅福,結(jié)合xpath過濾頁面中的URL

偽流程圖

偽流程圖

首先是紫色部分,就是主要流程:通過主流程來控制整個爬取的過程拖叙。
然后氓润,主要流程里面有三個偽小流程【初始化流程__ini__】、【獲取URL流程getqueneURL】和【爬取流程getPageContent】薯鳍。

重點代碼說明

【初始化流程__ini__】

  • 斷點續(xù)傳邏輯:通過將下載過的md5和url記錄到文件中的方式咖气,在每次執(zhí)行腳本前分析已記錄的md5值的方式來實現(xiàn)斷點續(xù)傳
self.md5_file = open(self.md5_file_name, 'r')  # 只讀方式打開md5的文件
self.md5_lists = self.md5_file.readlines()  # 將文件的內(nèi)容以列表的方式讀取出來
self.md5_file.close()  # 關(guān)閉文件
for md5_item in self.md5_lists:  # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
    self.download_bf.add(md5_item[:-2])  # 將去掉回車換行符的md5寫入BloomFilter對象當(dāng)中

【獲取URL流程getqueneURL】

  • deque雙向列表:通過popleft()高效讀取URL爬取。(這里有個GIL鎖挖滤,想了解可以自己深入了解下~~~)
# 用于記錄爬取URL的隊列(先進(jìn)先出)
now_queue = deque()  # 爬取隊列
bak_queue = deque()  # 備用隊列(爬取隊列為空的時候置換)
···
url = self.now_queue.popleft()  # 從左邊進(jìn)行獲取隊列內(nèi)容
  • try except:隊列為空的時候崩溪,增加深度&置換隊列
try:
    url = self.now_queue.popleft()  # 從左邊進(jìn)行獲取隊列內(nèi)容
    return url
except IndexError:
    self.now_level += 1  # 深度加一
    if self.now_level == self.max_level:  # 如果深度與設(shè)定的最大深度相等,停止爬蟲返回None
        return None
    if len(self.bak_queue) == 0:  # 如果備用隊列長度為0斩松,停止爬蟲返回None
        return None
    self.now_queue = self.bak_queue  # 將備用隊列傳遞給爬取隊列
    self.bak_queue = deque()  # 重置備用隊列
    return self.getQueneURL()  # 繼續(xù)執(zhí)行dequeuUrl方法伶唯,直到獲取到URL或者None

【爬取流程getPageContent】

  • md5:__init__流程中的斷點續(xù)傳和過濾URL
# 計算md5的值并將md5和寫入到文件中
dumd5 = hashlib.md5(now_url).hexdigest()  # 生成md5值
self.md5_lists.append(dumd5)  # 將md5加入到md5的列表中
self.md5_file.write(dumd5 + '\r\n')  # 將md5寫入文件
self.url_file.write(now_url + '\r\n')  # 將url寫入文件
self.download_bf.add(dumd5)  # 將md5加入到BloomFilter對象當(dāng)中
num_downloaded_pages += 1  # 用于統(tǒng)計當(dāng)前下載頁面的總數(shù)
  • xpath:獲取當(dāng)前文件中所有的URL
html = etree.HTML(html_page.lower().decode('utf-8'))
hrefs = html.xpath(u"http://a")
for href in hrefs:
    # 用于處理xpath抓取到的href,獲取有用的
    try:
        if 'href' in href.attrib:
            val = href.attrib['href']
            if val.find('javascript:') != -1:  # 過濾掉類似"javascript:void(0)"
                continue
            if val.startswith('http://') is False:  # 給"/mdd/calendar/"做拼接
                if val.startswith('/'):
                    val = 'http://cuiqingcai.com/{0}'.format(val)
                else:
                    continue
            if val[-1] == '/':  # 過濾掉末尾的/
                val = val[0:-1]
  • BloomFilter:過濾URL后入列
if hashlib.md5(val).hexdigest() not in self.download_bf:
    self.bak_queue.append(val)

結(jié)果展示

(占位---待繼續(xù)更新)

代碼不夠200行惧盹,這里附上所有代碼

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
# @Time    : 2017/6/9 19:23
# @Author  : Spareribs
# @File    : cuiqingcai_crawl.py
# @Notice  : 這是使用寬度優(yōu)先算法BSF實現(xiàn)的全站爬取的爬蟲


詳解1:我們已經(jīng)將md5和URL記錄到md5.txt和url.txt中乳幸,但是我們暫時不用url.txt,我們只需要將md5的值讀取到用于做判斷邏輯的BloomFilter對象當(dāng)中即可
"""

import os
import time
import urllib2
from collections import deque  # deque是為了高效實現(xiàn)插入和刪除操作的雙向列表
import httplib
import hashlib
from lxml import etree
from pybloom import BloomFilter

num_downloaded_pages = 0


class CuiQingCaiBSF():
    """
    這是使用寬度優(yōu)先算法BSF實現(xiàn)的全站爬取的爬蟲類钧椰,通過max_level來自定義抓取的深度
    """
    # 定義請求的頭部(目標(biāo)網(wǎng)站沒有做太多的安全措施粹断,所以原諒我)
    request_headers = {
        'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36",
    }

    # BSF寬度優(yōu)先算法的深度標(biāo)記
    now_level = 0  # 初始深度
    max_level = 2  # 爬取深度

    # 記錄文件(URL和計算得到的md5)
    dir_name = 'cuiqingcai/'
    if not os.path.exists(dir_name):  # 檢查用于存儲網(wǎng)頁文件夾是否存在,不存在則創(chuàng)建
        os.makedirs(dir_name)
    md5_file_name = dir_name + "md5.txt"  # 記錄已經(jīng)下載的md5的值
    url_file_name = dir_name + "url.txt"  # 記錄已經(jīng)下載的URL

    # 用于記錄爬取URL的隊列(先進(jìn)先出)
    now_queue = deque()  # 爬取隊列
    bak_queue = deque()  # 備用隊列(爬取隊列為空的時候置換)

    # 定義一個BloomFilter對象嫡霞,用于做URL去重使用
    download_bf = BloomFilter(1024 * 1024 * 16, 0.01)

    # 定義一個存放md5值的列表
    md5_lists = []

    def __init__(self, begin_url):
        """
        初始化處理瓶埋,主要是斷點續(xù)傳的邏輯
        """
        self.root_url = begin_url  # 將初始的URL傳入
        self.now_queue.append(begin_url)  # 將首個URL加入爬取隊列now_queue
        self.url_file = open(self.url_file_name, 'a+')  # 將首個url寫入url記錄文件

        # 用于處理斷點續(xù)傳邏輯(詳細(xì)請看-->詳解一)
        try:
            self.md5_file = open(self.md5_file_name, 'r')  # 只讀方式打開md5的文件
            self.md5_lists = self.md5_file.readlines()  # 將文件的內(nèi)容以列表的方式讀取出來
            self.md5_file.close()  # 關(guān)閉文件
            for md5_item in self.md5_lists:  # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
                self.download_bf.add(md5_item[:-2])  # 將去掉回車換行符的md5寫入BloomFilter對象當(dāng)中
        except IOError:
            print "【Error】{0} - File not found".format(self.md5_file_name)
        finally:
            self.md5_file = open(self.md5_file_name, 'a+')  # 增加編輯方式打開md5的文件

    # def enqueueUrl(self, url):
    #     self.bak_queue.append(url) # 將獲取到的url加入到備用隊列當(dāng)中

    def getQueneURL(self):
        """
        爬取隊列為空的時候,將備用隊列置換到爬取隊列
        """
        try:
            url = self.now_queue.popleft()  # 從左邊進(jìn)行獲取隊列內(nèi)容
            return url
        except IndexError:
            self.now_level += 1  # 深度加一
            if self.now_level == self.max_level:  # 如果深度與設(shè)定的最大深度相等秒际,停止爬蟲返回None
                return None
            if len(self.bak_queue) == 0:  # 如果備用隊列長度為0悬赏,停止爬蟲返回None
                return None
            self.now_queue = self.bak_queue  # 將備用隊列傳遞給爬取隊列
            self.bak_queue = deque()  # 重置備用隊列
            return self.getQueneURL()  # 繼續(xù)執(zhí)行dequeuUrl方法,直到獲取到URL或者None

    def getPageContent(self, now_url):
        """
        下載當(dāng)前爬取頁面娄徊,
        """
        global filename, num_downloaded_pages
        print "【Download】正在下載網(wǎng)址 {0} 當(dāng)前深度為{1}".format(now_url, self.now_level)
        try:
            # 使用urllib庫請求now_url地址闽颇,將頁面通過read方法讀取下來
            req = urllib2.Request(now_url, headers=self.request_headers)
            response = urllib2.urlopen(req)
            html_page = response.read()

            filename = now_url[7:].replace('/', '_')  # 處理URL信息,去掉"http://"寄锐,將/替換成_

            # 將獲取到的頁面寫入到文件中
            fo = open("{0}{1}.html".format(self.dir_name, filename), 'wb+')
            fo.write(html_page)
            fo.close()



        # 處理各種異常情況
        except urllib2.HTTPError, Arguments:
            print "【Error】:{0}\n".format(Arguments)
            return
        except httplib.BadStatusLine:
            print "【Error】:{0}\n".format('BadStatusLine')
            return
        except IOError:
            print "【Error】:IOError {0}\n".format(filename)
            return
        except Exception, Arguments:
            print "【Error】:{0}\n".format(Arguments)
            return

        # 計算md5的值并將md5和寫入到文件中
        dumd5 = hashlib.md5(now_url).hexdigest()  # 生成md5值
        self.md5_lists.append(dumd5)  # 將md5加入到md5的列表中
        self.md5_file.write(dumd5 + '\r\n')  # 將md5寫入文件
        self.url_file.write(now_url + '\r\n')  # 將url寫入文件
        self.download_bf.add(dumd5)  # 將md5加入到BloomFilter對象當(dāng)中
        num_downloaded_pages += 1  # 用于統(tǒng)計當(dāng)前下載頁面的總數(shù)

        # 解析頁面兵多,獲取當(dāng)前頁面中所有的URL
        try:
            html = etree.HTML(html_page.lower().decode('utf-8'))
            hrefs = html.xpath(u"http://a")
            for href in hrefs:
                # 用于處理xpath抓取到的href尖啡,獲取有用的
                try:
                    if 'href' in href.attrib:
                        val = href.attrib['href']
                        if val.find('javascript:') != -1:  # 過濾掉類似"javascript:void(0)"
                            continue
                        if val.startswith('http://') is False:  # 給"/mdd/calendar/"做拼接
                            if val.startswith('/'):
                                val = 'http://cuiqingcai.com/{0}'.format(val)
                            else:
                                continue
                        if val[-1] == '/':  # 過濾掉末尾的/
                            val = val[0:-1]
                        # 判斷如果這個URL沒有在BloomFilter中就加入BloomFilter的隊列
                        if hashlib.md5(val).hexdigest() not in self.download_bf:
                            self.bak_queue.append(val)
                        else:
                            print '【Skip】已經(jīng)爬取 {0} 跳過'.format(val)
                except ValueError:
                    continue
        except UnicodeDecodeError:  # 處理utf-8編碼無法解析的異常情況
            pass

    def start_crawl(self):
        """
        啟動腳本的主程序
        """

        while True:
            # time.sleep(10)
            url = self.getQueneURL()
            if url is None:
                break
            self.getPageContent(url)
            print "爬取隊列剩余URL數(shù)量為:{0},備用隊列剩余URL數(shù)量為:{1}".format(len(self.now_queue), len(self.bak_queue))

        # 最后關(guān)閉打開的md5和rul文件
        self.md5_file.close()
        self.url_file.close()


if __name__ == "__main__":
    print '【Begin】---------------------------------------------------------------'
    start_time = time.time()
    CuiQingCaiBSF("http://cuiqingcai.com/").start_crawl()
    print '【End】下載了 {0} 個頁面剩膘,花費時間 {1:.2f} 秒'.format(num_downloaded_pages, time.time() - start_time)

以上都是我的個人觀點衅斩,如果有不對,或者有更好的方法怠褐,歡迎留言指正~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末畏梆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子奈懒,更是在濱河造成了極大的恐慌奠涌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磷杏,死亡現(xiàn)場離奇詭異溜畅,居然都是意外死亡,警方通過查閱死者的電腦和手機极祸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門慈格,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遥金,你說我怎么就攤上這事浴捆。” “怎么了稿械?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵汤功,是天一觀的道長。 經(jīng)常有香客問我溜哮,道長滔金,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任茂嗓,我火速辦了婚禮餐茵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘述吸。我一直安慰自己忿族,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布蝌矛。 她就那樣靜靜地躺著道批,像睡著了一般。 火紅的嫁衣襯著肌膚如雪入撒。 梳的紋絲不亂的頭發(fā)上隆豹,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音茅逮,去河邊找鬼璃赡。 笑死判哥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碉考。 我是一名探鬼主播塌计,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侯谁!你這毒婦竟也來了锌仅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤墙贱,失蹤者是張志新(化名)和其女友劉穎技扼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嫩痰,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年窍箍,在試婚紗的時候發(fā)現(xiàn)自己被綠了串纺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡椰棘,死狀恐怖纺棺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邪狞,我是刑警寧澤祷蝌,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站帆卓,受9級特大地震影響巨朦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜剑令,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一糊啡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吁津,春花似錦棚蓄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至典尾,卻和暖如春役拴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钾埂。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工扎狱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侧到,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓淤击,卻偏偏與公主長得像匠抗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子污抬,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

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