基於 Scrapy-redis 的分布式爬蟲設計
目錄
前言
在本篇中,我假定您已經熟悉並安裝了 Python3。 如若不然,請參考 Python 入門指南。
關於 Scrapy
Scrapy 是一個為了爬取網站數據,提取結構性數據而編寫的應用框架。 可以應用在包括數據挖掘,信息處理或存儲歷史數據等一系列的程序中。
其最初是為了 網絡抓取 所設計的, 也可以應用在獲取 API 所返回的數據(例如 Amazon Associates Web Services ) 或者通用的網絡爬蟲。
架構概覽
安裝
環境
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 。
如果你還沒有放棄,以下內容可能會幫到你:
- Windows上Python3.5安裝Scrapy(lxml)
- Python爬蟲進階三之Scrapy框架安裝配置
- Microsoft Visual C++ Compiler for Python 2.7
- easy_install lxml on Python 2.7 on Windows
基本使用
初始化項目
- 命令行下初始化 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 信息
-
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"}, ......
進階使用
分布式爬蟲
架構
MasterSpider對start_urls中的 urls 構造request,獲取responseMasterSpider將response解析,獲取目標頁面的 url, 利用 redis 對 url 去重並生成待爬request隊列SlaveSpider讀取 redis 中的待爬隊列,構造requestSlaveSpider發起請求,獲取目標頁面的responseSlavespider解析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
在本機上測試是否能讀取 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
- 該爬蟲的
rulesrules是含有多個Rule對象的 tupleRule對象實例化常用的三個參數:link_extractor/callback/followlink_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_lxLinkExtractor對象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("//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()
此處定義把數據保存到 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池
本文摘自(特別感謝分享):https://www.jianshu.com/p/cd4054bbc757/
