Scrapy是一個基於Twisted的異步處理框架, 是純Python實現的爬蟲框架, 其架構清晰, 模塊之間的耦合程度低, 可擴展性極強, 能夠靈活完成各類需求. 咱們只須要定製開發幾個模塊就能夠輕鬆實現一個爬蟲.
Scrapy依賴twistedpython
linux下, linux
pip3 install scrapy
scrapy.cfg 項目的主配置信息。(真正爬蟲相關的配置信息在settings.py文件中)
items.py 設置數據存儲模板,用於結構化數據,如:Django的Model
pipelines 數據處理行爲,如:通常結構化的數據持久化
settings.py 配置文件,如:遞歸的層數、併發數,延遲下載等
spiders 爬蟲目錄,如:建立文件,編寫爬蟲規則
引擎(Scrapy) 用來處理整個系統的數據流處理, 觸發事務(框架核心)
調度器(Scheduler) 用來接受引擎發過來的請求, 壓入隊列中, 並在引擎再次請求的時候返回. 能夠想像成一個URL(抓取網頁的網址或者說是連接)的優先隊列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址
下載器(Downloader) 用於下載網頁內容, 並將網頁內容返回給蜘蛛(Scrapy下載器是創建在twisted這個高效的異步模型上的)
爬蟲(Spiders) 爬蟲是主要幹活的, 用於從特定的網頁中提取本身須要的信息, 即所謂的實體(Item)。用戶也能夠從中提取出連接,讓Scrapy繼續抓取下一個頁面
項目管道(Pipeline) 負責處理爬蟲從網頁中抽取的實體,主要的功能是持久化實體、驗證明體的有效性、清除不須要的信息。當頁面被爬蟲解析後,將被髮送到項目管道,並通過幾個特定的次序處理數據。
下載器中間件(Downloader Middlewares) 位於Scrapy引擎和下載器之間的框架,主要是處理Scrapy引擎與下載器之間的請求及響應。
爬蟲中間件(Spider Middlewares) 介於Scrapy引擎和爬蟲之間的框架,主要工做是處理蜘蛛的響應輸入和請求輸出。
調度中間件(Scheduler Middewares) 介於Scrapy引擎和調度之間的中間件,從Scrapy引擎發送到調度的請求和響應。
1. 【引擎】找到要執行的爬蟲,並執行爬蟲的 start_requests 方法,並的到一個 迭代器。 2. 迭代器循環時會獲取Request對象,而request對象中封裝了要訪問的URL和回調函數。 3. 將全部的request對象(任務)放到【調度器】中,用於之後被【下載器】下載。 4. 【下載器】去【調度器】中獲取要下載任務(就是Request對象),下載完成後執行回調函數。 5. 回到spider的回調函數中 yield Request() # 再次發起請求 yield Item() # 觸發一個信號去pipeline
1 建立project scrapy startproject <項目名稱> 2 建立爬蟲 cd <項目名稱> scrapy genspider <爬蟲名> <網址> # 如 scrapy genspider chouti chouti.com 3 啓動爬蟲 scrapy crawl <爬蟲名> --nolog # 不打印日誌
def parse(self, response): # response, 封裝了響應相關的全部數據 # 1 響應相關 # response.text # response.encoding # response.body # response.requeset # 當前響應時有那個請求發起, 請求中封裝了(要訪問的url, 回調函數) # 2 解析相關 response.xpath('//div[@href="x1"]/a').extract().extract_first() response.xpath('//div[@href="x1"]/a/text()').extract() tag_list = response.xpath('//div[@href="x1"]/a') for tag in tag_list: tag.xpath('.//p/text()').extract_first() # 3 再次發起請求 yield Request(url='xx', callback=self.parse)
當Spider解析完Response以後,Item就會傳遞到Item Pipeline,被定義的Item Pipeline組件會順次調用,完成一連串的處理過程,好比數據清洗、存儲等。redis
Item Pipeline的主要功能有以下4點。數據庫
- 清理HTML數據。
- 驗證爬取數據,檢查爬取字段。
- 查重並丟棄重複內容。
- 將爬取結果保存到數據庫。json
1. process_item(item, spider)。 # 核心方法 process_item()是必需要實現的方法,它必須返回Item類型的值或者拋出一個DropItem異常。 返回的是Item對象,那麼此Item會被低優先級的Item Pipeline的process_item()方法處理,直到全部的方法被調用完畢。 若是它拋出的是DropItem異常,那麼此Item會被丟棄,再也不進行處理。 process_item()方法的參數有以下兩個。 item,是Item對象,即被處理的Item。 spider,是Spider對象,即生成該Item的Spider。 2. open_spider(spider)。 在Spider開啓的時候被自動調用的。在這裏咱們能夠作一些初始化操做,如開啓數據庫鏈接等。其中,參數spider就是被開啓的Spider對象。 3. close_spider(spider)。 在Spider關閉的時候自動調用的。在這裏咱們能夠作一些收尾工做,如關閉數據庫鏈接等。其中,參數spider就是被關閉的Spider對象。 4. from_crawler(cls, crawler)。 是一個類方法,用@classmethod標識,是一種依賴注入的方式。它的參數是crawler,經過crawler對象,咱們能夠拿到Scrapy的全部核心組件,如全局配置的每一個信息,而後建立一個Pipeline實例。
1. 先寫Item類cookie
import scrapy class TxtItem(scrapy.Item): content = scrapy.Field() href = scrapy.Field()
2. 再寫pipeline類架構
# pipeline是全部爬蟲公用的, 給某個爬蟲定製pipeline, 使用spider.name併發
from scrapy.exceptions import DropItem class TxtPipeline(object): """ 源碼內容: 1. 判斷當前XdbPipeline類中是否有from_crawler 有: obj = XdbPipeline.from_crawler(....) 否: obj = XdbPipeline() 2. obj.open_spider() 3. obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item() 4. obj.close_spider() """ def __init__(self, path): self.f = None self.path = path @classmethod def from_crawler(cls, crawler): # 初始化時, 用於建立pipeline對象 path = crawler.settings.get('TXTPATH') return cls(path) def open_spider(self, spider): # 爬蟲開始執行 print('爬蟲開始了') self.f = open(self.path, 'a+', encoding='utf-8') def process_item(self, item, spider): # item爲字典數據格式 # yield item對象時, 執行 print('保存在到數據庫中...') self.f.write(json.dumps(dict(item), ensure_ascii=False)+'\n') # return item # 交給下一個pipeline的process_item()方法 raise DropItem() # 下一個pipeline的process_item方法中斷不執行 def close_spider(self, spider): # 爬蟲結束時執行 print('爬蟲結束了') self.f.close()
3. 配置pipelineapp
# 能夠配置多個pipeline, 值越小, 優先級越高框架
ITEM_PIPELINES = { 'scrapy_env.pipelines.TxtPipeline': 300, }
4. 爬蟲中
# yield每執行一次,process_item就調用一次。
yield TxtItem(content=content, href=href) # 會自動執行pipeline裏的process_item方法
原理: 把爬取請求放到調度器的爬取隊列前先去配置文件裏指定的【去重規則類】裏檢驗
# dont_filter=False (聽從去重規則, 默認配置) # dont_filter=True (不聽從去重規則) yield Request(url=page, callback=self.parse, dont_filter=False)
1. 編寫類DupeFilter
from scrapy.dupefilter import BaseDupeFilter from scrapy.utils.request import request_fingerprint class CustomizeDupeFilter(BaseDupeFilter): """ 自定義的url去重規則 :return: True表示已經訪問過;False表示未訪問過 """ def __init__(self): self.visited_fd = set() def request_seen(self, request): print('去重中') fd = request_fingerprint(request) # url的惟一標示 if fd in self.visited_fd: return True self.visited_fd.add(fd) def open(self): print('引擎開始///') def close(self, reason): print('引擎結束///')
2. 配置
DUPEFILTER_CLASS = 'scrapy_env.dupefilter.CustomizeDupeFilter'
3. 爬蟲中
yield Request(url=page, callback=self.parse) # 默認是使用去重規則的
1 配置文件中
DEPTH_LIMIT = 4 # 爬取深度爲4的url
# 爬蟲中看一下深度
print(response.meta.get('depth', 0))
import scrapy from scrapy.http.request import Request from scrapy.http.cookies import CookieJar class ChoutiSpider(scrapy.Spider): name = 'chouti' allowed_domains = ['chouti.com'] start_urls = ['http://chouti.com/'] cookie_dict = {} def parse(self, response): # 6.1 去響應頭中獲取cookie, 保存在cookie_jar對象中 cookie_jar = CookieJar() cookie_jar.extract_cookies(response, response.request) # 6.2 去cookie_jar對象中將cookie解析到字典中 for k, v in cookie_jar._cookies.items(): for i, j in v.items(): for m, n in j.items(): self.cookie_dict[m] = n.value print(self.cookie_dict) # 6.3 攜帶cookie去登陸 yield Request(url='https://dig.chouti.com/login', method='post', headers={ 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body='phone=8618648957737&password=/***/&oneMonth=1', cookies=self.cookie_dict, callback=self.check_result ) def check_result(self, response): print(response.text)
import scrapy from scrapy.http.request import Request class ChoutiSpider(scrapy.Spider): name = 'chouti' allowed_domains = ['chouti.com'] start_urls = ['http://chouti.com/'] def start_requests(self): url = 'http://dig.chouti.com/' # 1 設置了meta的cookiejar=True yield Request(url=url, callback=self.login, meta={'cookiejar': True}) def login(self, response): # print(response.headers.getlist('Set-Cookie')) yield Request( url='http://dig.chouti.com/login', method='POST', headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'}, body='phone=8618648957737&password=woshiniba&oneMonth=1', callback=self.check_result, meta={'cookiejar': True} # 2 會自動攜帶cookie過去 ) def check_result(self, response): print('檢查結果') print(response.text)
1 環境變量設置
# 在爬蟲啓動時,提早在os.envrion中設置代理便可。 def start_requests(self): import os os.environ['HTTPS_PROXY'] = "http://root:woshiniba@192.168.11.11:9999/" os.environ['HTTP_PROXY'] = '19.11.2.32'
2 經過meta傳參
# 粒度更細 yield Request(url=url,callback=self.parse,meta={'proxy':'"http://root:woshiniba@192.168.11.11:9999/"'})
3 經過自定義下載中間件
import base64 import random from six.moves.urllib.parse import unquote try: from urllib2 import _parse_proxy except ImportError: from urllib.request import _parse_proxy from six.moves.urllib.parse import urlunparse from scrapy.utils.python import to_bytes class XdbProxyMiddleware(object): def _basic_auth_header(self, username, password): user_pass = to_bytes( '%s:%s' % (unquote(username), unquote(password)), encoding='latin-1') return base64.b64encode(user_pass).strip() def process_request(self, request, spider): PROXIES = [ "http://root:woshiniba@192.168.11.11:9999/", "http://root:woshiniba@192.168.11.12:9999/", "http://root:woshiniba@192.168.11.13:9999/", "http://root:woshiniba@192.168.11.14:9999/", "http://root:woshiniba@192.168.11.15:9999/", "http://root:woshiniba@192.168.11.16:9999/", ] url = random.choice(PROXIES) orig_type = "" proxy_type, user, password, hostport = _parse_proxy(url) proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', '')) if user: creds = self._basic_auth_header(user, password) else: creds = None request.meta['proxy'] = proxy_url if creds: request.headers['Proxy-Authorization'] = b'Basic ' + creds
# 應用: 深度,優先級
1. 寫中間件
class SpiderMiddleware(object): def process_spider_input(self,response, spider): """ 下載完成,執行,而後交給parse處理 :param response: :param spider: :return: """ pass def process_spider_output(self,response, result, spider): """ spider處理完成,返回時調用 :param response: :param result: :param spider: :return: 必須返回包含 Request 或 Item 對象的可迭代對象(iterable) """ return result def process_spider_exception(self,response, exception, spider): """ 異常調用 :param response: :param exception: :param spider: :return: None,繼續交給後續中間件處理異常;含 Response 或 Item 的可迭代對象(iterable),交給調度器或pipeline """ return None def process_start_requests(self,start_requests, spider): """ 爬蟲啓動時調用 :param start_requests: :param spider: :return: 包含 Request 對象的可迭代對象 """ return start_requests
2. 配置
SPIDER_MIDDLEWARES = { # 'xdb.middlewares.XdbSpiderMiddleware': 543, 'xdb.sd.Sd1': 666, 'xdb.sd.Sd2': 667, }
# 應用: 更換代理IP,更換Cookies,更換User-Agent,自動重試。
1. 寫中間件
class DownMiddleware1(object): def process_request(self, request, spider): """ 請求須要被下載時,通過全部下載器中間件的process_request調用 :param request: :param spider: :return: None,繼續後續中間件去下載; Response對象,中止process_request的執行,開始執行process_response Request對象,中止中間件的執行,將Request從新調度器 raise IgnoreRequest異常,中止process_request的執行,開始執行process_exception """ pass def process_response(self, request, response, spider): """ spider處理完成,返回時調用 :param response: :param result: :param spider: :return: Response 對象:轉交給其餘中間件process_response Request 對象:中止中間件,request會被從新調度下載 raise IgnoreRequest 異常:調用Request.errback """ print('response1') return response def process_exception(self, request, exception, spider): """ 當下載處理器(download handler)或 process_request() (下載中間件)拋出異常 :param response: :param exception: :param spider: :return: None:繼續交給後續中間件處理異常; Response對象:中止後續process_exception方法 Request對象:中止中間件,request將會被從新調用下載 """ return None
2. 配置
DOWNLOADER_MIDDLEWARES = { #'xdb.middlewares.XdbDownloaderMiddleware': 543, # 'xdb.proxy.XdbProxyMiddleware':751, 'xdb.md.Md1':666, 'xdb.md.Md2':667, }
import sys from scrapy.cmdline import execute if __name__ == '__main__': execute(["scrapy","crawl","chouti","--nolog"])
a,在spiders同級建立任意目錄,如:commands
b,在其中建立 crawlall.py 文件 (此處文件名就是自定義的命令)
from scrapy.commands import ScrapyCommand from scrapy.utils.project import get_project_settings class Command(ScrapyCommand): requires_project = True def syntax(self): return '[options]' def short_desc(self): return 'Runs all of the spiders' def run(self, args, opts): spider_list = self.crawler_process.spiders.list() for name in spider_list: self.crawler_process.crawl(name, **opts.__dict__) self.crawler_process.start()
c,在settings.py 中添加配置 COMMANDS_MODULE = '項目名稱.目錄名稱'
d,在項目目錄執行命令
scrapy crawlall
自定義擴展時,利用信號在指定位置註冊制定操做
1. 自定義類
from scrapy import signals class MyExtension(object): def __init__(self, value): self.value = value @classmethod def from_crawler(cls, crawler): val = crawler.settings.getint('MMMM') ext = cls(val) crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened) crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed) return ext def spider_opened(self, spider): print('open') def spider_closed(self, spider): print('close')
2. 配置自定義擴展
EXTENSIONS = { # 'scrapy.extensions.telnet.TelnetConsole': None, 'xdb.ext.MyExtend': 666, }
scrapy-redis是一個基於redis的scrapy組件,經過它能夠快速實現簡單分佈式爬蟲程序,該組件本質上提供的功能:
1. dupefilter - URL去重規則(被調度器使用)
2. scheduler - 調度器
3. pipeline - 數據持久化
4. 定製起始URL
from scrapy.dupefilters import BaseDupeFilter class DdbDuperFiler(BaseDupeFilter): """ 基於redis的去重: 1 徹底自定義 """ def __init__(self): # 第一步: 鏈接redis self.conn = Redis(host='127.0.0.1', port='6379') def request_seen(self, request): """ 第二步: 檢測當前請求是否已經被訪問過 :return: True表示已經訪問過;False表示未訪問過 """ # 方式一 直接將url添加進集合中做爲判斷 # result = self.conn.sadd('urls', request.url) # if result == 1: # return False # return True # 方式二 先fingerprint再將url添加進集合中做爲判斷 fd = request_fingerprint(Request(url=request.url)) result = self.conn.sadd('urls', fd) if result == 1: return False return True
DUPEFILTER_CLASS = 'ddb.dup.DdbDuperFiler'
# 鏈接方式1 # REDIS_HOST = '127.0.0.1' # REDIS_PORT = '6379' # REDIS_PARAMS = {'password':'beta'} # redis鏈接參數 # 鏈接方式2(優先級高於1) REDIS_URL = 'redis://127.0.0.1:6379' # 'redis://user:pass@hostname:9001' REDIS_ENCODING = 'utf-8' DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' DUPEFILTER_KEY = 'dupefilter:%(timestamp)s' # 這是默認配置
from scrapy_redis.connection import get_redis_from_settings from scrapy_redis.dupefilter import RFPDupeFilter from scrapy_redis import defaults class RedisDuperFiler(RFPDupeFilter): """ 基於redis_redis的去重: 3 實現自定義redis中存儲的鍵的名字 """ @classmethod def from_settings(cls, settings): """Returns an instance from given settings. """ server = get_redis_from_settings(settings) # XXX: This creates one-time key. needed to support to use this # class as standalone dupefilter with scrapy's default scheduler # if scrapy passes spider on open() method this wouldn't be needed # TODO: Use SCRAPY_JOB env as default and fallback to timestamp. key = defaults.DUPEFILTER_KEY % {'timestamp': 'xiaodongbei'} debug = settings.getbool('DUPEFILTER_DEBUG') return cls(server, key=key, debug=debug)
REDIS_HOST = '127.0.0.1' REDIS_PORT = '6379' # REDIS_PARAMS = {'password':'beta'} # redis鏈接參數 REDIS_ENCODING = 'utf-8' DUPEFILTER_CLASS = 'ddb.dup.RedisDuperFiler' DUPEFILTER_KEY = 'dupefilter:%(timestamp)s' # 這是默認配置
# scrapy_redis爲咱們實現了配合Queue、DupFilter使用的調度器Scheduler
源碼地址: from scrapy_redis.scheduler import Scheduler
# enqueue_request: 向調度器中添加任務
# next_request: 去調度器中獲取一個任務
優先級隊列時:
DEPTH_PRIORITY = 1 # 廣度優先 DEPTH_PRIORITY = -1 # 深度優先
使用
# 1 鏈接redis配置 REDIS_HOST = '127.0.0.1' REDIS_PORT = '6379' REDIS_ENCODING = 'utf-8' # 2 去重的配置 DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' # 3 調度器的配置 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 調度器的其餘配置 SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue' # 默認使用優先級隊列(默認),其餘:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表) SCHEDULER_QUEUE_KEY = '%(spider)s:requests' # 調度器中請求存放在redis中的key SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat" # 對保存到redis中的數據進行序列化,默認使用pickle SCHEDULER_PERSIST = True # 是否在關閉時候保留原來的調度器隊列和去重記錄,True=保留,False=清空(默認) SCHEDULER_FLUSH_ON_START = True # 是否在開始以前清空 調度器隊列和去重記錄,True=清空,False=不清空(默認) SCHEDULER_IDLE_BEFORE_CLOSE = 2 # 去調度器隊列中獲取數據時,若是爲空,最多等待時間(最後沒數據,未獲取到)。 SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter' # 去重規則,在redis中保存時對應的key SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' # 去重規則對應處理的類
起始URL
a. 獲取起始URL時,去集合中獲取仍是去列表中獲取?True,集合;False,列表 REDIS_START_URLS_AS_SET = False # 獲取起始URL時,若是爲True,則使用self.server.spop;若是爲False,則使用self.server.lpop b. 編寫爬蟲時,起始URL從redis的Key中獲取 REDIS_START_URLS_KEY = '%(name)s:start_urls'