1. 程式人生 > >Python爬蟲:抓取內涵段子1000張搞笑圖片-上篇(小爬蟲誕生篇)

Python爬蟲:抓取內涵段子1000張搞笑圖片-上篇(小爬蟲誕生篇)

  出於興趣,在《幕課網:Python 開發簡單爬蟲》上學習了點兒 Python 爬蟲的入門知識,跟著視訊教程抓取了百度百科的 1000 個頁面。然後自己嘗試抓取一個國外網站的資料,但可能是由於最近召開武林大會的緣故,爬蟲寫到一半發現牆翻不過去了,聽說連付費的 vpn 都被封了,很是鬱悶。所以還是老老實實的拿國內網站練練手吧,作為一名段友,自然忘不了《內涵段子》,現在和大家分享一下我的思路、優化過程及爬蟲程式碼。

  我們的目標是:爬取 1000 張內涵段子的搞笑圖片,且圖片需點贊數超過 10000 次。

一、分析網頁原始碼

  爬蟲歸根到底就是程式,而程式就喜歡處理有規律的東西。寫爬蟲最重要的就是分析網頁的 html 結構,並從中分析出特定規律。

我們首先在瀏覽器中按 F12 來分析一下《內涵段子》主介面的html結構

  我使用的是搜狗瀏覽器,按 F12 彈出開發人員工具,左側顯示的是源網頁,右側顯示的是網頁 html。如圖所示,我所關心的就三部分:1、網址;2、圖片 url;3、點贊數。下面以“圖片 url”為例查詢其對應的 html:在源網頁中將滑鼠移動到圖片上,然後點選滑鼠右鍵,選擇“審查元素”,這時候右側的 html 就會自動跳轉到對應的 html 標籤。如下圖所示,圖片所在的標籤就是 img,裡面包含了圖片的 url 地址、是否為 gif、圖片描述等資訊。

  右側顯示的這些 html 標籤都是可編輯的,在標籤上滑鼠右鍵,選擇“Edit attribute”即可,如下圖所示。

  img 標籤及內容如下,這裡的“data-src”就是我們想要的!

<img id="groupImage" class="upload-img" data-src="http://p3.pstatp.com/large/3ebc0002c50f14a1959b" data-image-info="{&quot;is_gif&quot;:&quot;1&quot;,&quot;l&quot;:{&quot;w&quot;:&quot;480&quot;,&quot;h&quot;:&quot;272&quot;,&quot;s&quot;:&quot;http://p3.pstatp.com/large/3ebc0002c50f14a1959b&quot;,&quot;s1&quot;:&quot;http://pb9.pstatp.com/large/3ebc0002c50f14a1959b&quot;},&quot;m&quot;:{&quot;w&quot;:&quot;202&quot;,&quot;h&quot;:&quot;114&quot;,&quot;s&quot;:&quot;http://p3.pstatp.com/w202/3ebc0002c50f14a1959b&quot;,&quot;s1&quot;:&quot;http://pb9.pstatp.com/w202/3ebc0002c50f14a1959b&quot;}}"
onerror="this.src='http://p3.pstatp.com/w202/3ebc0002c50f14a1959b'" alt="這個是什麼東西,某寶有賣嗎?" style="width: 480px; height: 272px; visibility: visible;" src="http://p3.pstatp.com/large/3ebc0002c50f14a1959b">

  使用同樣的方法分析“點贊數”,可發現其所在的標籤及內容如下,這裡的“21017”就是我們想要的!

<span class="digg">21017</span>

  好了,現在我們找到了到“圖片 url”和“點贊數”,可是《內涵段子》預設只顯示 20 條記錄,也就是隻顯示 20 張圖片。要想獲取更多圖片,需要在頁面底端點選“載入更多”按鈕,可是我通過“審查元素”發現這只是一個普通的 div 標籤。

<div class="loading-more" id="loadMore">載入更多</div>

  可以斷定,這應該是通過 Ajax 等技術實現的網頁,點選“載入更多”非同步請求資料並重新整理網頁。但就我目前的水平而言,用爬蟲模擬點選這個按鈕是有困難的。幸運的是,我發現頁面重新整理之後,《內涵段子》顯示的圖片就會有變化,這樣的話我就可以通過多次重新抓取網頁從而達到多次重新整理內容的目的,N 次抓取、N 次重新整理。ok,現在就開始寫爬蟲程式碼了!

二、網頁下載器(Downloader)

  爬蟲在爬取頁面時,首先要將網頁下載下來,先暫存在記憶體中,因此首要任務就是實現網頁下載器(Downloader類)。訪問網頁我使用的是 urllib 庫。上程式碼:

# coding:utf8
import urllib.request
import random

class Downloader(object):

    def download_(self, url):
        if url is None:
            return

        agents = ['Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36','Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5', 'Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7', 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14', 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14', 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1']

        req = urllib.request.Request(url)
        req.add_header('User-Agent', random.choice(agents))
        response = urllib.request.urlopen(req, timeout=60)
        binary_data = response.read()

        if response.getcode() != 200:
            print('訪問失敗;', response.getcode())
            response.close()
            return None

        response.close() 
        return binary_data

  需要注意的是:(1) 我在程式碼中定義了一個數組 (agents),存放了七八個瀏覽器標識,然後在訪問網頁時用random.choice() 函式隨機取出一個標識,偽裝成特定的瀏覽器,可在一定程度上避免爬蟲被發現。(2) 我設定了 60 秒的超時時間,避免爬蟲長時間訪問網頁。

三、Html解析器(HtmlParser)

  通過網頁下載器我們已經順利把網頁下載下來了,下面的任務就是解析Html,提取我們需要的“圖片 url”、“點贊數”等資訊。

  Html解析器主要幹兩件事:(1) 解析 Html,獲取“圖片 url”和“點贊數”;(2) 進行邏輯判斷,排除“點贊數”低於一萬的圖片。上程式碼:

# coding:utf8
from bs4 import BeautifulSoup
import os

class HtmlParser(object):
    def __init__(self):
        self.rootpath = 'joke_essay'

    def get_filename_by_url(self, filename):
        invalid_chars = [':', '/', '\\', '*', '?', '<', '>', '|']
        for char in invalid_chars:
            while char in filename:
                filename = filename.replace(char, '')

        return filename

    def _get_new_imgs(self, soup):
        new_imgs = []
        # 獲取每個item標籤
        detail_wrappers = soup.find_all('div', class_='detail-wrapper')
        for detail_wrapper in detail_wrappers:
            # 獲取圖片url
            image = detail_wrapper.find('img', id='groupImage', class_='upload-img')
            # 獲取點贊數
            digg = detail_wrapper.find('span', class_='digg')
            if int(digg.get_text()) > 10000: # 點贊數需超過一萬
                img_url = image.get('data-src')
                filename = self.get_filename_by_url(img_url)
                if os.path.exists(self.rootpath + '\\' + filename + '.png'):
                    continue
                else:
                    new_imgs.append(img_url)

        return new_imgs 


    def parse(self, html_cont):
        if html_cont is None:
            return

        soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf-8')

        new_imgs = self._get_new_imgs(soup)

        return new_imgs

  我解析 Html 用的是 BeautifulSoup 庫,這個是比較常用的解析庫,標準用的是“html.parser”。值得注意的是,採用重新整理頁面的方式,得到的圖片有些是重複的,可以使用“圖片 url”來作為圖片的身份證,即如果判斷圖片具有相同的 url 那麼就可以認定圖片重複了,也就無需下載了。我直接將“圖片 url”作為新圖片檔案的檔名進行儲存,這樣在下載新圖片時只需要判斷目錄下是否有相同檔名的圖片,如果存在,那麼就不用再次下載了。這樣做的好處是即使程式中途崩掉,下次執行時重複的圖片仍然不會被下載,豈不妙哉!但是“圖片 url”中有些字元是檔名不允許使用的,比如*、/、?等,因此需要過濾掉非法字元,即使用 get_filename_by_url() 方法。最後將解析得到的“圖片 url”儲存到陣列 new_imgs 中。

四、檔案輸出器(FileOutputer)

  獲取到“圖片 url”陣列之後就可以進行圖片下載了,將圖片儲存到本地檔案系統中。上程式碼:

# coding:utf8
import os
from os import path
import downloader
import traceback

class FileOutputer(object):
    def __init__(self):
        self.downloader = downloader.Downloader()
        self.rootpath = 'joke_essay'
        if not path.exists(self.rootpath):
            os.mkdir(self.rootpath)


    def get_filename_by_url(self, filename):
        invalid_chars = [':', '/', '\\', '*', '?', '<', '>', '|']
        for char in invalid_chars:
            while char in filename:
                filename = filename.replace(char, '')

        return filename


    def output_file(self, new_imgs):
        successCount = 0;
        for img_url in new_imgs:
            try:
                binary_data = self.downloader.download_(img_url)
                filename = self.get_filename_by_url(img_url)
                fout = open(self.rootpath + '\\' + filename + '.png', 'wb')
                fout.write(binary_data)
                fout.close()
                successCount += 1
                print('圖片下載成功:%s' % img_url)
            except:
                traceback.print_exc()
                continue

        return successCount

  這裡我還是使用了網頁下載器中的 download_() 方法,通過相同的方式將圖片二進位制資料下載下來,然後儲存到本地。

五、小爬蟲誕生(Spider)

  先看專案結構(基於eclipse+pydev+python3.5):

  圖中的 spider_main 就是主程式,上程式碼:

# coding:utf8
import datetime
import downloader, html_parser, file_outputer

class Spider():
    def __init__(self):
        self.downloader = downloader.Downloader()
        self.outputer = file_outputer.FileOutputer()
        self.parser = html_parser.HtmlParser()

    def craw(self):
        visitCount = 0;
        sumCount = 0;
        while sumCount < 1000:
            html_cont = self.downloader.download_(start_url)
            visitCount += 1
            print('正在抓取網站:%s,總共已訪問%d次' % (start_url, visitCount))
            new_imgs = self.parser.parse(html_cont)
            print('此次抓取到 %d 張圖片,總共已抓取 %d 張' % (len(new_imgs), sumCount))
            sucCount = self.outputer.output_file(new_imgs)
            sumCount += sucCount

        return sumCount


if __name__ == '__main__':
    starttime = datetime.datetime.now()
    start_url = 'http://neihanshequ.com/pic/'
    spider = Spider()
    sumCount = spider.craw()

    endtime = datetime.datetime.now()
    print('==========  抓取完成 ,總共抓取%d張圖片,耗時:%s 秒 ==========' % (sumCount, str((endtime - starttime).seconds)))

  整個流程是這樣滴:(1) 記錄一下當前已下載圖片的個數,當不足1000張時,就 download_() 一下網址:http://neihanshequ.com/pic/,等同於重新整理一下,下載最新的網頁 html;(2) 用 parse() 解析一下 html,獲取“圖片 url”、“點贊數”標籤,進行邏輯判斷,對圖片進行篩選,將符合條件的“圖片 url”儲存在陣列中,作為引數返回;(3) 輸出圖片到本地。

  接下來就執行我們寫好的爬蟲,靜靜的等待著爬蟲想小蜜蜂一樣來來回回的忙碌吧。

  可以看到爬蟲已經開始幹活了,下面是下載的一些圖片。

  waiting……,大約運行了半小時,只下載好了 100 多張圖片,看來離我們的目標還是有點兒遠啊,這可不行。究其原因是通過多次爬取相同網址(http://neihanshequ.com/pic/)的方式存在大量的重複圖片,越到後來幾乎都爬不出沒下載過的圖片了,有點兒像挖比特幣,越來越難,哈哈。看來是要逼我祭出大招了,請看下篇:Python爬蟲:抓取內涵段子1000張搞笑圖片-下篇(小爬蟲優化篇)