文章目錄
1. 前言
scrapy的體系架構如下:
在Scrapy中數據流由 execution engine
控制, 過程如下:
Engine
從Spider
中獲取到爬取的初始請求;Engine
在Scheduler
中調度請求,並請求下一個要爬取的請求。Scheduler
返回下一個請求給Engine
。Engine
發送請求給Downloader
, 需要經過Downloader Middlewares
(見process_request()
).- 一旦頁面下載完成,
Downloader
會生成該頁面的響應,並將響應發送給Engine
, 需要經過Downloader Middlewares
(見process_response()
). Engine
從Downloader
接收到響應,並將響應發送給Spider
處理,需要經過Spider Middleware
(見process_spider_input()
).Spider
處理響應並返回item
和需要跟蹤的新請求給Engine
, 需要經過Spider Middleware
(見process_spider_output()
).Engine
發送處理過的item
給Item Pipelines
, 然後將已處理的請求發送到調度程序,並要求進行爬取的下一個請求。- 從第一步開始重複,直到
Scheduler
中沒有請求了。
1.1 什麼是Scrapy?
Scrapy
是一個應用框架,常用於爬取網站
和抽取結構化數據
。
1.2 scrapy、BS 和 lxml
BeautifulSoup
是解析 HTML
的庫。在Python程序員中是一個非常流行的Web抓取庫,它基於HTML代碼的結構構造了一個Python對象,並且能夠很好地處理錯誤的標記,但是它有一個缺點:速度慢
。
lxml
是一個XML
解析庫(它也解析HTML
),使用基於 ElementTree
. (LXML
不是Python
標準庫。)
scrapy
是爬蟲應用框架
1.3 爬取順序
默認的爬取順序是DFS
,因爲默認使用棧
來存放爬蟲請求。
1.4 需要解析的數據過大怎麼辦?
在使用XPath
選擇器解析HTML
數據時,需要在內存中構建整個HTML
的DOM
,這可能會導致解析很慢並且會佔用大量內存。
因此,爲了避免一次在內存中解析所有的HTML
數據,我們可以使用 scrapy.utils.iterators
模塊中的 xmliter
和 csviter
。
1.5 部署 Scrapy 爬蟲
有兩種部署方式:
Scrapyd (open source)
Scrapy Cloud (cloud-based)
1.6 Scrapy的依賴
Scrapy
是純python
寫的,它依賴於如下幾個關鍵的python
包。
lxml
, 高效的XML
和HTML
解析器parsel
, 是在lxml
之上編寫的HTML
/XML
數據提取庫w3lib
, 用於處理URL
和網頁編碼的多功能幫助器twisted
, 異步網絡框架cryptography
andpyOpenSSL
, 處理各種網絡級安全需求
2. 創建Scrapy項目
在當前目錄下,創建一個名爲 recruitment
的爬蟲項目。
scrapy startproject recruitment 項目目錄(不寫,則在當前目錄創建該項目)
recruitment
中的目錄結構如下:
recruitment/
scrapy.cfg # deploy configuration file
recruitment/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items definition file
middlewares.py # project middlewares file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
爬蟲的主要邏輯代碼寫在spiders
目錄中。比如,定義最初的爬取網站的url
,選擇如何遵循頁面中url
,如何解析下載的頁面,如何提取數據等。
import scrapy
class RecruitmentSpider(spcrapy.Spider):
# spider的標識,在一個項目中,此name必須是唯一的。在同一個項目不能爲兩個不同spider取相同的name。
name = "recruitment"
def start_requests(self):
"""
返回請求的可迭代對象,標識spider從哪些url開始爬取。後序的請求都是由此請求產生。
"""
urls = [
'https://www.zhipin.com/',
'https://www.lagou.com/',
'https://landing.zhaopin.com/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
"""
response參數必須是 TextResponse的實例對象,
解析每一個URL響應、抽取數據、發現新的需爬取的URL。
"""
page = response.url.split("/")[-2]
filename = 'recruitment-{}.html'.format(page)
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file {}'.format(filename))
3. 如何運行spider
在項目目錄下運行如下命令,這裏 recruitment
是上面定義的spider的name
。
scrapy crawl recruitment
終端中會有一些輸出信息。
start_requests
方法的返回值是 scrapy.Request
對象。
start_requests()
方法的快捷方式。很多時候可以不用我們來實現start_requests()
方法,而是直接在start_urls
這個類屬性中定義需要爬取的URL
,scrapy
會使用自己start_requests()
方法的默認實現。
所以上面的代碼可以簡化一下:
import scrapy
class RecruitmentSpider(scrapy.Spider):
# spider的標識,在一個項目中,此name必須是唯一的。在同一個項目不能爲兩個不同spider取相同的name。
name = "recruitment"
start_urls = [
'https://www.zhipin.com/',
'https://www.lagou.com/',
'https://landing.zhaopin.com/',
]
def parse(self, response):
"""
response參數必須是 TextResponse的實例對象,
解析每一個URL響應、抽取數據、發現新的需爬取的URL。
"""
page = response.url.split("/")[-2]
filename = 'recruitment-{}.html'.format(page)
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file {}'.format(filename))
4. Scrapy中的基礎概念
4.1 命令行工具
命令行工具控制Scrapy
。
Scrapy
從如下文件中查找配置:
/etc/scrapy.cfg
或c:\scrapy\scrapy.cfg
(系統範圍的配置)~/.config/scrapy.cfg
($XDG_CONFIG_HOME
) 和~/.scrapy.cfg
($HOME
) ,這是全局配置(用戶範圍的配置)- 項目根目錄下的
scrapy.cfg
。(項目範圍的配置)
這些文件中的設置將按照優先順序進行合併。從上到下,優先級越來越高。
一個項目的根目錄(包含scrapy.cfg
的目錄)可以被多個Scrapy項目共享,每個Scrapy項目都有自己的設置模塊。
# 創建一個項目
scrapy startproject myproject 項目目錄
cd myproject
# 查看help
scrapy -h
scarpy 命令 -h
4.2 spiders
主要的爬取邏輯在這裏。怎麼爬,怎麼提取數據等等。
抓取週期如下:
- 首先,生成對爬取第一個
URL
的初始請求,然後指定一個回調函數,該函數在調用時會使用從這些請求下載的響應。 要執行的第一個請求是通過調用start_requests()
(默認)方法 生成的對應start_urls
的Request
,然後將parse()
方法作爲Request
的回調函數。 - 在回調函數中,解析
response
並返回Item
對象,或Request
對象,或者是它們的可迭代對象。這些Requests
還將包含回調函數(可以相同,可以不同),在scrapy下載之後,會由指定的回調函數來處理該request
對應的response
。 - 在回調函數中,通常使用
選擇器
(也可以使用beautifulsoup
、lxml
或任何喜歡的機制)來解析頁面內容,並使用解析的數據生成item
。 - 最後,從
spider
返回的項目通常被持久化到數據庫(通過Item Pipeline
)或者使用Feed 導出
寫入文件中。
內置了幾個通用spider
類:
CrawlSpider
。這是最常用的爬行常規網站的蜘蛛,因爲它通過定義一組規則爲跟蹤鏈接提供了一種方便的機制。XMLFeedSpider
。CSVFeedSpider
。SitemapSpider
。
4.3 選擇器
使用選擇器從頁面中提取數據。
Scrapy選擇器支持如下:
CSS選擇器
。其實,CSS選擇器
也是在後臺轉換爲XPath
的XPath表達式
。是Scrapy
選擇器的基礎,很強大。
scrapy
使用自己的機制來提取數據,這就是scrapy
的selector
。在scrapy中要提取HTML
中的數據,可以使用3種方式:
response.selector.css()
或response.selector.xpath()
response.css()
。::text
表示提取該元素的文本內容,::attr(name)
表示提取該元素的某屬性值。response.xpath()
他們的返回值都是SelectorList
的實例對象,對此實例對象調用get()
或.getall()
方法,分別獲得第一個符合條件的元素,和所有符合條件的元素的list。使用.attrib
屬性可以獲取提取的元素中的某屬性值。
嵌套調用
由於.xpath()
和.css()
的返回值都是selector
,所以還可以繼續調用.xpath()
和.css()
方法。
elem_div = response.xpath("//div")
elem_a = elem_div.xpath(".//a").getall()
選擇元素屬性
從HTML元素中提取元素屬性的值
,有如下3種方法。
response.xpath("//a/@href").getall()
response.css("a::attr(href)").getall()
[a.attrib['href'] for a in reponse.css('a')]
使用正則表達式
Selector還可以使用.re()
方法來提取數據。返回值是unicode
字符串的list。 .re_first()
提取滿足正則表達式的第一個結果。
.get()
的別名是.extract_first()
.getall()
的別名是.extract()
因爲使用前者更清晰明瞭,所以推薦使用前者。
XPaths
在使用.xpath()
的時候,/
開頭表示是絕對的xpath路徑。
./
開頭表示相對的xpath路徑。
>>> divs = response.xpath('//div')
>>> for p in divs.xpath('//p'): # this is wrong - gets all <p> from the whole document
... print(p.get())
# correct way
>>> for p in divs.xpath('.//p'): # extracts all <p> inside
... print(p.get())
按照 class
來查詢時,使用.css()
是更好的選擇。
# class名爲 shout 的 <a>標籤的href屬性
>>> response.css('.shout').xpath('./a/@href/').getall()
4.4 Items
Spiders會將提取的數據存放在items
中,這是一個key-value
的Python對象。
Scrapy支持一下類型的Item
:
字典
Item對象
dataclass 對象
attrs對象
。
聲明一個Item
import scrapy
class Product(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
stock = scrapy.Field()
tags = scrapy.Field()
last_updated = scrapy.Field(serializer=str)
Scrapy中的Item定義有些像Django
中的Model
,但是更簡單,沒有不同的Field
之分。
聲明field
Field
對象是用來指明每一個字段的元數據的。Scrapy中沒有對Field對象所能接受的值做限制
。
需要注意的是
,在Item子類中定義的這些字段,並不是該子類的類屬性,而是應該通過Item.fields
來獲取這些字段。
其實Field類就是Python中dict類的別名,,它並沒有任何其它的功能或屬性。
創建items
>>> product = Product(name='Desktop PC', price=1000)
>>> print(product)
Product(name='Desktop PC', price=1000)
獲取字段的值
# 用法就跟字典很相似
>>> product['name']
Desktop PC
>>> product.get('name', 'unknown field')
Desktop PC
設置字段的值
>>> product['name'] = 'Desktop PC for sjl'
獲取所有填充了的值
>>> product.keys()
['price', 'name']
>>> product.items()
[('price, 1000), ('name', 'Desktop PC for sjl')]
複製 items
# 可以選擇淺拷貝和深拷貝,淺拷貝和深拷貝這裏就不多講了
product2 = product.copy()
product2 = product.deepcopy()
Scrapy並不是直接填充items,而是有自己的機制。
items爲爬取的數據提供容器,Item Loader爲該容器提供了填充機制。
4.5 Item Loader
Item Loader用來填充數據進item中。
from scrapy.loader import ItemLoader
from myproject.items import Product
def parse(self, response):
l = ItemLoader(item=Product(), response=response)
l.add_xpath('name', '//div[@class="product_name"]')
l.add_xpath('name', '//div[@class="product_title"]')
l.add_xpath('price', '//p[@id="price"]')
l.add_css('stock', 'p#stock]')
l.add_value('last_updated', 'today') # you can also use literal values
return l.load_item()
輸入和輸出處理器
一個Item Loader
爲每一個 item的字段都指定了一個輸入處理器和輸出處理器。
- 當提取的數據被
Item Loader
接收(如:add_xpath()
,add_css()
,add_value()
方法)時會使用輸入處理器處理它們,並保存在ItemLoader
中。 - 然後調用
ItemLoader.load_item()
方法獲得Item對象
並在輸出處理器處理這些數據之後進行填充到item中。
4.6 Scrapy shell
用於開發和調試你的spiders
代碼的。
scrapy shell <needing_scraped_url>
4.7 item pipeline
詳情見 初識 Scrapy - Item Pipeline
在item
被spider
抓取之後,它會被髮送到Item Pipeline
,該管道通過幾個按順序執行的組件來處理它。
每一個item pipeline組件
都是Python
的類。它們接收item
,並對它執行操作,還決定該項目是否應繼續通過管道,或者是否應刪除並不再處理。
item pipeline
的典型用途有:
- 清理
HTML
數據 - 驗證抓取的數據(檢查項目是否包含某些字段)
- 檢查重複項(並刪除它們)。通過
pipeline
的process_item()
方法實現 - 將爬取的項目存儲在數據庫中
4.8 feed導出
詳情見 初識 Scrapy - Feed導出
在實現scraper時,常需要的功能之一是能夠正確地存儲被抓取的數據,這意味着用被抓取的數據(通常稱爲“導出提要”)生成一個“導出文件”,供其他系統使用。
Scrapy通過Feed導出
提供了這樣一個開箱即用的功能。允許你根據抓取的items使用多種序列化格式
和存儲後端
生成feeds。
4.9 Scrapy的Request和Response
Scrapy使用Request
和Response
對象來爬取網站。
通常,Request
對象是在Spider中生成的,並在整個系統中傳遞,直到它們到達Downloader
,該Downloader
執行請求並返回Response
對象,該Response
對象返回到發出請求的Spider中。
4.10 link提取器
用於從repsonse
中提取link
。
LxmlLinkExtractor
中的 __init__
方法決定應該提取什麼樣的link。
LxmlLinkExtractor.extract_links
接收 Response
對象並返回 scrapy.link.Link
對象。
4.11 設置
設置的優先級:
- 命令行選項(最高優先級)
- 每一個spider的設置
- 項目的setting模塊
- 每個命令的默認設置
- 默認的全局設置
在Spider中通過self.settings
來訪問設置。
4.12 異常
Scrapy中有一些內置的異常:
CloseSpider
。在spider的request
關閉或停止時,在request的回調函數中引發。DontCloseSpider
。在spider_idle信號處理器
中引發,以阻止關閉spider。DropItem
。在item pipeline
階段引發,以停止處理某Item。IgnoreRequest
。由Scheduler
或任何downloader中間件
引發,以表示此request應當被忽略。NotConfigured
。由某些組件引發,以表示這些組件是禁用的。NotSupported
。引發此異常表示不支持某特性。StopDownload
。由bytes_received
信號處理器引發,以表示response沒有更多的可下載的bytes了。
5. 參考文獻
[1] Scrapy 官方文檔