scrapy

- Scrap Engine(引擎)
負責控制數據流在系統中所有組件中流動,並在相應動作發生時觸發事件,是整個爬蟲的調度中心。
- 調度器( Scheduler)
調度器接收從引擎發送過來的 request,並將他們加入到爬取隊列,以便之后引擎請求他們時提供給引擎。初始的爬取URL和后續在頁面中獲取的待爬取的URL將放入調度器中,等待引擎得統一調度爬取。同時調度器會自動去除重復的URL(如果特定的URL不需要去重也可以通過設置實現,如ρost請求的URL)
- 下載器( Downloader)
下載器負責獲取頁面數據並提供給引擎,而后將獲取得response信息提供給 spider。
- Spiders爬蟲
Spider是編寫的類,作用如下:
- 編寫用於分析 response並提取item即獲取到的item)
- 分析頁面中得url,提交給 Scheduler調度器繼續爬取。
由於網站頁面內容結構不同,一個spider一般負責處理一個(或一些)特定的網站。多個網站可以使用多個spider分別進行爬取。
- Item pipeline
頁面中餓內容被提取出來封裝到一個數據結構中,即一個item,每一個item被發送到項目管道( Pipeline),並經過設置好次序的pipeline程序處理這些數據,最后將存入本地文件或存入數據庫持久化。
item pipeline的一些典型應用
- 處理HTML數據
- 驗證爬取的數據(檢查item包含某些字段)
- 查重(或丟棄)
- 將爬取結果保存到數據庫中
- 下載器中間件(Downloader middlewares)
下載器中間件是在引擎和下載器之間的特定鈎子(specific hook),在下載進行下載前,以及下載完成返回數據的階段進行攔截,處理請求和響應。它提供了一個簡便的機制,通過插入自定義代碼來擴展 Scrapy功能,通過設置下載器中間件可以實現爬蟲自動更換 user-agent、實現IP代理功能等功能。
- Spider中間件( Spider middlewares)
Spider中間件,是在引擎和 Spider之間的特定鈎子,處理 spider的輸入response和輸出(items或 requests)
安裝
scrapy使用Twisted基於事件的高效異步網絡框架來處理網絡通信,可以加快下載速度,使用pip安裝scrapy時會自動解決安裝依賴,在windows下如果安裝Twisted出現問題,手動下載編譯好的Twisted包安裝即可。
- 安裝wheel支持:pip install wheel
- 安裝scrapy框架:pip install scrapy
基本使用
創建項目
scrapy提供了命令快速的創建一個項目,自動生成項目框架
scrapy startproject <pro_name> . # 在當前目錄創建項目
創建后項目目錄如下:
pro_name/ # 項目目錄 scrapy.cfg # 必要的配置文件 pro_name/ # 項目全局目錄 spiders/ # 爬蟲類 __init__.py __init__.py items.py # item,定義數據結構儲存數據 middlewares.py # 中間鍵類 pipelines.py # 管道 setting.py # 全局重要的設置 setting中的設置 BOT_NAME = 'spider_name' # 爬蟲名 SPIDER_MODULES = ['pro_name.spiders'] # 爬蟲模塊 USER_AGENT # UA,配置 ROBOTSTXT_OBEY = False # 是否遵從robots協議 # CONCURRENT_REQUESTS = 32 # 最大請求數,默認16 # COOKIES_ENABLED = False # 一般登錄時候使用cookie # DEFAULT_REQUEST_HEADERS = { # 可定義請求頭 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en', } # 兩個中間件,指定為模塊中的類,將會自動按照優先級依次調用 # SPIDER_MIDDLEWARES = { # 中間鍵類們,key為模塊位置,value為優先級,越小越優先 'pro_name.middlewares.Pro_nameSpiderMiddleware': 543, } # DOWNLOADER_MIDDLEWARES = { 'pro_name.middlewares.Pro_nameDownloaderMiddleware': 543, } ITEM_PIPELINES = { # 管道處理,指定處理類,按照順序調用 'wuhan.pipelines.PeoplePipeline': 300, }
Item
item可以看作是一個儲存數據的數據結構, 定義我們需要爬取內容的數據字段用於儲存數據並將每一條數據封裝為一個Item對象,再由scrapy核心交由pipelines依次處理即可。假如我們需要爬取的每一條招聘內容,包含了公司名,工作地點,薪資等字段數據,我們可以定義一個數據結構Item去儲存這些數據
import scrapy class workItem(scrapy.Item): cp_name = scrapy.Field() # class scrapy.Field(dict): 繼承於dict cp_addr = scrapy.Field() salary = scrapy.Field()
該類繼承scrapy中的內部類,定義的屬性值為一個scrapy.Field()實例,實際為一個字典對象。
爬蟲類
爬蟲類中分為兩部分工作,提供想要爬取的url和解析返回的response信息。scrapy提供了模塊快速的創建一個模板。scrapy genspider -t basic name domain.com
-t指定模板的類型為basic,name為創建的py文件名,執行后將會在spider目錄中生成文件name.py文件,文件內容如下。
# -*- coding: utf-8 -*- import scrapy class DomainSpider(scrapy.Spider): name = 'spider_name' # 爬蟲名 allowed_domains = ['domain.com'] # 爬 取的內容必須在這些域下,否則爬取 start_urls = ['http://domain.com/'] # 起始的url # 下載器下載了目標頁面的內容,封裝為response對象並注入到response參數中, def parse(self, response): # 返回的response值,類型為scrapy.http.response.html.HtmlResponse pass # 使用參數注解方便了解該對象的方法
parse方法中負責對response中的內容進行數據處理,一般包括數據數據提取和url提取,數據使用Item進行封裝,url繼續交由調度器進行再次繼續進行訪問。
解析response
返回的數據被scrapy封裝為一個HtmlResponse對象,他是一個response對象的一個子類。
def parse(self, response): print(response.text, response.body, response.headers) # 文本內容和頭信息
為了方便頁面內容的提取,scrapy包裝了lxml,並可以通過調用response的方法直接調用,並通過xpath語法進行提取。直接調用xpath方法即可
def parse(self, response): tag = response.xpath("//xpath") # tag為提取html的標簽封裝的xpath對象,可以繼續調用xpath
同樣還支持css選擇器,調用css方法即可
def parse(self, response): css_tag = respons.css("li a::text")
Item封裝數據
from ..items import WorkItem # 使用相對導入 def parse(self, response): # 使用xpath 或者 css選擇器提取到了內容 cp_name = response.xpath("//cp_name").extract() cp_addr = response.xpath("//cp_addr").extract() salary = response.xpath("//salary").extract() # 實例化item對象 item = WorkItem item["cp_name"] = cp_name # 將數據對應封裝到item對象屬性中。 item["cp_addr"] = cp_addr item["salary"] = salary return [item] # return值將會交給pipeline處理,需要一個可迭代對象 # yield item # 或者使用yield每次返回一個item,返回一個可迭代對象即可 # 該數據也可以通過命令直接保存到指定的文件中 # 命令行執行 scrapy crwal -h 查看幫助 # --output 或者 -o "path/to/" 存入指定的文件 # 例如 scrapy crawl -o "tmp/data/work.json" # 支持的文件格(json,csv, xml, marshal, pickle)
pipeline
每一個pipeline會依次處理返回的每一個item數據,多個pipeline同時存在可以設置優先級。使用pipeline只需要將對應pipeline類在setting列表中注冊並指定優先級即可,scrapy將會依次調用這些pipeline工作。
在項目文件中有pipeline.py文件,在內部可根據需要定義pipeline。定義完成必須在setting.py的pipelines列表中聲明即可。
- 開啟並添加pipeline
ITEM_PIPELINES = {'pro_name.pipelines.TestPipeline':500} # 指定這個pipeline的模塊位置進行注冊
- 自定義pipeline
每一個pipeline類可以定以下方法,如果指定,scrapy將在對應的時刻調用這些方法執行啊。
class AaaPipeline(object): def __init__(self): # pipeline創建時候調用一次 print("init +++++++++++++++") def open_spider(self, spider): print("open_spider ++++++++++++++++++") # spider開啟時調用一次 print(spider, type(spider)) # 該spider對象即為spider文件中的DomainSpider對象 return "open ====" def process_item(self, item, spider): # 必須定義該方法 # 處理一條條item的方法 # spider類中的parse方法每返回一條item數據,該方法調用一次。 print(item) print(spider, type(spider)) return item def close_spider(self, spider): # spider關閉時候調用 print("close_spider ++++++++++++++++++") print(spider, type(spider)) return "close ===="
process_item方法負責對每個item對象進行處理,可以將數據保存存到磁盤或者數據庫中,open_spider和close_spider方法通常用於創建和關閉數據庫連接或者文件對象等操作。
url提取
我們需要從頁面中提取下一頁的url,再次進行爬取,在之前的parse函數中完成這個任務。
import scrapy class Spider(scrapy.Spider): name = "domain" start_utl = "" allowed_domain = [] def parse(self, response): urls = response.xpath("//div[@class='paginator']/span[@class='next']/a/@href") .re(r'start=/d+') # 匹配url使用正則過濾 # 將每個url拼接為為全路徑,並封裝為Request對象返回 yield from (scrapy.Request(response.urljoin(url)) for url in urls)
提取的url只需要將其封裝為scrapy.Request對象進行返回,scrapy將會將其作為請求交給調度器,並由下載器再次發送請求。調度器對url自動有去重功能,重復的url提交給調度器,將會自動去重。
多頁面定向爬取
如果該網站有多個網頁需要進行爬取,但是頁面的內容結構差異較大,這樣一個parse函數將不能夠很好的解析統一的解析。我們需要對頁面進行分類。通過url的差異,使得不同類的內容執行不同解析parse函數,各自完成解析。
實現這個功能,scrapy提供了另一個模板-crawl,如同basic模板的創建方式
scrapy genspider -t crawl name domian.com
進行創建,創建后內容如下:
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class $classname(CrawlSpider): # CrawlSpider是Spider的子類 name = '$name' allowed_domains = ['$domain'] start_urls = ['http://$domain/'] # 定義了一個規則,從response頁面中提取指定的鏈接,鏈接可以匹配allow="Items/",將會爬取鏈接頁面執行會執行callback函數 # rules = ( Rule(LinkExtractor(allow=r'Items/',), callback='parse_item', follow=True), 。。。 ) def parse_item(self, response): item = {} #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get() #item['name'] = response.xpath('//div[@id="name"]').get() #item['description'] = response.xpath('//div[@id="description"]').get() return item
scrapy.spider.crawl.CrawlSpider是scrapy.spider.Spider的子類,增強了功能,在其中可以使用LinkExtractor,Rule
Rule的規則
- rules元組中可以定義多條規則,每一條規則映射一個callback用於處理
- LinkExtractor對象用於過濾url,其中參數包括:
- allow參數傳入一個正則字符串或者多個正則字符串組成可迭代對象,該正則字符串只會匹配頁面的中的
<a>
中的href
屬性的值。將會提取頁面中所有的href
進行匹配。 - deny:於allow相反,匹配的拒絕訪問
- allow_domain:允許域
- deny_domain:拒絕域
- follow:是否繼續跟進鏈接。
爬取執行過程
執行爬蟲開始爬取過程,從打印的日志中可以看到相關的信息。scrapy list
可以列出當前存在的爬蟲名,使用scrapy crwal spider_name
開始爬取
c:\\users\username\python\aaa> scrapy crawl spider_name # 執行命令,開始scrapy進行爬取 2020-03-31 11:31:32 [scrapy.utils.log] INFO: Scrapy 1.8.0 started (bot: aaa) # 加載的庫 2020-03-31 11:31:32 [scrapy.utils.log] INFO: Versions: lxml 4.4.2.0, libxml2 2.9.5, csssel ect 1.1.0, parsel 1.5.2, w3lib 1.21.0, Twisted 19.10.0, Python 3.6.6 (v3.6.6:4cf1f54eb7, J un 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)], pyOpenSSL 19.1.0 (OpenSSL 1.1.1d 10 Se p 2019), cryptography 2.8, Platform Windows-10-10.0.18362-SP0 # 爬蟲的初始化配置值,例如user_agent信息。 2020-03-31 11:31:32 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'aaa', 'NEWSP IDER_MODULE': 'aaa.spiders', 'SPIDER_MODULES': ['aaa.spiders'], 'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safar i/537.36'} 2020-03-31 11:31:32 [scrapy.extensions.telnet] INFO: Telnet Password: 8e9ba500a3d20796 # 中間件信息,這里沒有使用,加載了默認的幾個中間件 2020-03-31 11:31:32 [scrapy.middleware] INFO: Enabled extensions: ['scrapy.extensions.corestats.CoreStats', 'scrapy.extensions.telnet.TelnetConsole', 'scrapy.extensions.logstats.LogStats'] # 下載器中間件downloadermiddlewares信息 2020-03-31 11:31:32 [scrapy.middleware] INFO: Enabled downloader middlewares: ['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware', 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware', 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware', 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware', 'scrapy.downloadermiddlewares.retry.RetryMiddleware', 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware', 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware', 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware', 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware', 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware', 'scrapy.downloadermiddlewares.stats.DownloaderStats'] # spidermiddlewares 2020-03-31 11:31:32 [scrapy.middleware] INFO: Enabled spider middlewares: ['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware', 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware', 'scrapy.spidermiddlewares.referer.RefererMiddleware', 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware', 'scrapy.spidermiddlewares.depth.DepthMiddleware'] # 至此,配置信息和中間件信息加載完畢,開始執行爬取內容 # pipeline 初始化,執行了__init__方法,這是我們自己打印的內容 init +++++++++++++++ 2020-03-31 11:31:32 [scrapy.middleware] INFO: Enabled item pipelines: ['aaa.pipelines.AaaPipeline'] # pipeline中open_spider方法執行,spider對象已被創建,即parse方法所屬spider類 2020-03-31 11:31:32 [scrapy.core.engine] INFO: Spider opened open_spider ++++++++++++++++++ <TestspiserSpider 'testspider' at 0x1e9d2784198> <class 'aaa.spiders.testspiser.Testspiser Spider'> 2020-03-31 11:31:32 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), s craped 0 items (at 0 items/min) 2020-03-31 11:31:32 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1 :6023 2020-03-31 11:31:32 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <G ET http://www.douban.com/> from <GET http://douban.com/> 2020-03-31 11:31:32 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <G ET https://www.douban.com/> from <GET http://www.douban.com/> 2020-03-31 11:31:33 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.douban.com/ > (referer: None) # parse解析方法執行 200 ================== # process_item方法執行。 {'age': 123, 'name': 'abc'} <TestspiserSpider 'testspider' at 0x1e9d2784198> <class 'aaa.spiders.testspiser.Testspiser Spider'> 2020-03-31 11:31:33 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.douban.com/ > {'age': 123, 'name': 'abc'} # 關閉爬蟲,執行pipeline中的close_spider方法 2020-03-31 11:31:33 [scrapy.core.engine] INFO: Closing spider (finished) close_spider ++++++++++++++++++ <TestspiserSpider 'testspider' at 0x1e9d2784198> <class 'aaa.spiders.testspiser.Testspiser Spider'> # 本次爬取的執行總結信息 2020-03-31 11:31:33 [scrapy.statscollectors] INFO: Dumping Scrapy stats: {'downloader/request_bytes': 860, 'downloader/request_count': 3, 'downloader/request_method_count/GET': 3, 'downloader/response_bytes': 19218, 'downloader/response_count': 3, 'downloader/response_status_count/200': 1, 'downloader/response_status_count/301': 2, 'elapsed_time_seconds': 0.727021, 'finish_reason': 'finished', 'finish_time': datetime.datetime(2020, 3, 31, 3, 31, 33, 388677), 'item_scraped_count': 1, 'log_count/DEBUG': 4, 'log_count/INFO': 10, 'response_received_count': 1, 'scheduler/dequeued': 3, 'scheduler/dequeued/memory': 3, 'scheduler/enqueued': 3, 'scheduler/enqueued/memory': 3, 'start_time': datetime.datetime(2020, 3, 31, 3, 31, 32, 661656)} 2020-03-31 11:31:33 [scrapy.core.engine] INFO: Spider closed (finished)
middleware
從文章開頭的scrapy建構圖中可以看到,scrapy提供了兩個中間件,分別是SpiderMiddleware和DownloaderMiddleware,在setting.py文件中,也有對應的中間件變量
SPIDER_MIDDLEWARES = { 'name.middlewares.NameSpiderMiddleware': 543, } DOWNLOADER_MIDDLEWARES = { 'name.middlewares.NameDownloaderMiddleware': 543, }
以上配置文件中的中間件是我們自定義中間件,但是通過上面的日志文件可以看到還加載許多內置的中間件,這些中間件在固定的被加載,並按照設置的優先級順序依次調用。中間件的作用主要是為框架提供可擴展性,讓用戶能夠更好的根據自身需求在程序適當的位置添加處理程序。
如何添加自己的中間件
在scrapy生成的框架中,可以找到一個middleware.py文件,內部定義了兩個類,並有部分初始信息,但未實現任何功能 :
# SpiderMiddleware class AaaSpiderMiddleware(object): # 不是所有的方法必須定義,如果沒有定義將跳過執行 @classmethod def from_crawler(cls, crawler): # 創建spider時調用該方法 s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_spider_input(self, response, spider): # 數據流入spider模塊時調用,也就是下載的response信息流入spider,在經過parse函數解析前 # 返回值應為None或者報錯 return None def process_spider_output(self, response, result, spider): # 從spider輸出的數據將經過該方法的處理,包括兩個操作。 # - 向item pipeline中注入數據時 # - 向scheduler提交的新的請求對象時 # 必須返回一個可迭代對象,字典,或者Item對象。 for i in result: yield i def process_spider_exception(self, response, exception, spider): # 當spider或者process_spider_input方法出現了異常時調用 # 返回None或者 an iterable of Request, dict or Item objects. pass def process_start_requests(self, start_requests, spider): # 第一次對入口url發起請求時會調用該方法。 for r in start_requests: yield r def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name) # 打印的日志信息 # DownloaderMiddleware class AaaDownloaderMiddleware(object): # 從架構圖中的位置可以看出DownloaderMiddleware的只能處理兩個數據流, # Request請求和Response響應,分別對應了兩個方法,process_request和process_response @classmethod def from_crawler(cls, crawler): s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) return s def process_request(self, request: scrapy.http.request.Request, spider): # 返回值必須為:None 或者 Response對象或者Request對象。 # 或者拋出IgnoreRequest錯誤,再由process_exception處理 return None def process_response(self, request, response, spider): # 返回值同上 return response def process_exception(self, request, exception, spider): # 處理上面兩個方法的錯誤 # 返回值: # - return None: 繼續釋放錯誤 # - return Response對象或者Request對象: 壓制錯誤 pass def spider_opened(self, spider): spider.logger.info('Spider opened: %s' % spider.name)
這兩個中間件是scrapy提供的模板,自定義中間件只需要按照上面模板定義一個類,指定對應的方法即可。如果有多個中間件可以進行分別定義,每個完成各自的功能。最后在setting.py中的中間件列表中添加該類(需要從根目錄開始執行模塊位置),並指定該類優先級值即可,值越大,優先級越低。
添加代理
如果使用同一個ip地址快速進行爬取,容易被服務端的反爬蟲機制檢測到,該ip地址將會短暫的,將導致無法訪問該網站。為了避免非人類的快速操作,我們可以將setting文件中的DOWNLOAD_DELAY = 3
設置為一個合適的值,每次爬取后,將會間隔指定時間再次爬取內容,將會降低被封ip的風險,但是這樣會嚴重影響爬取速度。更好的辦法就是使用ip代理的方式。
ip代理是通過中間的代理服務器A訪問目標網站,再由服務器A將信息發送給我們,服務器A長時間快速訪問任然會導致A的IP地址被禁用,這就需要一個含有大量ip的代理池,每次隨機選取進行訪問,然后將所有的數據集中到我們自己的機器上。由於使用的ip眾多,這樣單個ip連續出現頻率將會降低,不會被認定為是爬蟲的操作。這樣就可以在短暫的時間內大量的爬取數據而不被發現。ip代理也是常用的反反爬手段之一。
添加代理一般我們使用單獨的一個middleware
class ProxyDownloaderMiddleware: proxies = [ "http://120.83.105.247:9999", ] def process_request(self, request, spider): if self.proxies: request.meta["proxy"] = random.choices(self.proxies) print(request.url, request.meta["proxy"], "===================================")
代理是在發送Request之前更換代理信息,所以在process_request方法中提前在request.meta屬性中添加proxy的值,實現代理功能。
最后在setting.py中啟用這個中間件模塊,使其能夠被調用實現代理功能。
數據存入MongoDB
我們最終提取到數據將會分裝到一個個Item中,交給pipeline依次處理,例如可以將數據存入MongoDB數據庫中來保存這些數據。
將數據存入MongoDB中,需要提供對應pipeline處理來自spider封裝好的item信息,我們可以將寫入mongo的操作看作一個獨立的功能,使用單獨一個pipeline完成。並且通過提供不同的MongoDB連接參數,包括服務主機地址,端口,庫名等信息,來方便連接不同mongo服務。為了方便的配置這些信息,可以將這些數據寫入配置文件中setting.py中,以方便對其進行修改,配置文件中的內容將會綁定到該項目中所有的spider中,如果有多個spider需要保存到同一個mongo服務中,可以配置到setting.py中配置保障配置信息共享。如果是spider獨有的,可以單獨在該spider類中定義這些屬性,spider會優先讀取類中定義的屬性,如果沒有,再去配置文件中查找。
import pymongo from pymongo.collection import Collection class ReviewsPipeline(object): # 創建spider時創建該mongo客戶端,該方法只會在項目啟動時執行一次 def open_spider(self, spider): # 從settting讀取配置信息,如果spider有settings屬性,將優先讀取自己的settings屬性 # 而不是全局配置中settings屬性。 mongo_host = spider.settings.get("MONGO_HOST", "localhost") mongo_port = spider.settings.get("MONGO_PORT", 27017) mongo_collection = spider.settings.get("MONGO_COLLECTION", "default-col") db_name = spider.settings.get("MONGO_DB", "default-db") try: self.client = pymongo.MongoClient(host=mongo_host, port=mongo_port) self.collection:Collection = self.client[mongo_collection] self.db = self.collection[db_name] except Exception as e: print("connect mongodb error: ", e) raise def process_item(self, item, spider): try: self.db.insert_one(dict(item)) except Exception as e: logging.error(e) # 插入數據失敗記錄日志信息 return item def close_spider(self, spider): # 關閉 self.client.close()
同樣的,需要使用該pipeline,需要在setting文件的PIPELINES中添加該pipeline並指定優先級信息。