教你如何快速實現一個圖片爬蟲

SylvanasSun發表於2017-09-22

什麼是爬蟲?


如果是沒有接觸過爬蟲的人可能會有些許疑惑,爬蟲是個什麼東西呢?其實爬蟲的概念很簡單,在網際網路時代,全球資訊網已然是大量資訊的載體,如何有效地利用並提取這些資訊是一個巨大的挑戰。當我們使用瀏覽器對某個網站傳送請求時,伺服器會響應HTML文字並由瀏覽器來進行渲染顯示。爬蟲正是利用了這一點,通過程式模擬使用者的請求,來獲得HTML的內容,並從中提取需要的資料和資訊。如果把網路想象成一張蜘蛛網,爬蟲程式則像是蜘蛛網上的蜘蛛,不斷地爬取資料與資訊。

爬蟲的概念非常簡單易懂,利用python內建的urllib庫都可以實現一個簡單的爬蟲,下面的程式碼是一個非常簡單的爬蟲,只要有基本的python知識應該都能看懂。它會收集一個頁面中的所有<a>標籤(沒有做任何規則判斷)中的連結,然後順著這些連結不斷地進行深度搜尋。

from bs4 import BeautifulSoup
import urllib
import os
from datetime import datetime

# 網頁的實體類,只含有兩個屬性,url和標題
class Page(object):
    def __init__(self,url,title):
        self._url = url
        self._title = title

    def __str__(self):
        return '[Url]: %s [Title]: %s' %(self._url,self._title)

    __repr__ = __str__

    @property
    def url(self):
        return self._url

    @property
    def title(self):
        return self._title

    @url.setter
    def url(self,value):
        if not isinstance(value,str):
            raise ValueError('url must be a string!')
        if value == '':
            raise ValueError('url must be not empty!')
        self._url = value

    @title.setter
    def title(self,value):
        if not isinstance(value,str):
            raise ValueError('title must be a string!')
        if value == '':
            raise ValueError('title must be not empty!')
        self._title = value

class Spider(object):

    def __init__(self,init_page):
        self._init_page = init_page # 種子網頁,也就是爬蟲的入口
        self._pages = []
        self._soup = None # BeautifulSoup 一個用來解析HTML的解析器

    def crawl(self):
        start_time = datetime.now()
        print('[Start Time]: %s' % start_time)
        start_timestamp = start_time.timestamp()
        tocrawl = [self._init_page] # 記錄將要爬取的網頁
        crawled = [] # 記錄已經爬取過的網頁
        # 不斷迴圈,直到將這張圖搜尋完畢
        while tocrawl:
            page = tocrawl.pop()
            if page not in crawled:
                self._init_soup(page)
                self._packaging_to_pages(page)
                links = self._extract_links()
                self._union_list(tocrawl,links)
                crawled.append(page)
        self._write_to_curdir()
        end_time = datetime.now()
        print('[End Time]: %s' % end_time)
        end_timestamp = end_time.timestamp()
        print('[Total Time Consuming]: %f.3s' % (start_timestamp - end_timestamp) / 1000)

    def _init_soup(self,page):
        page_content = None
        try:
            # urllib可以模擬使用者請求,獲得響應的HTML文字內容
            page_content = urllib.request.urlopen(page).read()
        except:
            page_content = ''
        # 初始化BeautifulSoup,引數二是使用到的解析器名字    
        self._soup = BeautifulSoup(page_content,'lxml')

    def _extract_links(self):
        a_tags = self._soup.find_all('a') # 找到所有a標籤
        links = []
        # 收集所有a標籤中的連結
        for a_tag in a_tags:
            links.append(a_tag.get('href'))
        return links

    def _packaging_to_pages(self,page):
        title_string = ''
        try:
            title_string = self._soup.title.string # 獲得title標籤中的文字內容
        except AttributeError as e :
            print(e)
        page_obj = Page(page,title_string)
        print(page_obj)
        self._pages.append(page_obj)

    # 將爬取到的所有資訊寫入到當前目錄下的out.txt檔案
    def _write_to_curdir(self):
        cur_path = os.path.join(os.path.abspath('.'),'out.txt')
        print('Start write to %s' % cur_path)
        with open(cur_path,'w') as f:
            f.write(self._pages)

    # 將dest中的不存在於src的元素合併到src
    def _union_list(self,src,dest):
        for dest_val in dest:
            if dest_val not in src:
                src.append(dest_val)

    @property
    def init_page(self):
        return self._init_page

    @property
    def pages(self):
        return self._pages


def test():
    spider = Spider('https://sylvanassun.github.io/')
    spider.crawl()

if __name__ == '__main__':
    test()複製程式碼

但是我們如果想要實現一個效能高效的爬蟲,那需要的複雜度也會增長,本文旨在快速實現,所以我們需要藉助他人實現的爬蟲框架來當做腳手架,在這之上來構建我們的圖片爬蟲(如果有時間的話當然也鼓勵自己造輪子啦)。

本文作者為: SylvanasSun(sylvanas.sun@gmail.com).轉載請務必將下面這段話置於文章開頭處(保留超連結).
本文首發自SylvanasSun Blog,原文連結: sylvanassun.github.io/2017/09/20/…

BeautifulSoup


BeautifulSoup是一個用於從HTMLXML中提取資料的python。Beautiful Soup自動將輸入文件轉換為Unicode編碼,輸出文件轉換為utf-8編碼。你不需要考慮編碼方式,除非文件沒有指定一個編碼方式,這時,Beautiful Soup就不能自動識別編碼方式了。然後,你僅僅需要說明一下原始編碼方式就可以了。

利用好BeautifulSoup可以為我們省去許多編寫正規表示式的時間,如果當你需要更精準地進行搜尋時,BeautifulSoup也支援使用正規表示式進行查詢。

BeautifulSoup3已經停止維護了,現在基本使用的都是BeautifulSoup4,安裝BeautifulSoup4很簡單,只需要執行以下的命令。

pip install beautifulsoup4複製程式碼

然後從bs4模組中匯入BeautifulSoup物件,並建立這個物件。

from bs4 import BeautifulSoup

soup = BeautifulSoup(body,'lxml')複製程式碼

建立BeautifulSoup物件需要傳入兩個引數,第一個是需要進行解析的HTML內容,第二個引數為解析器的名字(如果不傳入這個引數,BeautifulSoup會預設使用python內建的解析器html.parser)。BeautifulSoup支援多種解析器,有lxmlhtml5libhtml.parser

第三方解析器需要使用者自己安裝,本文中使用的是lxml解析器,安裝命令如下(它還需要先安裝C語言庫)。

pip install lxml複製程式碼

下面以一個例子演示使用BeautifulSoup的基本方式,如果還想了解更多可以去參考BeautifulSoup文件

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

soup = BeautifulSoup(html,'lxml')
# 格式化輸出soup中的內容
print(soup.prettify())

# 可以通過.操作符來訪問標籤物件
title = soup.title
print(title)
p = soup.p
print(p)

# 獲得title標籤中的文字內容,這2個方法得到的結果是一樣的
print(title.text)
print(title.get_text())


# 獲得head標籤的所有子節點,contents返回的是一個列表,children返回的是一個迭代器
head = soup.head
print(head.contents)
print(head.children)

# 獲得所有a標籤,並輸出每個a標籤href屬性中的內容
a_tags = soup.find_all('a')
for a_tag in a_tags:
    print(a_tag['href'])
# find函式與find_all一樣,只不過返回的是找到的第一個標籤    
print(soup.find('a')['href'])

# 根據屬性查詢,這2個方法得到的結果是一樣的
print(soup.find('p',class_='title'))
print(soup.find('p',attrs={'class': 'title'}))複製程式碼

Scrapy


Scrapy是一個功能強大的爬蟲框架,它已經實現了一個效能高效的爬蟲結構,並提供了很多供程式設計師自定義的配置。使用Scrapy只需要在它的規則上編寫我們的爬蟲邏輯即可。

首先需要先安裝Scrapy,執行命令pip install scrapy。然後再執行命令scrapy startproject 你的專案名來生成Scrapy的基本專案資料夾。生成的專案結構如下。

你的專案名/
    scrapy.cfg
    你的專案名/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...複製程式碼
  • scrapy.cfg : 專案的配置檔案。

  • items.py:物品模組,使用者需要在這個模組中定義資料封裝的實體類。

  • pipelines.py:管道模組,使用者需要在這個模組中定義處理資料的邏輯(如儲存到資料庫等)。

  • settings.py:這個模組定義了整個專案中的各種配置變數。

  • spiders/:在這個包中定義使用者自己的爬蟲模組。

啟動Scrapy的爬蟲也很簡單,只需要執行命令scrapy crawl 你的爬蟲名。下面介紹Scrapy中的關鍵模組的演示案例,如果想要了解有關Scrapy的更多資訊,請參考Scrapy官方文件

items


items模組主要是為了將爬取到的非結構化資料封裝到一個結構化物件中,自定義的item類必須繼承自scrapy.Item,且每個屬性都要賦值為scrapy.Field()

import scrapy

class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()複製程式碼

操作item物件就像操作一個dict物件一樣簡單。

product = Product()
# 對屬性賦值
product['name'] = 'Sylvanas'
product['price'] = 998
# 獲得屬性
print(product['name'])
print(product['price'])複製程式碼

pipelines


當一個Item經由爬蟲封裝之後將會到達Pipeline類,你可以定義自己的Pipeline類來決定將Item的處理策略。

每個Pipeline可以實現以下函式。

  • process_item(item, spider): 每個Pipeline都會呼叫此函式來處理Item,這個函式必須返回一個Item,如果在處理過程中遇見錯誤,可以丟擲DropItem異常。

  • open_spider(spider): 當spider開始時將會呼叫此函式,可以利用這個函式進行開啟檔案等操作。

  • close_spider(spider):當spider關閉時將會呼叫此函式,可以利用這個函式對IO資源進行關閉。

  • from_crawler(cls, crawler): 這個函式用於獲取settings.py模組中的屬性。注意這個函式是一個類方法。

from scrapy.exceptions import DropItem

class PricePipeline(object):

    vat_factor = 1.15

    def __init__(self, HELLO):
        self.HELLO = HELLO

    def process_item(self, item, spider):
        if item['price']:
            if item['price_excludes_vat']:
                item['price'] = item['price'] * self.vat_factor
            return item
        else:
            raise DropItem("Missing price in %s" % item)

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings # 從crawler中獲得settings
        return cls(settings['HELLO']) # 返回settings中的屬性,將由__init__函式接收複製程式碼

當定義完你的Pipeline後,還需要在settings.py中對你的Pipeline進行設定。

ITEM_PIPELINES = {
    # 後面跟的數字是優先順序別
    'pipeline類的全路徑': 300,
}複製程式碼

spiders


spiders模組中,使用者可以通過自定義Spider類來制定自己的爬蟲邏輯與資料封裝策略。每個Spider都必須繼承自class scrapy.spider.Spider,這是Scrapy中最簡單的爬蟲基類,它沒有什麼特殊功能,Scrapy也提供了其他功能不同的Spider類供使用者選擇,這裡就不多敘述了,可以去參考官方文件。

使用者可以通過以下屬性來自定義配置Spider:

  • name: 這是Spider的名稱,Scrapy需要通過這個屬性來定位Spider並啟動爬蟲,它是唯一且必需的。

  • allowed_domains: 這個屬性規定了Spider允許爬取的域名。

  • start_urlsSpider開始時將抓取的網頁列表。

  • start_requests(): 該函式是Spider開始抓取時啟動的函式,它只會被呼叫一次,有的網站必須要求使用者登入,可以使用這個函式先進行模擬登入。

  • make_requests_from_url(url): 該函式接收一個url並返回Request物件。除非重寫該函式,否則它會預設以parse(response)函式作為回撥函式,並啟用dont_filter引數(這個引數是用於過濾重複url的)。

  • parse(response): 當請求沒有設定回撥函式時,則會預設呼叫parse(response)

  • log(message[, level, component]): 用於記錄日誌。

  • closed(reason): 當Spider關閉時呼叫。

import scrapy


class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]

    def parse(self, response):
        self.log('A response from %s just arrived!' % response.url)複製程式碼

其他依賴庫


Requests


Requests也是一個第三方python庫,它比python內建的urllib更加簡單好用。只需要安裝(pip install requests),然後導包後,即可輕鬆對網站發起請求。

import requests

# 支援http的各種型別請求
r = requests.post("http://httpbin.org/post")
r = requests.put("http://httpbin.org/put")
r = requests.delete("http://httpbin.org/delete")
r = requests.head("http://httpbin.org/get")
r = requests.options("http://httpbin.org/get")

# 獲得響應內容
r.text # 返回文字
r.content # 返回位元組
r.raw # 返回原始內容
r.json() # 返回json複製程式碼

關於更多的引數與內容請參考Requests文件

BloomFilter


BloomFilter是一個用於過濾重複資料的資料結構,我們可以使用它來對重複的url進行過濾。本文使用的BloomFilter來自於python-bloomfilter,其他作業系統使用者請使用pip install pybloom命令安裝,windows使用者請使用pip install pybloom-live(原版對windows不友好)。

分析


介紹了需要的依賴庫之後,我們終於可以開始實現自己的圖片爬蟲了。我們的目標是爬https://www.deviantart.com/網站中的圖片,在寫爬蟲程式之前,還需要先分析一下頁面的HTML結構,這樣才能針對性地找到圖片的源地址。

為了保證爬到的圖片的質量,我決定從熱門頁面開始爬,連結為https://www.deviantart.com/whats-hot/

開啟瀏覽器的開發者工具後,可以發現每個圖片都是由一個a標籤組成,每個a標籤的classtorpedo-thumb-link,而這個a標籤的href正好就是這張圖片的詳情頁面(如果我們從這裡就開始爬圖片的話,那麼爬到的可都只是縮圖)。

進入到詳情頁後,不要馬上爬取當前圖片的源地址,因為當前頁顯示的圖片並不是原始格式,我們對圖片雙擊放大之後再使用開發者工具抓到這個圖片所在的img標籤後,再讓爬蟲獲取這個標籤中的源地址。

在獲得圖片的源地址之後,我的策略是讓爬蟲繼續爬取該頁中推薦的更多圖片,通過開發者工具,可以發現這些圖片都被封裝在一個classtt-crop thumbdiv標籤中,而該標籤裡的第一個a子標籤正好就是這個圖片的詳情頁連結。

初始配置


在對網頁的HTML進行分析之後,可以開始寫程式了,首先先用Scrapy的命令來初始化專案。之後在settings.py中做如下配置。

# 這個是網路爬蟲協議,爬蟲訪問網站時都會檢查是否有robots.txt檔案,
# 然後根據檔案中的內容選擇性地進行爬取,我們這裡設定為False即不檢查robots.txt
ROBOTSTXT_OBEY = False

# 圖片下載的根目錄路徑
IMAGES_STORE = '.'

# 圖片最大下載數量,當下載的圖片達到這個數字時,將會手動關閉爬蟲
MAXIMUM_IMAGE_NUMBER = 10000複製程式碼

然後定義我們的Item

import scrapy


class DeviantArtSpiderItem(scrapy.Item):
    author = scrapy.Field() # 作者名
    image_name = scrapy.Field() # 圖片名
    image_id = scrapy.Field() # 圖片id
    image_src = scrapy.Field() # 圖片的源地址複製程式碼

建立自己的spider模組與Spider類。

import requests
from bs4 import BeautifulSoup
# this import package is right,if PyCharm give out warning please ignore
from deviant_art_spider.items import DeviantArtSpiderItem
from pybloom_live import BloomFilter
from scrapy.contrib.linkextractors.lxmlhtml import LxmlLinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.http import Request

class DeviantArtImageSpider(CrawlSpider):
    name = 'deviant_art_image_spider'

    # 我不想讓scrapy幫助過濾所以設定為空
    allowed_domains = ''

    start_urls = ['https://www.deviantart.com/whats-hot/']

    rules = (
        Rule(LxmlLinkExtractor(
            allow={'https://www.deviantart.com/whats-hot/[\?\w+=\d+]*', }),
            callback='parse_page', # 設定回撥函式
            follow=True # 允許爬蟲不斷地跟隨連結進行爬取
        ),
    )

    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36"
                      " (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
        "Referer": "https://www.deviantart.com/"
    }

    # 初始化BloomFilter
    filter = BloomFilter(capacity=15000)複製程式碼

DeviantArtImageSpider繼承自CrawlSpider,該類是Scrapy最常用的Spider類,它通過Rule類來定義爬取連結的規則,上述程式碼中使用了正規表示式https://www.deviantart.com/whats-hot/[\?\w+=\d+]*,這個正規表示式將訪問每一頁的熱門頁面。

解析熱門頁面


爬蟲啟動時將會先訪問熱門頁面,請求得到響應之後會呼叫回撥函式,我們需要在這個回撥函式中獲取上述分析中得到的<a class = 'torpedo-thumb-link'>標籤,然後抽取出每張圖片的詳情頁連結。

    def parse_page(self, response):
        soup = self._init_soup(response, '[PREPARING PARSE PAGE]')
        if soup is None:
            return None
        # 找到所有class為torpedo-thumb-link的a標籤    
        all_a_tag = soup.find_all('a', class_='torpedo-thumb-link')
        if all_a_tag is not None and len(all_a_tag) > 0:
            for a_tag in all_a_tag:
                # 提取圖片詳情頁,然後對詳情頁連結發起請求,並設定回撥函式
                detail_link = a_tag['href']
                request = Request(
                    url=detail_link,
                    headers=self.headers,
                    callback=self.parse_detail_page
                )
                # 通過request與response物件來傳遞Item
                request.meta['item'] = DeviantArtSpiderItem()
                yield request
        else:
            self.logger.debug('[PARSE FAILED] get <a> tag failed')
            return None

    # 初始化BeautifulSoup物件
    def _init_soup(self, response, log):
        url = response.url
        self.headers['Referer'] = url
        self.logger.debug(log + ' ' + url)
        body = requests.get(url, headers=self.headers, timeout=2).content
        soup = BeautifulSoup(body, 'lxml')
        if soup is None:
            self.logger.debug('[PARSE FAILED] read %s body failed' % url)
            return None
        return soup複製程式碼

解析詳情頁


parse_page()函式會不斷地傳送請求到詳情頁連結,解析詳情頁的回撥函式需要處理資料封裝到Item,還需要提取詳情頁中更多圖片的詳情連結然後傳送請求。

    def parse_detail_page(self, response):
        if response.url in self.filter:
            self.logger.debug('[REPETITION] already parse url %s ' % response.url)
            return None
        soup = self._init_soup(response, '[PREPARING DETAIL PAGE]')
        if soup is None:
            return None
        # 包裝Item並返回    
        yield self.packing_item(response.meta['item'], soup)
        self.filter.add(response.url)
        # 繼續抓取當前頁中的其他圖片
        all_div_tag = soup.find_all('div', class_='tt-crop thumb')
        if all_div_tag is not None and len(all_div_tag) > 0:
            for div_tag in all_div_tag:
                detail_link = div_tag.find('a')['href']
                request = Request(
                    url=detail_link,
                    headers=self.headers,
                    callback=self.parse_detail_page
                )
                request.meta['item'] = DeviantArtSpiderItem()
                yield request
        else:
            self.logger.debug('[PARSE FAILED] get <div> tag failed')
            return None

    # 封裝資料到Item
    def packing_item(self, item, soup):
        self.logger.debug('[PREPARING PACKING ITEM]..........')
        img = soup.find('img', class_='dev-content-full')
        img_alt = img['alt'] # alt屬性中儲存了圖片名與作者名
        item['image_name'] = img_alt[:img_alt.find('by') - 1]
        item['author'] = img_alt[img_alt.find('by') + 2:]
        item['image_id'] = img['data-embed-id'] # data-embed-id屬性儲存了圖片id
        item['image_src'] = img['src']
        self.logger.debug('[PACKING ITEM FINISHED] %s ' % item)
        return item複製程式碼

處理Item


對於Item的處理,只是簡單地將圖片命名與下載到本地。我沒有使用多程式或者多執行緒,也沒有使用Scrapy自帶的ImagePipeline(自由度不高),有興趣的童鞋可以自己選擇實現。

import requests
import threading
import os
from scrapy.exceptions import DropItem, CloseSpider


class DeviantArtSpiderPipeline(object):
    def __init__(self, IMAGE_STORE, MAXIMUM_IMAGE_NUMBER):
        if IMAGE_STORE is None or MAXIMUM_IMAGE_NUMBER is None:
            raise CloseSpider('Pipeline load settings failed')
        self.IMAGE_STORE = IMAGE_STORE
        self.MAXIMUM_IMAGE_NUMBER = MAXIMUM_IMAGE_NUMBER
        # 記錄當前下載的圖片數量
        self.image_max_counter = 0
        # 根據圖片數量建立資料夾,每1000張在一個資料夾中
        self.dir_counter = 0

    def process_item(self, item, spider):
        if item is None:
            raise DropItem('Item is null')
        dir_path = self.make_dir()
        # 拼接圖片名稱
        image_final_name = item['image_name'] + '-' + item['image_id'] + '-by@' + item['author'] + '.jpg'
        dest_path = os.path.join(dir_path, image_final_name)
        self.download_image(item['image_src'], dest_path)
        self.image_max_counter += 1
        if self.image_max_counter >= self.MAXIMUM_IMAGE_NUMBER:
            raise CloseSpider('Current downloaded image already equal maximum number')
        return item

    def make_dir(self):
        print('[IMAGE_CURRENT NUMBER] %d ' % self.image_max_counter)
        if self.image_max_counter % 1000 == 0:
            self.dir_counter += 1
        path = os.path.abspath(self.IMAGE_STORE)
        path = os.path.join(path, 'crawl_images')
        path = os.path.join(path, 'dir-' + str(self.dir_counter))
        if not os.path.exists(path):
            os.makedirs(path)
            print('[CREATED DIR] %s ' % path)
        return path

    def download_image(self, src, dest):
        print('[Thread %s] preparing download image.....' % threading.current_thread().name)
        response = requests.get(src, timeout=2)
        if response.status_code == 200:
            with open(dest, 'wb') as f:
                f.write(response.content)
                print('[DOWNLOAD FINISHED] from %s to %s ' % (src, dest))
        else:
            raise DropItem('[Thread %s] request image src failed status code = %s'
                           % (threading.current_thread().name, response.status_code))

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        return cls(settings['IMAGES_STORE'], settings['MAXIMUM_IMAGE_NUMBER'])複製程式碼

settings.py中註冊該Pipeline

ITEM_PIPELINES = {
    'deviant_art_spider.pipelines.DeviantArtSpiderPipeline': 300,
}複製程式碼

IP代理池


有些網站會有反爬蟲機制,為了解決這個問題,每次請求都使用不同的IP代理,有很多網站提供IP代理服務,我們需要寫一個爬蟲從雲代理中抓取它提供的免費IP代理(免費IP很不穩定,而且我用了代理之後反而各種請求失敗了Orz...)。

import os

import requests
from bs4 import BeautifulSoup


class ProxiesSpider(object):
    def __init__(self, max_page_number=10):
        self.seed = 'http://www.ip3366.net/free/'
        self.max_page_number = max_page_number # 最大頁數
        self.crawled_proxies = [] # 爬到的ip,每個元素都是一個dict
        self.verified_proxies = [] # 校驗過的ip
        self.headers = {
            'Accept': '*/*',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)'
                          ' Chrome/45.0.2454.101 Safari/537.36',
            'Accept-Language': 'zh-CN,zh;q=0.8'
        }
        self.tocrawl_url = []

    def crawl(self):
        self.tocrawl_url.append(self.seed)
        page_counter = 1
        while self.tocrawl_url:
            if page_counter > self.max_page_number:
                break
            url = self.tocrawl_url.pop()
            body = requests.get(url=url, headers=self.headers, params={'page': page_counter}).content
            soup = BeautifulSoup(body, 'lxml')
            if soup is None:
                print('PARSE PAGE FAILED.......')
                continue
            self.parse_page(soup)
            print('Parse page %s done' % (url + '?page=' + str(page_counter)))
            page_counter += 1
            self.tocrawl_url.append(url)
        self.verify_proxies()
        self.download()

    # 解析頁面並封裝
    def parse_page(self, soup):
        table = soup.find('table', class_='table table-bordered table-striped')
        tr_list = table.tbody.find_all('tr')
        for tr in tr_list:
            ip = tr.contents[1].text
            port = tr.contents[3].text
            protocol = tr.contents[7].text.lower()
            url = protocol + '://' + ip + ':' + port
            self.crawled_proxies.append({url: protocol})
            print('Add url %s to crawled_proxies' % url)

    # 對ip進行校驗
    def verify_proxies(self):
        print('Start verify proxies.......')
        while self.crawled_proxies:
            self.verify_proxy(self.crawled_proxies.pop())
        print('Verify proxies done.....')

    def verify_proxy(self, proxy):
        proxies = {}
        for key in proxy:
            proxies[str(proxy[key])] = key # requests的proxies的格式必須為 協議 : 地址
        try:
            if requests.get('https://www.deviantart.com/', proxies=proxies, timeout=2).status_code == 200:
                print('verify proxy success %s ' % proxies)
                self.verified_proxies.append(proxy)
        except:
            print('verify proxy fail %s ' % proxies)

    # 儲存到檔案中
    def download(self):
        current_path = os.getcwd()
        parent_path = os.path.dirname(current_path)
        with open(parent_path + '\proxies.txt', 'w') as f:
            for proxy in self.verified_proxies:
                for key in proxy.keys():
                    f.write(key + '\n')


if __name__ == '__main__':
    spider = ProxiesSpider()
    spider.crawl()複製程式碼

得到了IP代理池之後,還要在Scrapymiddlewares.py模組定義代理中介軟體類。

import time
from scrapy import signals
import os
import random


class ProxyMiddleware(object):
    # 每次請求前從IP代理池中選擇一個IP代理並進行設定
    def process_request(self, request, spider):
        proxy = self.get_proxy(self.make_path())
        print('Acquire proxy %s ' % proxy)
        request.meta['proxy'] = proxy

    # 請求失敗,重新設定IP代理
    def process_response(self, request, response, spider):
        if response.status != 200:
            proxy = self.get_proxy(self.make_path())
            print('Response status code is not 200,try reset request proxy %s ' % proxy)
            request.meta['proxy'] = proxy
            return request
        return response

    def make_path(self):
        current = os.path.abspath('.')
        parent = os.path.dirname(current)
        return os.path.dirname(parent) + '\proxies.txt'

    # 從IP代理檔案中隨機獲得一個IP代理地址
    def get_proxy(self, path):
        if not os.path.isfile(path):
            print('[LOADING PROXY] loading proxies failed proxies file is not exist')
        while True:
            with open(path, 'r') as f:
                proxies = f.readlines()
            if proxies:
                break
            else:
                time.sleep(1)
        return random.choice(proxies).strip()複製程式碼

最後在settings.py中進行註冊。

DOWNLOADER_MIDDLEWARES = {
    # 這個中介軟體是由scrapy提供的,並且它是必需的
    'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 543, 
    # 我們自定義的代理中介軟體
    'deviant_art_spider.middlewares.ProxyMiddleware': 540 
}複製程式碼

End


我們的圖片爬蟲已經完成了,執行命令scrapy crawl deviant_art_image_spider,然後盡情蒐集圖片吧!

想要獲得本文中的完整原始碼與P站爬蟲請點我,順便求個star...

最近心血來潮想要寫爬蟲,所以花了點時間過了一遍python語法便匆匆上手了,程式碼寫的有點醜也不夠pythonic,各位看官求請吐槽。

相關文章