爬蟲筆記(11)性能問題

1.

過年也沒啥事干,繼續(xù)搗鼓爬蟲土铺。開始是準備爬豆瓣電影的胶滋,豆瓣存在一些反爬機制,爬一會就爬不動了悲敷。當然后面是突破了這個限制究恤,使用隨機bid,設置cookie后德。據說會出現驗證碼部宿,我爬了幾萬部電影也沒有出現這個問題。初期的想法是使用代理ip瓢湃,網絡上的免費代理ip大都不靠譜理张,懶得搗鼓。
在豆瓣電影這個爬蟲中绵患,我其實是使用兩個步驟來執(zhí)行涯穷。第一部分是按照年標簽查找電影,從1900到2017年藏雏,將每個電影鏈接存入到mongodb中,然后進行去重(手動)作煌。

class DoubanMovies(scrapy.Spider):
    name = "douban"
    prefix = 'https://movie.douban.com/tag/'
    start_urls = []
    for i in range(1900,2018):
        start_urls.append(prefix+str(i))

    def parse(self,response):
        names = response.xpath('//div[@class="pl2"]/a/text()').extract()
        names = [name.strip('\n/ ') for name in names]
        names = [name for name in names if len(name)>0] #去掉空名字
        movie_urls = response.xpath('//div[@class="pl2"]/a/@href').extract()
        hrefs = response.xpath('//div[@class="paginator"]/a/@href').extract()#獲取分頁鏈接
        for href in hrefs:
           yield scrapy.Request(href, callback=self.parse)
        for i in range(len(names)):
            yield {'name':names[i],'url':movie_urls[i]}

關于mongodb去重的問題掘殴,我使用的臨時表。主要是我對mongodb確實不熟悉粟誓,而且我對JavaScript這樣的語法著實不感冒奏寨。下面的代碼很簡單,每個電影鏈接就是https://movie.douban.com/subject/26685451/ 鹰服,我這里特地把這中間的數字提取出來病瞳,然后進行對比,這個肯定是唯一的悲酷。distinct會把獲取的數據重復的進行合并套菜,這在sql中也有這功能。

nums = movies.distinct("number")#我把鏈接中的數字提取了出來
m = db.movies1
for num in mums:
  movie = movies.find_one({"number":num})
  m.insert_one(movie)
moives.drop()#刪除原來的數據表
m.rename('movie')#把新表命名

還有個問題就是針對douban的時間限制设易,需要使用DOWNLOAD_DELAY設置時間間隔逗柴,當然我使用bid突破了這個限制。
下面是第二個爬蟲的代碼顿肺,這個代碼就是訪問每個電影頁面提取相關數據戏溺,然后存到mongodb中渣蜗。數據提取沒什么難度,為了快速判斷xpath有效旷祸,最好開一個scrapy shell進行測試耕拷。

class DoubanMovies(scrapy.Spider):
    name = "doubansubject"
    def start_requests(self):
        MONGO_URI = 'mongodb://localhost/'
        client = MongoClient(MONGO_URI)
        db = client.douban
        movie = db.movie
        cursor = movie.find({})
        urls = [c['url'] for c in cursor]
        for url in urls:
            bid = "".join(random.sample(string.ascii_letters + string.digits, 11))
            yield scrapy.Request(url,callback=self.parse,cookies={"bid":bid})
def parse(self,response):

    title = response.xpath('//span[@property="v:itemreviewed"]/text()').extract_first()
    year = response.xpath('//span[@class="year"]/text()').extract_first()#(2016)
    pattern_y = re.compile(r'[0-9]+')
    year = pattern_y.findall(year)
    if len(year)>0:
        year = year[0]
    else:
        year = ""

    directors = response.xpath('//a[@rel="v:directedBy"]/text()').extract()#導演?有沒有可能有多個導演呢
    '''
    評分人數
    '''
    votes= response.xpath('//span[@property="v:votes"]/text()').extract_first()#評分人數
    '''
    分數
    '''
    score = response.xpath('//strong[@property="v:average"]/text()').extract_first()#抓取分數
    #編劇不好找等會弄
    '''
    演員
    '''
    actors = response.xpath('//a[@rel="v:starring"]/text()').extract()#演員
    genres = response.xpath('//span[@property="v:genre"]/text()').extract()#電影類型
    html = response.body.decode('utf-8')
    pattern_zp = re.compile(r'<span class="pl">制片國家/地區(qū):</span>(.*)<br/>')
    nations = pattern_zp.findall(html)
    if len(nations)>0 :
        nations = nations[0]
        nations = nations.split('/')
        nations = [n.strip() for n in nations]
    '''
    多個國家之間以/分開托享,前后可能出現空格也要刪除
    '''
    pattern_bj = re.compile(r"<span ><span class='pl'>編劇</span>: <span class='attrs'>(.*)</span></span><br/>")
    bj_as = pattern_bj.findall(html)
    '''
    bj_as 內容是
    [<a>編劇</a>,<a></a>,<a></a>,<a></a>,]
    需要進一步提取
    '''
    p = re.compile(r'>(.*)<')
    bj = [p.findall(bj) for bj in bj_as]
    '''
    p.findall也會產生數組骚烧,需要去掉括號,只有有數據才能去掉
    '''
    bj = [b[0].strip() for b in bj if len(b)>0]#編劇的最終結果

    '''
    語言
    <span class="pl">語言:</span> 英語 / 捷克語 / 烏克蘭語 / 法語<br/>
    '''
    pattern_lang = re.compile(r'<span class="pl">語言:</span>(.*)<br/>')
    langs = pattern_lang.findall(html)
    if len(langs)>0:
        langs = langs[0]
        langs = langs.split('/')
        langs = [l.strip() for l in langs]
    runtime = response.xpath('//span[@property="v:runtime"]/@content').extract_first()
    '''
    上映日期也有多個
    '''
    releasedates = response.xpath('//span[@property="v:initialReleaseDate"]/text()').extract()
    '''
    標簽
    '''
    tags = response.xpath('//div[@class="tags-body"]/a/text()').extract()

    ##這里不能用return
    yield {"title":title,"year":year,"directors":directors,"score":score,"votes":votes,
    "actors":actors,"genres":genres,"nations":nations,"bj":bj,"langs":langs,"runtime":runtime,
    "releasedates":releasedates,"url":response.url
    }

上面的代碼確實能正常工作嫌吠,但是有個缺點就是太慢止潘,不到五萬個頁面就要幾個小時,顯然瓶頸在分析這一塊辫诅。性能問題會在下面一個例子中討論凭戴。

2

性能問題確實是個大問題,在滿足能爬取的情況下炕矮,速度要優(yōu)化么夫。這幾天抓取一個AV網站,沒錯AV網站的種子文件肤视。先抓取文章列表档痪,再抓取每個詳細頁面,訪問種子下載頁面邢滑,最后下載里面的種子文件腐螟。

  • 方法一
    這個代碼很簡單使用的是requests來下載文件,里面的下載功能代碼就是從requests教程中拷貝出來的困后。
def process_item(self, item, spider):
        try:
            bt_urls = item['bt_urls']
            if not os.path.exists(self.dir_path):
                os.makedirs(self.dir_path)
            '''
            檢查文件夾是否存在這段代碼應該放到open_spider中去才是合適的乐纸,啟動檢查一下后面就不管了
            '''
            for url in bt_urls:
                response = requests.get(url,stream=True)
                attachment = response.headers['Content-Disposition']
                pattern = re.compile(r'filename="(.+)"')
                filename = pattern.findall(attachment)[0]
                filepath = '%s/%s' % (self.dir_path,filename)
                with open(filepath, 'wb') as handle:
                    #response = requests.get(image_url, stream=True)
                    for block in response.iter_content(1024):
                        if not block:
                            break
                        handle.write(block)
                '''
                整個代碼肯定會嚴重影響爬蟲的運行速度,是否考慮多進程方法
                '''
        except Exception as e:
            print(e)
        return item

bt_url種子的鏈接就放在bt_urls摇予,由于是從下載頁面返回的item汽绢,實際中最多只有一個鏈接。這個代碼運行起來沒什么問題侧戴,但是速度相當慢宁昭。scrapy使用的是異步網絡框架,但是requests是實實在在的同步方法酗宋,單線程的情況下必然影響到整個系統(tǒng)的執(zhí)行积仗。必須要突破這個瓶頸,實際中要先考慮代碼能正確運行再考慮其它方面蜕猫。

  • 方法二
    既然在本線程中直接下載會造成線程阻塞斥扛,那開啟一個新的進程如何。
class DownloadBittorrent2(object):

    def __init__(self, dir_path):
        self.dir_path = dir_path
        # self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            dir_path =crawler.settings.get('DIR_PATH'),
        )

    def open_spider(self, spider):
        if not os.path.exists(self.dir_path):
            os.makedirs(self.dir_path)

    def close_spider(self, spider):
        pass

    def downloadprocess(self,url):
        try:
            response = requests.get(url,stream=True)
            attachment = response.headers['Content-Disposition']
            pattern = re.compile(r'filename="(.+)"')
            filename = pattern.findall(str(attachment))[0]#這里attachment是bytes必須要轉化
            filepath = '%s/%s' % (self.dir_path,filename)
            with open(filepath, 'wb') as handle:
                #response = requests.get(image_url, stream=True)
                for block in response.iter_content(1024):
                    if not block:
                        break
                    handle.write(block)
        except Exception as e:
            print(e)
    def process_item(self, item, spider):

        bt_urls = item['bt_urls']
        if len(bt_urls)>0:#最多只有一個url
            p = Process(target=self.downloadprocess,args=(bt_urls[0],))
            p.start()

        return item

這個代碼也能正常工作,但是報錯稀颁,直接導致服務器掛了芬失。
HTTPConnectionPool(host='taohuabbs.info', port=80): Max retries exceeded with url: /forum.php?mod=attachment&aid=MjAxNzk 0fDA0OTkxZjM0fDE0ODU4NjY0OTZ8MHwxMzMzNDA= (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConne ction object at 0x00000000041F9048>: Failed to establish a new connection: [WinError 10060]由于連接方在一段時間后沒有正確答復或連接的主機沒有反應,連接嘗試失敗匾灶。',))
這個可能跟設置的延遲有關系(我就沒有設置延遲)棱烂,反正就是把服務器弄死了。還有就是requests在這種異常情況下容錯能力有問題阶女。

  • 方法三
    既然scrapy自帶了一個Filespipeline颊糜,那么是不是可以考慮用這個來下載呢!可以試試秃踩!
class DownloadBittorrent3(FilesPipeline):
    def get_media_requests(self, item, info):
        for file_url in item['bt_urls']:
            yield scrapy.Request(file_url)

代碼報錯了衬鱼,原因是文件名打不開。這個就涉及到如何命名下載文件名的問題憔杨。如果鏈接中帶*.jpg這樣類似的名字鸟赫,程序不會有問題,如果不是會怎么樣消别,鏈接中可能出現操作系統(tǒng)不允許在文件名中出現的字符抛蚤,這就會報錯。我對系統(tǒng)自帶的這個pipeline了解甚少寻狂,就沒有繼續(xù)研究岁经。
還有一點我希望文件名來自于服務器的反饋,對于下載文件服務器可能會把文件名發(fā)過來蛇券,這個就在headers的Content-Disposition字段中缀壤。也就是是說我必須要先訪問網絡之后才能確定文件名。

  • 方法四
    前面我們都使用了pipeline來處理纠亚,實際上我們完全可以不用pipeline而直接在spider中處理诉位。
    def download(self,response):
        '''
        在爬取過程中發(fā)現有可能返回不是torrent文件,這時候要考慮容錯性問題菜枷,雖然爬蟲并不會掛掉
        '''
        attachment = response.headers['Content-Disposition']
        pattern = re.compile(r'filename="(.+)"')
        filename = pattern.findall(attachment.decode('utf-8'))[0]
        filepath = '%s/%s' % (self.settings['DIR_PATH'],filename)
        with open(filepath, 'wb') as handle:
            handle.write(response.body)

這種方法性能不錯,對比前面50/min速度叁丧,這個可以達到100/min啤誊。其實我們可以更快。

3

在實際的下載中拥娄,我們要充分利用scrapy的網絡下載框架蚊锹,這個性能好容錯性高,而且也好排錯稚瘾。上面的10060錯誤牡昆,我估計放在http中可能就是503(服務器無法到達)。
前面的方法都在單線程中運作,雖然后面有多進程版的下載代碼丢烘,由于沒有scrapy穩(wěn)定所以我考慮用多個爬蟲來實現柱宦。如果啟動兩個scrapy爬蟲,一個負責爬頁面播瞳,一個負責下載掸刊,這樣的效率應該會高不少。雖然前面的筆記中有提到相關代碼赢乓,使用redis來實現分布式忧侧。當然在單機上稱不上分布式,但是使用redis作為進程間通訊手段確實極好的牌芋,不管是多進程還是分布式都能非常高效的工作蚓炬。github上有基于redis版本的scrapy,這里我的想法是第一個爬蟲負責爬頁面的屬于一般爬蟲(使用原版的scrapy)躺屁,而第二個爬蟲使用基于redis的爬蟲肯夏。

  • 1 scrapy-redis安裝
    pip install scrapy-redis
    安裝方法倒是很簡單,但是這個代碼比較舊了楼咳,版本是0.6.3熄捍,這個版本在python3.5上工作不正常(出錯跟轉碼有關str,具體情況不懂)母怜,處理的辦法就是把0.6.7的代碼下載下來直接覆蓋就可以了(反正我也看不懂代碼余耽,覆蓋了能工作)。
  • 2 配置
    scrapy-redis的配置還是在settings中苹熏,參考文檔
    文檔中有幾個必須配置的參數:
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    后面還可以配置redis服務器端口號碟贾,還有redis服務器地址。
    REDIS_START_URLS_BATCH_SIZE
    上面的參數對代表每次redis從redis服務器中獲取的鏈接數量轨域,這個調高可能會增加性能袱耽。
  • 3 頁面爬蟲
class BtSpiderEx(scrapy.Spider):
    name = 'btspiderex'
    start_urls = ['http://taohuabbs.info/forum-181-1.html']
    def parse(self,response):
        urls = response.xpath('//a[@onclick="atarget(this)"]/@href').extract()
        for url in urls:
            yield scrapy.Request(response.urljoin(url),callback=self.parsedetail)

        page_urls = response.xpath('//div[@class="pg"]/a/@href').extract()
        for url in page_urls:
            yield scrapy.Request(response.urljoin(url),callback=self.parse)

    def parsedetail(self,response):
        hrefs = response.xpath('//p[@class="attnm"]/a/@href').extract()
        for h in hrefs:
            yield scrapy.Request(response.urljoin(h),callback=self.parsedown)

    def parsedown(self,response):
        '''
        其實每次只能分析出一個bt鏈接
        '''
        bt_urls = response.xpath('//div[@style="padding-left:10px;"]/a/@href').extract()
        yield {'bt_urls':bt_urls}

頁面爬蟲代碼其實相對于前面的實現,變得更加簡單干发,這里把將下載鏈接推送到redis服務器的任務交給pipeline朱巨。

class DownloadBittorrent(object):
    def __init__(self, dir_path):
        self.dir_path = dir_path
    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            dir_path =crawler.settings.get('DIR_PATH'),
        )
    def open_spider(self, spider):
        if not os.path.exists(self.dir_path):
            os.makedirs(self.dir_path)
        self.conn = redis.Redis(port=6666)
    def close_spider(self,spdier):
        pass
    def process_item(self, item, spider):

        bt_urls = item['bt_urls']
        for url in bt_urls:
            self.conn.lpush('redisspider:start_urls',url)
        return item

open_spider在爬蟲啟動的時候啟動,這里就可以打開redis和建立下載文件夾枉长。redisspider:start_urls這個是redis隊列名冀续,缺省情況下scrapy-redis爬蟲的隊列就是爬蟲名+start_urls。

  • 4 下載爬蟲
    下載爬蟲只負責從redis獲取鏈接然后下載必峰。
from scrapy_redis.spiders import RedisSpider
import re
class DistributeSpider(RedisSpider):
    name = 'redisspider'
    def parse(self,response):
        DIR_PATH = "D:/bt"
        if 'Content-Disposition' in response.headers:
            attachment = response.headers['Content-Disposition']
            pattern = re.compile(r'filename="(.+)"')
            filename = pattern.findall(attachment.decode('utf-8'))[0]
            filepath = '%s/%s' % (DIR_PATH,filename)#DIR_PATH = "D:/bt"
            with open(filepath, 'wb') as handle:
                handle.write(response.body)

settings.py配置洪唐,只列出了主要參數,這里修改了默認端口號:

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_PORT = 6666
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末吼蚁,一起剝皮案震驚了整個濱河市凭需,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖粒蜈,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顺献,死亡現場離奇詭異,居然都是意外死亡薪伏,警方通過查閱死者的電腦和手機滚澜,發(fā)現死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫁怀,“玉大人设捐,你說我怎么就攤上這事√潦纾” “怎么了萝招?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長存捺。 經常有香客問我槐沼,道長,這世上最難降的妖魔是什么捌治? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任岗钩,我火速辦了婚禮,結果婚禮上肖油,老公的妹妹穿的比我還像新娘兼吓。我一直安慰自己,他們只是感情好森枪,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布视搏。 她就那樣靜靜地躺著,像睡著了一般县袱。 火紅的嫁衣襯著肌膚如雪浑娜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天式散,我揣著相機與錄音筋遭,去河邊找鬼。 笑死暴拄,一個胖子當著我的面吹牛漓滔,可吹牛的內容都是我干的。 我是一名探鬼主播揍移,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼反肋!你這毒婦竟也來了那伐?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罕邀,沒想到半個月后畅形,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡诉探,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年日熬,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肾胯。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡竖席,死狀恐怖,靈堂內的尸體忽然破棺而出敬肚,到底是詐尸還是另有隱情毕荐,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布艳馒,位于F島的核電站憎亚,受9級特大地震影響,放射性物質發(fā)生泄漏弄慰。R本人自食惡果不足惜第美,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陆爽。 院中可真熱鬧什往,春花似錦、人聲如沸墓陈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贡必。三九已至兔港,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仔拟,已是汗流浹背衫樊。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留利花,地道東北人科侈。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像炒事,于是被迫代替她去往敵國和親臀栈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容