基于 Scrapy-redis 的分布式爬蟲設計

目錄


前言

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

關于 Scrapy

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

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

架構概覽

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 的虛擬環境,篇幅有限,此處不再贅述。

安裝 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 作為開發環境。

如果你非要這么做,你可能會遇到以下異常:

  • 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("http://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 信息

  • ExampleSpider 繼承自 Spider,定義了 namestart_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. MasterSpiderstart_urls 中的 urls 構造 request,獲取 response
  2. MasterSpiderresponse 解析,獲取目標頁面的 url, 利用 redis 對 url 去重并生成待爬 request 隊列
  3. SlaveSpider 讀取 redis 中的待爬隊列,構造 request
  4. SlaveSpider 發起請求,獲取目標頁面的 response
  5. Slavespider 解析 response,獲取目標數據,寫入生產數據庫
關于 Redis

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

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
wps96F1.tmp

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


wps96F2.tmp

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


wps96F3.tmp

可以確信 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("http://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("http://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("http://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?是一個 callable 或 string (該spider中同名的函數將會被調用)。 從 link_extractor中每獲取到鏈接時將會調用該函數。該回調函數接受一個response作為其第一個參數, 并返回一個包含?Item?以及(或)?Request?對象(或者這兩者的子類)的列表(list)。
      • follow?是一個布爾(boolean)值,指定了根據該規則從response提取的鏈接是否需要跟進。 如果?callback?為None,?follow?默認設置為?True?,否則默認為?False?。
      • process_links 處理所有的鏈接的回調,用于處理從response提取的links,通常用于過濾(參數為link列表)
      • process_request 鏈接請求預處理(添加header或cookie等)
  • ebay_main_lx / ebay_category2_lx
    • LinkExtractor 對象
      • allow?(a regular expression (or list of)) – 必須要匹配這個正則表達式(或正則表達式列表)的URL才會被提取?如果沒有給出(或為空), 它會匹配所有的鏈接?
      • deny 排除正則表達式匹配的鏈接(優先級高于allow)
      • allow_domains 允許的域名(可以是str或list)
      • deny_domains 排除的域名(可以是str或list)
      • restrict_xpaths: 取滿足XPath選擇條件的鏈接(可以是str或list)
      • restrict_css 提取滿足css選擇條件的鏈接(可以是str或list)
      • tags 提取指定標簽下的鏈接,默認從a和area中提取(可以是str或list)
      • attrs 提取滿足擁有屬性的鏈接,默認為href(類型為list)
      • unique 鏈接是否去重(類型為boolean)
      • process_value 值處理函數(優先級大于allow)
  • parse_main / parse_category2
    • 用于解析符合對應 rule 的 url 的 response 的方法
  • _filter_url / _build_url
    • 一些有關 url 的工具方法
  • LinkItem
    • 繼承自 Item 對象
    • Item 對象是種簡單的容器,用于保存爬取到得數據。 其提供了類似于 dict 的 API 以及用于聲明可用字段的簡單語法。

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("http://span[contains(@id,'prcIsum')]/text()").extract_first()
        item["item_id"] = response.xpath("http://div[@id='descItemNumber']/text()").extract_first()
        item["seller_name"] = response.xpath("http://span[@class='mbg-nw']/text()").extract_first()
        item["sold"] = response.xpath("http://span[@class='vi-qtyS vi-bboxrev-dsplblk vi-qty-vert-algn vi-qty-pur-lnk']/a/text()").extract_first()
        item["cat_1"] = response.xpath("http://li[@class='bc-w'][1]/a/span/text()").extract_first()
        item["cat_2"] = response.xpath("http://li[@class='bc-w'][2]/a/span/text()").extract_first()
        item["cat_3"] = response.xpath("http://li[@class='bc-w'][3]/a/span/text()").extract_first()
        item["cat_4"] = response.xpath("http://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 與其他,但大致功能都比較類似
SlaveSpiderebay_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()

此處定義把數據保存到 Mysql 的 ExamplePipeline,

其中,pipeline 的 open_spider 和 spider_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池

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容