1. Scrapy框架 Scrapy功能非常強大,爬取效率高,相關擴展組件多,可配置和可擴展程度非常高,它幾乎可以應對所有反爬網站,是目前Python中使用最廣泛的爬蟲框架。 1.1 Scrapy介紹 1.1.1 架構介紹 Scrapy是一個基於Twisted的非同步處理框架,是純Python實現的爬 ...
1. Scrapy框架
Scrapy功能非常強大,爬取效率高,相關擴展組件多,可配置和可擴展程度非常高,它幾乎可以應對所有反爬網站,是目前Python中使用最廣泛的爬蟲框架。
1.1 Scrapy介紹
1.1.1 架構介紹
Scrapy是一個基於Twisted的非同步處理框架,是純Python實現的爬蟲框架,其架構清晰,模塊之間的耦合程度低,可擴展性極強,可以靈活完成各種需求。我們只需要定製開發幾個模塊就可以輕鬆實現一個爬蟲。
它可以分為如下的幾個部分:
Engine:引擎,處理整個系統的數據流處理、觸發事務,是整個框架的核心。
Item:項目,它定義了爬取結果的數據結構,爬取的數據會被賦值成該Item對象。
Scheduler:調度器,接受引擎發過來的請求並將其加入隊列中,在引擎再次請求的時候將請求提供給引擎。
Downloader:下載器,下載網頁內容,並將網頁內容返回給蜘蛛。
Spiders:蜘蛛,其內定義了爬取的邏輯和網頁的解析規則,它主要負責解析響應並生成提取結果和新的請求。
Item Pipeline:項目管道,負責處理由蜘蛛從網頁中抽取的項目,它的主要任務是清洗、驗證和存儲數據。
Downloader Middlewares:下載器中間件,位於引擎和下載器之 的鉤子框架,主要處理引擎與下載器之間的請求及響應。
Spider Middlewares:蜘蛛中間件,位於引擎和蜘蛛之間的鉤子框架,主要處理蜘蛛輸入的響應和輸出的結果及新的請求。
1.1.2 數據流
Scrapy中的數據流由引擎控制,數據流的過程如下:
(1) Engine首先打開一個網站,找到處理該網站的Spider,並向該Spider請求第一個要爬取的URL。
(2) Engine從Spider中獲取到第一個要爬取的URL,並通過Scheduler以Request的形式調度。
(3) Engine向Scheduler請求下一個要爬取的URL。
(4) Scheduler返回下一個要爬取的URL給Engine,Engine將URL通過Downloader Middlewares發給 Downloader下載。
(5) 一旦頁面下載完畢,Downloader生成該頁面的Response,並將其通過Downloader Middlewares發送給Engine。
(6) Engine從下載器中接收到Response,並將其通過Spider Middleware發送給Spider處理。
(7) Spider處理Response,並返回爬取到的Item及新的Request給Engine。
(8) Engine將Spider返回的Item給Item Pipeline,將新的Request給Scheduler。
(9)重覆第(2)步到第(8)步,直到Scheduler中沒有更多的Request,Engine關閉該網站,爬取結束。
通過多個組件的相互協作、不同組件完成工作的不同、組件對非同步處理的支持Scrapy最大限度地利用了網路帶寬,大大提高了數據爬取和處理的效率。
1.1.3 項目結構
Scrapy框架是通過命令行來創建項目的,代碼的編寫還是需要IDE。項目創建之後,項目文件的格式如下所示:
scrapy.cfg
project/
__init__.py
items.py
pipelines.py
settings.py
middlewares.py
spiders/
__init__.py
spider1.py
spider2.py
...
這裡各個文件的功能描述如下:
scrapy.cfg:它是Scrapy項目的配置文件,其內定義了項目的配置文件路徑、部署相關信息等內容
items.py:它定義Item數據結構,所有的Item的定義都可以放這裡。
pipelines.py:它定義Item Pipeline的實現,所有的Item Pipeline的實現都可以放這裡。
settings.py:它定義項目的全局配置。
middlewares.py:它定義Spider Middlewares和Downloader Middlewares的實現。
spiders:其內包含一個個Spider的實現,每個Spider都有一個文件。
1.2 Scrapy入門
接下來就寫一個簡單的項目,讓我們對Scrapy的基本用法和原理有大致的瞭解。
1.2.1 創建項目
首先安裝Scrapy模塊,再創建Scrapy項目。
pip install scrapy -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
創建Scrapy一個項目,項目文件可以直接用scrapy命令生成:
scrapy startproject tutorial
這個命令可以在任意文件夾運行。
這個命令將會創建一個名為tutoria文件夾,文件夾結構如下所示:
1.2.2 創建Spider
Spider是自己定義的類,Scrapy用它來從網頁里抓取內容,並解析抓取的結果。不過這個類必須繼承Scrapy提供的Spider類scrapy.Spider,還要定義Spider的名稱和起始請求,以及怎樣處理爬取後的結果的方法。
也可以使用命令行創建一個Spider。比如要生成top250這個Spider。
cd tutorial
scrapy genspider top250 movie.douban.com/250
進入剛纔創建的tutorial文件夾,然後執行genspider命令。第一個參數是Spider名稱,第二個參數是網站功能變數名稱。執行完畢之後,spiders文件夾中多了一個quotes.py,它就是剛剛創建的Spider。
這裡有三個屬性——name、allowed_domains和start_urls,還有一個方法parse。
name,它是每個項目唯一的名字,用來區分不同的Spider。
allowed_domains,它是允許爬取的功能變數名稱,如果初始或後續的請求鏈接不是這個功能變數名稱下的,則請求鏈接會被過濾掉。
start_urls,它包含了Spider在啟動時爬取的url列表,初始請求是由它來定義的。
parse,它是Spider一個方法。預設情況下,被調用時start_urls裡面的鏈接構成的請求完成下載執行後,返回的響應就會作為唯一的參數傳遞給這個函數。該方法負責解析返回的響應、提取數據或者進一步生成要處理的請求。
1.2.3 創建Item
Item是保存爬取數據的容器,它的使用方法和字典類似。不過,相比字典,Item多了額外的保護機制,可以避免拼寫錯誤或者定義欄位錯誤。
創建Item需要繼承scrapy.Item類,並且定義類型為scrapy.Field的欄位。觀察目標網站,我們可以獲取到到內容有serial_number、movie_name、introduce、star、evaluate、describe。
定義Item,此時修改items.py:
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://docs.scrapy.org/en/latest/topics/items.html import scrapy class DoubanItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() serial_number = scrapy.Field() movie_name = scrapy.Field() introduce = scrapy.Field() star = scrapy.Field() evaluate = scrapy.Field() describe = scrapy.Field()
這裡定義了三個欄位,接下來爬取時我們會使用到這個Item。
1.2.4 解析Response
前面已經說了,parse()方法的參數resposne是start_urls裡面的鏈接爬取後的結果。所以在parse()方法中,我們可以直接對response變數包含的內容進行解析,比如瀏覽請求結果的網頁源代碼,或者進一步分析源代碼內容,或者找出結果中的鏈接而得到下一個請求。我們可以看到網頁中既有我們想要的結果,又有下一頁的鏈接,這兩部分內容我們都要進行處理。
首先看看網頁結構:
每一頁都有多個class為item的區塊,每個區塊內都包含serial_number、movie_name、introduce、star、evaluate、describe那麼我們先找出所有的item,然後提取每一個item中的內容。
提取的方式可以是css選擇器或XPath選擇器,top250.py的XPath改寫如下:
# -*- coding: utf-8 -*- import scrapy class Top250Spider(scrapy.Spider): name = 'top250' allowed_domains = ['movie.douban.com'] start_urls = ['http://movie.douban.com/250/'] def parse(self, response): movie_list = response.xpath('//div[@class="item"]') for i_item in movie_list: serial_number = i_item.xpath('.//em/text()').extract_first() movie_name = i_item.xpath('.//div[@class="hd"]/a/span[1]/text()').extract_first() content = i_item.xpath('.//div[@class="bd"]/p[1]/text()').extract() for i_content in content: introduce = "".join(i_content.split()) star = i_item.xpath('.//span[@class="rating_num"]/text()').extract_first() evaluate = i_item.xpath('.//div[@class="star"]//span[4]/text()').extract_first() describe = i_item.xpath('.//p[@class="quote"]//span/text()').extract_first()
這裡首先利用選擇器選取所有的item,並將其賦值為movie_list變數,然後利用for迴圈對每個item遍歷,解析每個item的內容。
1.2.5 使用Item
前面定義了Item,接下來就要使用它了。Item可以理解為字典,不過在聲明的時候需要實例化。然後依次用剛纔解析的結果賦值Item的每一段,最後將Item返回即可。
修改top250.py文件:
# -*- coding: utf-8 -*- import scrapy class Top250Spider(scrapy.Spider): name = 'top250' allowed_domains = ['movie.douban.com'] start_urls = ['http://movie.douban.com/250/'] def parse(self, response): movie_list = response.xpath('//div[@class="item"]') for i_item in movie_list: douban_item = DoubanItem() douban_item['serial_number'] = i_item.xpath('.//em/text()').extract_first() douban_item['movie_name'] = i_item.xpath('.//div[@class="hd"]/a/span[1]/text()').extract_first() content = i_item.xpath('.//div[@class="bd"]/p[1]/text()').extract() for i_content in content: content_s = "".join(i_content.split()) douban_item['introduce'] = content_s douban_item['star'] = i_item.xpath('.//span[@class="rating_num"]/text()').extract_first() douban_item['evaluate'] = i_item.xpath('.//div[@class="star"]//span[4]/text()').extract_first() douban_item['describe'] = i_item.xpath('.//p[@class="quote"]//span/text()').extract_first() yield douban_item
這樣首頁的所有內容都可以被解析出來,並被賦值成一個個DoubanItem。
1.2.6 後續Request
前面的操作實現了從初始頁面抓取內容。那麼,下一頁的內容該如何抓取?這就需要我們從當前頁面中找到信息來生成下一個請求,然後在下一個請求的頁面里找到信息再構造再下一個請求。這樣 迴圈往複迭代,從而實現整站的爬取。
將頁面拉到最底部,如圖:
這裡有後頁按鈕。查看它的源代碼,可以發現它的鏈接是?start=25&filter=,完整鏈接是:https://movie.douban.com/top250?start=25&filter=,通過這個鏈接我們就可以構造下一個請求。
構造請求時需要用到scrapy.Request這裡我們傳遞兩個參數——url和callback。
Request參數:
url:它是請求鏈接。
callback:它是回調函數。當指定了該回調函數的請求完成之後,獲取到響應,引擎會將該響應作為參數傳遞給這個回調函數。回調函數進行解析或生成下一個請求,回調函數如上文parse()所示。
由於parse()就是解析serial_number、movie_name、introduce、star、evaluate、describe的方法,而下一頁的結構和剛纔已經解析的頁面結構是一樣的,所以我們可以再次使用parse()方法來做頁面解析。
接下來我們要做的就是利用選擇器得到下一頁鏈接並生成請求,在parse()方法後追加代碼:
# -*- coding: utf-8 -*- import scrapy from tutorial.items import DoubanItem class Top250Spider(scrapy.Spider): name = 'top250' allowed_domains = ['movie.douban.com'] start_urls = ['http://movie.douban.com/250/'] def parse(self, response): movie_list = response.xpath('//div[@class="item"]') for i_item in movie_list: douban_item = DoubanItem() douban_item['serial_number'] = i_item.xpath('.//em/text()').extract_first() douban_item['movie_name'] = i_item.xpath('.//div[@class="hd"]/a/span[1]/text()').extract_first() content = i_item.xpath('.//div[@class="bd"]/p[1]/text()').extract() for i_content in content: content_s = "".join(i_content.split()) douban_item['introduce'] = content_s douban_item['star'] = i_item.xpath('.//span[@class="rating_num"]/text()').extract_first() douban_item['evaluate'] = i_item.xpath('.//div[@class="star"]//span[4]/text()').extract_first() douban_item['describe'] = i_item.xpath('.//p[@class="quote"]//span/text()').extract_first() yield douban_item next_link = response.xpath('//span[@class="next"]/link/@href').extract() if next_link: next_link = next_link[0] yield scrapy.Request('https://movie.douban.com/top250' + next_link, callback=self.parse)
1.2.7 運行程式
進入目錄,運行如下命令:
scrapy crawl top250
信息刷的太快了,上面都刷沒了,只要顯示中沒有出現報錯,這樣就算成功了,大家可以自己運行一遍。
首先,Scrapy輸出了當前的版本號以及正在啟動的項目名稱。接著輸出了當前settings.py中一些重寫後的配置。然後輸出了當前所應用的Middlewares和Pipelines。Middlewares預設是啟用的,可以settings.py中修改。Pipelines 預設是空,同樣也可以在settings.py中配置。
settings.py:
# -*- coding: utf-8 -*- # Scrapy settings for tutorial project # # For simplicity, this file contains only settings considered important or # commonly used. You can find more settings consulting the documentation: # # https://docs.scrapy.org/en/latest/topics/settings.html # https://docs.scrapy.org/en/latest/topics/downloader-middleware.html # https://docs.scrapy.org/en/latest/topics/spider-middleware.html BOT_NAME = 'tutorial' SPIDER_MODULES = ['tutorial.spiders'] NEWSPIDER_MODULE = 'tutorial.spiders' # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'tutorial (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36' # Obey robots.txt rules # ROBOTSTXT_OBEY = True ROBOTSTXT_OBEY = False # Configure maximum concurrent requests performed by Scrapy (default: 16) #CONCURRENT_REQUESTS = 32 # Configure a delay for requests for the same website (default: 0) # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs #DOWNLOAD_DELAY = 3 DOWNLOAD_DELAY = 0.5 # The download delay setting will honor only one of: #CONCURRENT_REQUESTS_PER_DOMAIN = 16 #CONCURRENT_REQUESTS_PER_IP = 16 # Disable cookies (enabled by default) #COOKIES_ENABLED = False # Disable Telnet Console (enabled by default) #TELNETCONSOLE_ENABLED = False # Override the default request headers: #DEFAULT_REQUEST_HEADERS = { # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', # 'Accept-Language': 'en', #} # Enable or disable spider middlewares # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html #SPIDER_MIDDLEWARES = { # 'tutorial.middlewares.TutorialSpiderMiddleware': 543, #} # Enable or disable downloader middlewares # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html #DOWNLOADER_MIDDLEWARES = { # 'tutorial.middlewares.TutorialDownloaderMiddleware': 543, #} # Enable or disable extensions # See https://docs.scrapy.org/en/latest/topics/extensions.html #EXTENSIONS = { # 'scrapy.extensions.telnet.TelnetConsole': None, #} # Configure item pipelines # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { 'tutorial.pipelines.TutorialPipeline': 300, } # Enable and configure the AutoThrottle extension (disabled by default) # See https://docs.scrapy.org/en/latest/topics/autothrottle.html #AUTOTHROTTLE_ENABLED = True # The initial download delay #AUTOTHROTTLE_START_DELAY = 5 # The maximum download delay to be set in case of high latencies #AUTOTHROTTLE_MAX_DELAY = 60 # The average number of requests Scrapy should be sending in parallel to # each remote server #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # Enable showing throttling stats for every response received: #AUTOTHROTTLE_DEBUG = False # Enable and configure HTTP caching (disabled by default) # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings #HTTPCACHE_ENABLED = True #HTTPCACHE_EXPIRATION_SECS = 0 #HTTPCACHE_DIR = 'httpcache' #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
接下來就是輸出各個頁面的抓取結果了,可以看到爬蟲一邊解析,一邊翻頁,直至將所有內容取完畢,然後終止。
最後,Scrapy輸出了整個抓取過程的統計信息,如請求的位元組數、請求次數、響應次數、完成原因等。
整個Scrapy程式成功運行。我們通過非常簡單的代碼就完成了一個網站內容的爬取,這樣相比之前一步一步寫程式簡潔很多。
1.2.8 保存到文件
運行完Scrapy後,我們只在控制台看到了輸出結果。如果想保存結果該怎麼辦呢?
要完成這個任務其實不需要任何額外的代碼,Scrapy提供的Feed Exports可以輕鬆將抓取結果輸出。例如,我們想將上面的結果保存成JSON文件,可以執行如下命令:
scrapy crawl top250 -o top250.json
命令運行後,項目內多了一個quotes.json文件(沒有就刷新),文件包含了剛纔抓取的所有內容,內容是JSON格式。
另外我們還可以每一個Item輸出一行JSON,輸出尾碼為jl,為jsonline的縮寫:
scrapy crawl top250 -o top250.jl
或
scrapy crawl top250 -o top250.jsonlines
輸出格式還支持很多種,例如csv、xml、pickle、marshal等,還支持ftp、s3等遠程輸出,另外還可以通過自定義ItemExporter來實現其他的輸出。
例如,下麵命令對應的輸出分別為csv、xml、pickle、marshal格式以及ftp遠程輸出:
scrapy crawl top250 -o top250.csv
scrapy crawl top250 -o top250.xml
scrapy crawl top250 -o top250.pickle
scrapy crawl top250 -o top250.marshal
scrapy crawl top250 -o ftp://user:[email protected]/path/to/top250.csv
其中,ftp輸出需要正確配置用戶名、密碼、地址、輸出路徑,否則會報錯。
通過Scrapy提供的Feed Exports,我們可以輕鬆地輸出抓取結果到文件。對於一些小型項目來說, 這應該足夠了。不過如果想要更複雜的輸出,如輸出到資料庫等,我們可以使用Item Pileline來完成。