基於 Scrapy-redis 的分佈式爬蟲詳細設計

基於 Scrapy-redis 的分佈式爬蟲設計

 

目錄


前言

在本篇中,我假定您已經熟悉並安裝了 Python3。 如若否則,請參考 Python 入門指南css

關於 Scrapy

Scrapy 是一個爲了爬取網站數據,提取結構性數據而編寫的應用框架。 能夠應用在包括數據挖掘,信息處理或存儲歷史數據等一系列的程序中。html

其最初是爲了 網絡抓取 所設計的, 也能夠應用在獲取 API 所返回的數據(例如 Amazon Associates Web Services ) 或者通用的網絡爬蟲。python

架構概覽

 
Paste_Image.png

安裝

環境

  

Redis 3.2.5
Python 3.5.2
    Scrapy 1.3.3
    scrapy-redis 0.6.8
    redis-py 2.10.5
    PyMySQL 0.7.10
    SQLAlchemy 1.1.6

 

Debian / Ubuntu / Deepin 下安裝

安裝前你可能須要把 Python3 設置爲默認的 Python 解釋器,或者使用 virtualenv 搭建一個 Python 的虛擬環境,篇幅有限,此處再也不贅述。mysql

安裝 Redis

sudo apt-get install redis-server

 

安裝 Scrapy

sudo apt-get install build-essential libssl-dev libffi-dev python-dev
sudo apt install python3-pip
sudo pip install scrapy scrapy-reids

 

安裝 scrapy-redis

sudo pip install scrapy-reids

 

Windows 下安裝

因爲目前 Python 實現的一部分第三方模塊在 Windows 下並無可用的安裝包,我的並不推薦以 Windows 做爲開發環境。正則表達式

若是你非要這麼作,你可能會遇到如下異常:redis

  • ImportError: DLL load failed: %1 不是有效的 Win32 應用程序
    • 這是因爲你安裝了 64 位的 Python,但卻意外安裝了 32 位的模塊
  • Failed building wheel for cryptography
    • 你須要升級你的 pip 並從新安裝 cryptography 模塊
  • ERROR: 'xslt-config' is not recognized as an internal or external command,
    operable program or batch file.
    • 你須要從 lxml 的官網下載該模塊編譯好的 exe 安裝包,並用 easy_install 手動進行安裝
  • ImportError: Nomodule named win32api
    • 這是個 Twisted bug ,你須要安裝 pywin32 。

若是你尚未放棄,如下內容可能會幫到你:算法


基本使用

初始化項目

  • 命令行下初始化 Scrapy 項目
scrapy startproject spider_ebay

 

  • 執行後將會生成如下目錄結構
└── spider_ebay
  ├── spider_ebay
  │   ├── __init__.py
  │   ├── items.py
  │   ├── middlewares.py
  │   ├── pipelines.py
  │   ├── settings.py
  │   └── spiders
  │       └── __init__.py
  └── scrapy.cfg

 

建立爬蟲

  • 建立文件 
    spider_ebay/spider_ebay/spiders/example.py

     

  • 代碼以下:
from scrapy.spiders import Spider

class ExampleSpider(Spider):
    name = 'example'
    start_urls = ['http://www.ebay.com/sch/allcategories/all-categories']

    def parse(self, response):
        datas = response.xpath("//div[@class='gcma']/ul/li/a[@class='ch']")
        for data in datas:
            try:
                yield {
                    'name': data.xpath("text()").extract_first(),
                    'link': data.xpath("@href").extract_first()
                }
                # or
                # yield self.make_requests_from_url(data.xpath("@href").extract_first())
            except:
                pass

 

  • 該例爬取了 eBay 商品分類頁面下的子分類頁的 url 信息sql

  • ExampleSpider 繼承自 Spider,定義了 name 、start_urls 屬性與 parse 方法。
    程序經過 name 來調用爬蟲,爬蟲運行時會先從 strart_urls 中提取 url 構造 request,獲取到對應的 response 時,
    利用 parse 方法解析 response,最後將目標數據或新的 request 經過 yield 語句以生成器的形式返回。數據庫

運行爬蟲

cd spider_ebay
scrapy crawl example -o items.json

 

爬取結果

  

spider_ebay/items.json

 

[
{"name": "Antiquities", "link": "http://www.ebay.com/sch/Antiquities/37903/i.html"},
{"name": "Architectural & Garden", "link": "http://www.ebay.com/sch/Architectural-Garden/4707/i.html"},
{"name": "Asian Antiques", "link": "http://www.ebay.com/sch/Asian-Antiques/20082/i.html"},
{"name": "Decorative Arts", "link": "http://www.ebay.com/sch/Decorative-Arts/20086/i.html"},
{"name": "Ethnographic", "link": "http://www.ebay.com/sch/Ethnographic/2207/i.html"},
{"name": "Home & Hearth", "link": "http://www.ebay.com/sch/Home-Hearth/163008/i.html"},
{"name": "Incunabula", "link": "http://www.ebay.com/sch/Incunabula/22422/i.html"},
{"name": "Linens & Textiles (Pre-1930)", "link": "http://www.ebay.com/sch/Linens-Textiles-Pre-1930/181677/i.html"},
{"name": "Manuscripts", "link": "http://www.ebay.com/sch/Manuscripts/23048/i.html"},
{"name": "Maps, Atlases & Globes", "link": "http://www.ebay.com/sch/Maps-Atlases-Globes/37958/i.html"},
{"name": "Maritime", "link": "http://www.ebay.com/sch/Maritime/37965/i.html"},
{"name": "Mercantile, Trades & Factories", "link": "http://www.ebay.com/sch/Mercantile-Trades-Factories/163091/i.html"},
{"name": "Musical Instruments (Pre-1930)", "link": "http://www.ebay.com/sch/Musical-Instruments-Pre-1930/181726/i.html"},
{"name": "Other Antiques", "link": "http://www.ebay.com/sch/Other-Antiques/12/i.html"},
{"name": "Periods & Styles", "link": "http://www.ebay.com/sch/Periods-Styles/100927/i.html"},
{"name": "Primitives", "link": "http://www.ebay.com/sch/Primitives/1217/i.html"},
{"name": "Reproduction Antiques", "link": "http://www.ebay.com/sch/Reproduction-Antiques/22608/i.html"},
{"name": "Restoration & Care", "link": "http://www.ebay.com/sch/Restoration-Care/163101/i.html"},
{"name": "Rugs & Carpets", "link": "http://www.ebay.com/sch/Rugs-Carpets/37978/i.html"},
{"name": "Science & Medicine (Pre-1930)", "link": "http://www.ebay.com/sch/Science-Medicine-Pre-1930/20094/i.html"},
{"name": "Sewing (Pre-1930)", "link": "http://www.ebay.com/sch/Sewing-Pre-1930/156323/i.html"},
{"name": "Silver", "link": "http://www.ebay.com/sch/Silver/20096/i.html"},
{"name": "Art from Dealers & Resellers", "link": "http://www.ebay.com/sch/Art-from-Dealers-Resellers/158658/i.html"},
{"name": "Direct from the Artist", "link": "http://www.ebay.com/sch/Direct-from-the-Artist/60435/i.html"},
{"name": "Baby Gear", "link": "http://www.ebay.com/sch/Baby-Gear/100223/i.html"},
{"name": "Baby Safety & Health", "link": "http://www.ebay.com/sch/Baby-Safety-Health/20433/i.html"},
{"name": "Bathing & Grooming", "link": "http://www.ebay.com/sch/Bathing-Grooming/20394/i.html"},
{"name": "Car Safety Seats", "link": "http://www.ebay.com/sch/Car-Safety-Seats/66692/i.html"},
{"name": "Carriers, Slings & Backpacks", "link": "http://www.ebay.com/sch/Carriers-Slings-Backpacks/100982/i.html"},
{"name": "Diapering", "link": "http://www.ebay.com/sch/Diapering/45455/i.html"},
{"name": "Feeding", "link": "http://www.ebay.com/sch/Feeding/20400/i.html"},
{"name": "Keepsakes & Baby Announcements", "link": "http://www.ebay.com/sch/Keepsakes-Baby-Announcements/117388/i.html"},
......

 


進階使用

分佈式爬蟲

架構

 
i (1).png
  1. MasterSpider 對 start_urls 中的 urls 構造 request,獲取 response
  2. MasterSpider 將 response 解析,獲取目標頁面的 url, 利用 redisurl 去重並生成待爬 request 隊列
  3. SlaveSpider 讀取 redis 中的待爬隊列,構造 request
  4. SlaveSpider 發起請求,獲取目標頁面的 response
  5. Slavespider 解析 response,獲取目標數據,寫入生產數據庫
關於 Redis

Redis 是目前公認的速度最快的基於內存的鍵值對數據庫express

Redis 做爲臨時數據的緩存區,能夠充分利用內存的高速讀寫能力大大提升爬蟲爬取效率。

關於 scrapy-redis

scrapy-redis 是爲了更方便地實現 Scrapy 分佈式爬取,而提供的一些以 Redis 爲基礎的組件。

scrapy 使用 python 自帶的 collection.deque 來存放待爬取的 request。scrapy-redis 提供了一個解決方案,把 deque 換成 redis 數據庫,能讓多個 spider 讀取同一個 redis 數據庫裏,解決了分佈式的主要問題。

配置

使用 scrapy-redis 組件前須要對 Scrapy 配置作一些調整

spider_ebay/settings.py

 

# 過濾器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 調度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 調度狀態持久化
SCHEDULER_PERSIST = True

# 請求調度使用優先隊列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue'

# redis 使用的端口和地址
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379

 

增長併發

併發是指同時處理數量。其有全侷限制和局部(每一個網站)的限制。

Scrapy 默認的全局併發限制對同時爬取大量網站的狀況並不適用。 增長多少取決於爬蟲能佔用多少 CPU。 通常開始能夠設置爲 100 。
不過最好的方式是作一些測試,得到 Scrapy 進程佔取 CPU 與併發數的關係。 爲了優化性能,應該選擇一個能使CPU佔用率在80%-90%的併發數。

增長全局併發數的一些配置:

# 默認 Item 併發數:100
CONCURRENT_ITEMS = 100

# 默認 Request 併發數:16
CONCURRENT_REQUESTS = 16

# 默認每一個域名的併發數:8
CONCURRENT_REQUESTS_PER_DOMAIN = 8

# 每一個IP的最大併發數:0表示忽略
CONCURRENT_REQUESTS_PER_IP = 0

 

緩存

scrapy默認已經自帶了緩存,配置以下

# 打開緩存
HTTPCACHE_ENABLED = True

# 設置緩存過時時間(單位:秒)
#HTTPCACHE_EXPIRATION_SECS = 0

# 緩存路徑(默認爲:.scrapy/httpcache)
HTTPCACHE_DIR = 'httpcache'

# 忽略的狀態碼
HTTPCACHE_IGNORE_HTTP_CODES = []

# 緩存模式(文件緩存)
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

 

Redis 遠程鏈接

安裝完成後,redis默認是不能被遠程鏈接的,此時要修改配置文件/etc/redis.conf

# bind 127.0.0.1

 

修改後,重啓redis服務器

systemctl restart redis

 

若是要增長redis的訪問密碼,修改配置文件/etc/redis.conf

requirepass passwrd

 

增長了密碼後,啓動客戶端的命令變爲:

redis-cli -a passwrd

 

測試是否能遠程登錄

使用 windows 的命令窗口進入 redis 安裝目錄,用命令進行遠程鏈接 redis:

redis-cli -h 192.168.1.112 -p 6379

 

 

在本機上測試是否能讀取 master 的 redis


 

在遠程機器上讀取是否有該數據


 

能夠確信 redis 配置完成

MasterSpider

# coding: utf-8
from scrapy import Item, Field
from scrapy.spiders import Rule
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.linkextractors import LinkExtractor
from redis import Redis
from time import time
from urllib.parse import urlparse, parse_qs, urlencode


class MasterSpider(RedisCrawlSpider):
    name = 'ebay_master'
    redis_key = 'ebay:start_urls'

    ebay_main_lx = LinkExtractor(allow=(r'http://www.ebay.com/sch/allcategories/all-categories', ))
    ebay_category2_lx = LinkExtractor(allow=(r'http://www.ebay.com/sch/[^\s]*/\d+/i.html',
                                             r'http://www.ebay.com/sch/[^\s]*/\d+/i.html?_ipg=\d+&_pgn=\d+',
                                             r'http://www.ebay.com/sch/[^\s]*/\d+/i.html?_pgn=\d+&_ipg=\d+',))

    rules = (
        Rule(ebay_category2_lx, callback='parse_category2', follow=False),
        Rule(ebay_main_lx, callback='parse_main', follow=False),
    )

    def __init__(self, *args, **kwargs):
        domain = kwargs.pop('domain', '')
        # self.allowed_domains = filter(None, domain.split(','))
        super(MasterSpider, self).__init__(*args, **kwargs)

    def parse_main(self, response):
        pass
        data = response.xpath("//div[@class='gcma']/ul/li/a[@class='ch']")
        for d in data:
            try:
                item = LinkItem()
                item['name'] = d.xpath("text()").extract_first()
                item['link'] = d.xpath("@href").extract_first()
                yield self.make_requests_from_url(item['link'] + r"?_fsrp=1&_pppn=r1&scp=ce2")
            except:
                pass

    def parse_category2(self, response):
        data = response.xpath("//ul[@id='ListViewInner']/li/h3[@class='lvtitle']/a[@class='vip']")
        redis = Redis()
        for d in data:
            # item = LinkItem()
            try:
                self._filter_url(redis, d.xpath("@href").extract_first())

            except:
                pass
        try:
            next_page = response.xpath("//a[@class='gspr next']/@href").extract_first()
        except:
            pass
        else:
            # yield self.make_requests_from_url(next_page)
            new_url = self._build_url(response.url)
            redis.lpush("test:new_url", new_url)
            # yield self.make_requests_from_url(new_url)
            # yield Request(url, headers=self.headers, callback=self.parse2)

    def _filter_url(self, redis, url, key="ebay_slave:start_urls"):
        is_new_url = bool(redis.pfadd(key + "_filter", url))
        if is_new_url:
            redis.lpush(key, url)


    def _build_url(self, url):
        parse = urlparse(url)
        query = parse_qs(parse.query)
        base = parse.scheme + '://' + parse.netloc + parse.path

        if '_ipg' not in query.keys() or '_pgn' not in query.keys() or '_skc' in query.keys():
            new_url = base + "?" + urlencode({"_ipg": "200", "_pgn": "1"})
        else:
            new_url = base + "?" + urlencode({"_ipg": query['_ipg'][0], "_pgn": int(query['_pgn'][0]) + 1})
        return new_url


class LinkItem(Item):
    name = Field()
    link = Field()

 

MasterSpider 繼承來自 scrapy-redis 組件下的 RedisCrawlSpider,相比 ExampleSpider 有了如下變化:

  • redis_key
    • 該爬蟲的 start_urls 的存放容器由原先的 Python list 改至 redis list,因此此處須要 redis_key 存放 redis list key
  • rules
    • rules 是含有多個 Rule 對象的 tuple
    • Rule 對象實例化經常使用的三個參數:link_extractor / callback / follow
      • link_extractor 是一個LinkExtractor 對象。 其定義瞭如何從爬取到的頁面提取連接
      • callback 是一個 callablestring (該spider中同名的函數將會被調用)。 從 link_extractor中每獲取到連接時將會調用該函數。該回調函數接受一個response做爲其第一個參數, 並返回一個包含 Item 以及(或) Request 對象(或者這二者的子類)的列表(list)。
      • follow 是一個布爾(boolean)值,指定了根據該規則從response提取的連接是否須要跟進。 若是 callback None, follow 默認設置爲 True ,不然默認爲 False 。
      • process_links 處理全部的連接的回調,用於處理從response提取的links,一般用於過濾(參數爲link列表)
      • process_request 連接請求預處理(添加headercookie等)
  • ebay_main_lx / ebay_category2_lx
    • LinkExtractor 對象
      • allow (a regular expression (or list of)) – 必需要匹配這個正則表達式(或正則表達式列表)的URL纔會被提取。若是沒有給出(或爲空), 它會匹配全部的連接。
      • deny 排除正則表達式匹配的連接(優先級高於allow
      • allow_domains 容許的域名(能夠是strlist
      • deny_domains 排除的域名(能夠是strlist
      • restrict_xpaths 取知足XPath選擇條件的連接(能夠是strlist
      • restrict_css 提取知足css選擇條件的連接(能夠是strlist
      • tags 提取指定標籤下的連接,默認從aarea中提取(能夠是strlist
      • attrs 提取知足擁有屬性的連接,默認爲href(類型爲list
      • unique 連接是否去重(類型爲boolean
      • process_value 值處理函數(優先級大於allow
  • parse_main / parse_category2
    • 用於解析符合對應 ruleurlresponse 的方法
  • _filter_url / _build_url
    • 一些有關 url 的工具方法
  • LinkItem
    • 繼承自 Item 對象
    • Item 對象是種簡單的容器,用於保存爬取到得數據。 其提供了相似於 dictAPI 以及用於聲明可用字段的簡單語法。

SlaveSpider

# coding: utf-8
from scrapy import Item, Field
from scrapy_redis.spiders import RedisSpider


class SlaveSpider(RedisSpider):
    name = "ebay_slave"
    redis_key = "ebay_slave:start_urls"

    def parse(self, response):
        item = ProductItem()
        item["price"] = response.xpath("//span[contains(@id,'prcIsum')]/text()").extract_first()
        item["item_id"] = response.xpath("//div[@id='descItemNumber']/text()").extract_first()
        item["seller_name"] = response.xpath("//span[@class='mbg-nw']/text()").extract_first()
        item["sold"] = response.xpath("//span[@class='vi-qtyS vi-bboxrev-dsplblk vi-qty-vert-algn vi-qty-pur-lnk']/a/text()").extract_first()
        item["cat_1"] = response.xpath("//li[@class='bc-w'][1]/a/span/text()").extract_first()
        item["cat_2"] = response.xpath("//li[@class='bc-w'][2]/a/span/text()").extract_first()
        item["cat_3"] = response.xpath("//li[@class='bc-w'][3]/a/span/text()").extract_first()
        item["cat_4"] = response.xpath("//li[@class='bc-w'][4]/a/span/text()").extract_first()
        yield item


class ProductItem(Item):
    name = Field()
    price = Field()
    sold = Field()
    seller_name = Field()
    pl_id = Field()
    cat_id = Field()
    cat_1 = Field()
    cat_2 = Field()
    cat_3 = Field()
    cat_4 = Field()
    item_id = Field()

 

SlaveSpider 繼承自 RedisSpider,屬性與方法相比 MasterSpider 簡單了很多,少了 rules與其餘,但大體功能都比較相似
SlaveSpider 從 ebay_slave:start_urls 下讀取構建好的目標頁面的 request,對 response 解析出目標數據,以 ProductItem 的形式輸出數據

數據存儲

scrpay-redis 默認狀況下會將爬取到的目標數據寫入 redis
利用 Python 豐富的數據庫接口支持能夠經過 Pipeline 把 Item 中的數據存放在任意一種常見的數據庫中

關於 SQLAlchemy

SQLAlchemy 是在Python中最有名的 ORM 框架。經過 SQLAlchemy 你能夠用操做對象的方式來操做 mysql,sqlite,sqlserver,oracle 等大部分常見數據庫

安裝

pip install pymysql
pip install sqlalchemy

 

  • ebay_spider/settings.py
ITEM_PIPELINES = {
    'ebay_spider.pipelines.ExamplePipeline': 300,
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

 

咱們在 settings.py 模塊中配置 ebay_spider.pipelines.ExamplePipeline 把 ExamplePipeline 配置到爬蟲上,後面的數字 300 表示 pipeline 的執行順序,數值小的先執行
scrapy_redis.pipelines.RedisPipeline 是 scrapy-redis 使用的默認的 pipeline,若是不須要 redis 保存目標數據,能夠不配置

  • ebay_spider/pipelines.py
from scrapy.exceptions import DropItem
from sqlalchemy import create_engine

from .model.config import DBSession
from .model.transfer import Transfer


class ExamplePipeline(object):
    def open_spider(self, spider):
        self.session = DBSession()
        self.session.execute('SET NAMES utf8;')
        self.session.execute('SET CHARACTER SET utf8;')
        self.session.execute('SET character_set_connection=utf8;')

    def process_item(self, item, spider):
        a = Transfer(
            transfer_order_id = item['session_online_id'],
            transfer_content = item['session_name'].encode('utf8')
        )
        self.session.merge(a)
        self.session.commit()
        return item

    def close_spider(self, spider):
        self.session.close()

 

此處定義把數據保存到 MysqlExamplePipeline

其中,pipelineopen_spiderspider_closed 兩個方法,在爬蟲啓動和關閉的時候調用
pipeline 在爬蟲啓動時,創建起與 Mysql 的鏈接。當 spider 輸出 Item 時將 Item 中的數據存入 Mysql 中。在爬蟲關閉的同時,關閉與數據庫的鏈接

  • ebay_spider/models/config.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("mysql+pymysql://root:12345678@localhost/beston")
DBSession = sessionmaker(bind=engine)

 

這是 ExamplePipeline 中使用到的數據庫鏈接配置。要注意的是,此處使用的是 pymysql 做爲數據庫驅動,而不是 MySQLdb。

  • ebay_spider/models/transfer.py
# coding:utf8
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Transfer(Base):

    # 表名
    __tablename__ = 'bt_transfer'

    __table_args__ = {
        'mysql_engine': 'MyISAM',
        'mysql_charset': 'utf8'
    }

    # 表結構
    transfer_id = Column(Integer, primary_key=True)
    transfer_order_id = Column(Integer)
    transfer_content = Column(String(255))

 

以上是 Mysql ORM 模型,定義了 bt_transfer 表。也可以使用 SQLAlchemy 的命令來生成此表。

anti-anti-spider

大多網站對爬蟲的活動都進行了限制,anti-anti-spider 即 反反爬蟲。是爲了突破這些限制的一些解決方案的稱呼。
如下介紹幾種經常使用的方案

僞造 User-Agent

經過僞造 request header 中的 User-Agent 能夠模仿瀏覽器操做,從而繞過一些網站的反爬蟲機制

  • 首先創建一個 User-Agent 池
  • user_agent.py
agents = [
    "Mozilla/5.0 (Linux; U; Android 2.3.6; en-us; Nexus S Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
    "Avant Browser/1.2.789rel1 (http://www.avantbrowser.com)",
    "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",
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7",
    "Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10",
    "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.11) Gecko/2009060215 Firefox/3.0.11 (.NET CLR 3.5.30729)",
    "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB5",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E)",
    "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
    "Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110622 Firefox/6.0a2",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b4pre) Gecko/20100815 Minefield/4.0b4pre",
    "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0 )",
    "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98; Win 9x 4.90)",
    "Mozilla/5.0 (Windows; U; Windows XP) Gecko MultiZilla/1.6.1.0a",
    "Mozilla/2.02E (Win95; U)",
    "Mozilla/3.01Gold (Win95; I)",
    "Mozilla/4.8 [en] (Windows NT 5.1; U)",
    "Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.4) Gecko Netscape/7.1 (ax)",
    "HTC_Dream Mozilla/5.0 (Linux; U; Android 1.5; en-ca; Build/CUPCAKE) AppleWebKit/528.5  (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
    "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0",
    "Mozilla/5.0 (Linux; U; Android 1.5; en-us; sdk Build/CUPCAKE) AppleWebkit/528.5  (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
    "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
    "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
    "Mozilla/5.0 (Linux; U; Android 1.5; en-us; htc_bahamas Build/CRB17) AppleWebKit/528.5  (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1",
    "Mozilla/5.0 (Linux; U; Android 2.1-update1; de-de; HTC Desire 1.19.161.5 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
    ......
]

 

  • 重寫 UserAgentMiddleware
import random
from .user_agent import agents
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware

class UserAgentmiddleware(UserAgentMiddleware):

    def process_request(self, request, spider):
        agent = random.choice(agents)
        request.headers["User-Agent"] = agent

 

UserAgentmiddleware 定義了函數 process_request(request, spider),Scrapy 每個 request 經過中間件都會隨機的從 user_agent.py 中獲取一個僞造的 User-Agent 放入 request 的 header,來達到欺騙的目的。

IP proxy

反爬蟲一個最經常使用的方法的就是限制 ip。爲了不最壞的狀況,能夠利用代理服務器來爬取數據,scrapy 設置代理服務器只須要在請求前設置 Request 對象的 meta 屬性,添加 proxy 值便可,
能夠經過中間件來實現:

class ProxyMiddleware(object):
    def process_request(self, request, spider):
        proxy = 'https://178.33.6.236:3128'     # 代理服務器
        request.meta['proxy'] = proxy

 

另外,也可使用大量的 IP Proxy 創建起代理 IP 池,請求時隨機調用來避免更嚴苛的 IP 限制機制,方法相似 User-Agent 池

URL Filter

正常業務邏輯下,爬蟲不會對重複爬取同一個頁面兩次。因此爬蟲默認都會對重複請求進行過濾,但當爬蟲體量達到千萬級時,默認的過濾器佔用的內存將會遠遠超乎你的想象。
爲了解決這個問題,能夠經過一些算法來犧牲一點點過濾的準確性來換取更小的空間複雜度

Bloom Filter

Bloom Filter能夠用於檢索一個元素是否在一個集合中。它的優勢是空間效率和查詢時間都遠遠超過通常的算法,缺點是有必定的誤識別率和刪除困難。

Hyperloglog

HyperLogLog是一個基數估計算法。其空間效率很是高,1.5K內存能夠在偏差不超過2%的前提下,用於超過10億的數據集合基數估計。

這兩種算法都是合適的選擇,以 Hyperloglog 爲例
因爲 redis 已經提供了支持 hyperloglog 的數據結構,因此只需對此數據結構進行操做便可
MasterSpider 下的 _filter_url 實現了過濾 URL 的功能

def _filter_url(self, redis, url, key="ebay_slave:start_urls"):
    is_new_url = bool(redis.pfadd(key + "_filter", url))
    if is_new_url:
        redis.lpush(key, url)

 

當 redis.pfadd() 執行時,一個 url 嘗試插入 hyperloglog 結構中,若是 url 存在返回 0,反之返回 1。由此來判斷是否要將該 url 存放至待爬隊列


總結

Scrapy 是一個優秀的爬蟲框架。性能上,它快速強大,多線程併發與事件驅動的設計能將爬取效率提升幾個數量級;功能上,它又極易擴展,支持插件,無需改動核心代碼。但若是要運用在在大型爬蟲項目中,不支持分佈式設計是它的一個大硬傷。幸運的是,scrapy-redis 組件解決了這個問題,並給 Scrapy 帶來了更多的可能性。


相關資料

Scrapy 1.0 文檔
Scrapy-Redis’s documentation
使用SQLAlchemy
布隆過濾器
HyperLogLog
Python 入門指南
scrapy_redis去重優化
基於Scrapy-Redis的分佈式以及cookies池

 

本文摘自(特別感謝分享):https://www.jianshu.com/p/cd4054bbc757/

相關文章
相關標籤/搜索