scrapy總結
1 scrapy項目開發流程
1.1 創建項目命令
scrapy startproject guokespider
1.2 創建一個爬蟲
在終端中,先進入到爬蟲項目目錄下, 然後執行生成爬蟲命令,格式scrapy genspider 爬蟲名 域名
scrapy genspider guoke www.guokr.com
1.3 數據建模
在items.py
文件中進行建模
1.4 編寫爬蟲
- 修改
start_urls
- 檢查修改
allowed_domains
- 編寫爬蟲解析響應方法
- 在
pipelines.py
文件中,創建處理數據的管道,用於保存數據 - 修改
settings.py
配置文件, 註冊管道
1.5 運行爬蟲
scrapy crawl guoke --nolog
下面來簡單介紹一下各個主要文件的作用:
scrapy.cfg :項目的配置文件
mySpider/ :項目的Python模塊,將會從這裏引用代碼
mySpider/items.py :項目的目標文件
mySpider/pipelines.py :項目的管道文件
mySpider/settings.py :項目的設置文件
mySpider/spiders/ :存儲爬蟲代碼目錄
2 scrapy的運行流程
Scrapy Engine(引擎)
: 負責Spider
、ItemPipeline
、Downloader
、Scheduler
中間的通訊,信號、數據傳遞等。Scheduler(調度器)
: 存放request對象,把request對象>引擎>下載器中間件==>下載器,並按照一定的方式進行整理排列,入隊,當引擎
需要時,交還給引擎
。Downloader(下載器)
:負責下載Scrapy Engine(引擎)
發送的所有Requests請求,並將其獲取到的Responses交還給Scrapy Engine(引擎)
,由引擎
交給Spider
來處理,生成response對象>下載器中間件>引擎>爬蟲中間件>爬蟲Spider(爬蟲)
:提取url和數據,提取的url會轉換成request對象>爬蟲中間件>引擎>調度器,而提取的數據通過引擎交給管道,即數據>爬蟲中間件>引擎>管道Item Pipeline(管道)
:數據處理,保存數據。它負責處理Spider
中獲取到的Item,並進行進行後期處理(詳細分析、過濾、存儲等)的地方.Downloader Middlewares(下載中間件)
:你可以當作是一個可以自定義擴展下載功能的組件。Spider Middlewares(Spider中間件)
:你可以理解爲是一個可以自定擴展和操作引擎
和Spider
中間通信
的功能組件(比如進入Spider
的Responses;和從Spider
出去的Requests)
3 代碼案例
3.1 新建scrapy項目
- 創建爬蟲項目,命令:scrapy startproject 項目名稱
- 創建爬蟲文件,命令:scrapy genspider 文件名稱 域名
3.2 構建項目模型
import scrapy
class BookItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
book_cate01 = scrapy.Field()
book_cate02 = scrapy.Field()
book_cate03 = scrapy.Field()
book_href = scrapy.Field()
book_name = scrapy.Field()
book_store = scrapy.Field()
book_num = scrapy.Field()
構建的字段模型 可以像 python字典一樣使用
在爬蟲文件中suning.py
導入定義的字段項目模型:
import scrapy
from book.items import BookItem
然後再解析方法parse
中實例項目模型即可當做字典使用:
def parse(self, response, **kwargs):
"""解析響應中的數據
:param **kwargs:
"""
# 一級分類列表
div_list = response.xpath('//div[@class="menu-item"]')[:7]
# 二級分類列表
div_sub_list = response.xpath('//div[@class="menu-list"]/div[@class="menu-sub"]')
print(div_sub_list)
for div in div_list[:1]:
item = BookItem()
item['book_cate01'] = div.xpath('.//h3/a/text()').extract_first()
pass
3.3 啓用管道
class SomethingPipeline(object):
def __init__(self):
# 可選實現,做參數初始化等
# doing something
def process_item(self, item, spider):
# item (Item 對象) – 被爬取的item
# spider (Spider 對象) – 爬取該item的spider
# 這個方法必須實現,每個item pipeline組件都需要調用該方法,
# 這個方法必須返回一個 Item 對象,被丟棄的item將不會被之後的pipeline組件所處理。
return item
def open_spider(self, spider):
# 在爬蟲開始的時候僅執行一次
# 可選實現,當spider被開啓時,這個方法被調用。
def close_spider(self, spider):
# 在爬蟲結束的時候僅執行一次
# 可選實現,當spider被關閉時,這個方法被調用
-
啓用一個Item Pipeline組件 爲了啓用Item Pipeline組件,需要在 settings.py文件中ITEM_PIPELINES 註冊:
# 分配給每個類的整型值,確定了他們運行的順序,item按數字從低到高的順序,通過pipeline,通常將這些數字定 ITEM_PIPELINES = { 'book.pipelines.BookPipeline': 300, }
3.4 數據持久化
以mongodb
爲例,將數據存入mongodb:
import pymongo
class BookPipeline(object):
def open_spider(self, spider):
"""
爬蟲開始只執行一次
:param spider:
:return:
"""
# 創建mongodb的數據庫連接
self.client = pymongo.MongoClient(host='192.168.99.100', port=27018)
# 選擇數據庫和數據表
self.cursor = self.client['suning']['book']
# 清空數據
self.cursor.delete_many({})
def process_item(self, item, spider):
# 保存到數據庫中
self.cursor.insert_one(dict(item))
return item
def close_spider(self, spider):
"""
爬蟲結束時只執行一次
:param spider:
:return:
"""
# 關閉連接
self.client.close()
print('爬蟲結束')
3.5 數據解析
# -*- coding: utf-8 -*-
from copy import deepcopy
from pprint import pprint
import scrapy
from book.items import BookItem
class SuningSpider(scrapy.Spider):
name = 'suning'
allowed_domains = ['suning.com']
start_urls = ['https://book.suning.com/']
# 當前頁的書籍計數
book_num = 0
def parse(self, response, **kwargs):
"""解析響應中的數據
:param **kwargs:
"""
# 一級分類列表
div_list = response.xpath('//div[@class="menu-item"]')[:7]
# 二級分類列表
div_sub_list = response.xpath('//div[@class="menu-list"]/div[@class="menu-sub"]')
print(div_sub_list)
for div in div_list[:1]:
item = BookItem()
item['book_cate01'] = div.xpath('.//h3/a/text()').extract_first()
# 一級分類下的元素列表
sub_div = div_sub_list[div_list.index(div)]
# 一級分類的所有二級分類和三級分類
p_list = sub_div.xpath('./div[@class="submenu-left"]/p')
for p in p_list[:1]:
# 二級分類
item['book_cate02'] = p.xpath('./a/text()').extract_first()
# 三級分類元素列表
li_list = p.xpath('./following-sibling::ul[1]/li')
for li in li_list[:1]:
item['book_cate03'] = li.xpath('./a/text()').extract_first()
item['book_href'] = li.xpath('./a/@href').extract_first()
yield scrapy.Request(
item['book_href'],
callback=self.parse_book_list,
meta={'item': deepcopy(item)}
)
# 獲取當前頁的後半部分的數據
next_part_url = "https://list.suning.com/emall/showProductList.do?ci={}&pg=03&cp=0&il=0&iy=0&adNumber=0&n=1&ch=4&prune=0&sesab=ACBAAB&id=IDENTIFYING&paging=1&sub=0"
ci = item['book_href'].split('-')[1]
next_part_url = next_part_url.format(ci)
print(next_part_url)
yield scrapy.Request(
next_part_url,
callback=self.parse_book_list,
meta={'item': deepcopy(item)}
)
print('=' * 10)
next_part_url.format(ci)
def parse_book_list(self, response):
"""解析response中的所有book"""
print(1)
item = response.meta.get('item')
li_list = response.xpath('//li[contains(@class,"product book")]')
for li in li_list:
item['book_name'] = li.xpath('.//p[@class="sell-point"]/a/text()').extract_first().strip()
item['book_href'] = li.xpath('.//p[@class="sell-point"]/a/@href').extract_first()
item['book_store'] = li.xpath('.//p[contains(@class, "seller oh no-more")]/a/text()').extract_first()
yield scrapy.Request(
response.urljoin(item['book_href']),
callback=self.parse_book_detail,
meta={'item': deepcopy(item)}
)
# TODO 分頁
def parse_book_detail(self, response):
"""解析書籍信息"""
# get date
item = response.meta.get('item')
self.book_num += 1
item['book_num'] = self.book_num
yield item
4 CrawlSpider的高階使用
通過命令創建 CrawlSpider爬蟲的代碼:
scrapy genspider -t crawl tencent tencent.com
它是Spider的派生類,Spider類的設計原則是隻爬取start_url列表中的網頁,而CrawlSpider類定義了一些規則(rule)來提供跟進link的方便的機制,從爬取的網頁中獲取link並繼續爬取的工作更適合。
CrawlSpider繼承於Spider類,除了繼承過來的屬性外(name、allow_domains),還提供了新的屬性和方法
4.1 rules
CrawlSpider使用rules來決定爬蟲的爬取規則,並將匹配後的url請求提交給引擎。所以在正常情況下,CrawlSpider不需要單獨手動返回請求了。
在rules中包含一個或多個Rule對象,每個Rule對爬取網站的動作定義了某種特定操作,比如提取當前相應內容裏的特定鏈接,是否對提取的鏈接跟進爬取,對提交的請求設置回調函數等。
如果多個rule匹配了相同的鏈接,則根據規則在本集合中被定義的順序,第一個會被使用。
class scrapy.spiders.Rule(
link_extractor,
callback = None,
cb_kwargs = None,
follow = None,
process_links = None,
process_request = None
)
-
link_extractor
:是一個Link Extractor對象,用於定義需要提取的鏈接。 -
callback
: 從link_extractor中每獲取到鏈接時,參數所指定的值作爲回調函數,該回調函數接受一個response作爲其第一個參數。注意:當編寫爬蟲規則時,避免使用parse作爲回調函數。由於CrawlSpider使用parse方法來實現其邏輯,如果覆蓋了 parse方法,crawl spider將會運行失敗。
-
follow
:是一個布爾(boolean)值,指定了根據該規則從response提取的鏈接是否需要跟進。 如果callback爲None,follow 默認設置爲True ,否則默認爲False。 -
process_links
:指定該spider中哪個的函數將會被調用,從link_extractor中獲取到鏈接列表時將會調用該函數。該方法主要用來過濾。 -
process_request
:指定該spider中哪個的函數將會被調用, 該規則提取到每個request時都會調用該函數。 (用來過濾request)
4.2 LinkExtractors
class scrapy.linkextractors.LinkExtractor
Link Extractors 的目的很簡單: 提取鏈接。
每個LinkExtractor有唯一的公共方法是 extract_links(),它接收一個 Response 對象,並返回一個 scrapy.link.Link 對象。
Link Extractors要實例化一次,並且 extract_links 方法會根據不同的 response 調用多次提取鏈接。
class scrapy.linkextractors.LinkExtractor(
allow = (),
deny = (),
allow_domains = (),
deny_domains = (),
deny_extensions = None,
restrict_xpaths = (),
tags = ('a','area'),
attrs = ('href'),
canonicalize = True,
unique = True,
process_value = None
)
主要參數:
allow
:滿足括號中“正則表達式”的URL會被提取,如果爲空,則全部匹配。deny
:滿足括號中“正則表達式”的URL一定不提取(優先級高於allow)。allow_domains
:會被提取的鏈接的domains。deny_domains
:一定不會被提取鏈接的domains。restrict_xpaths
:使用xpath表達式,和allow共同作用過濾鏈接。
4.3 爬取規則(Crawling rules)
以騰訊招聘爲例,給出配合rule使用CrawlSpider的例子:
-
首先運行
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
-
導入LinkExtractor,創建LinkExtractor實例對象。:
from scrapy.linkextractors import LinkExtractor page_lx = LinkExtractor(allow=('position.php?&start=\d+'))
allow : LinkExtractor對象最重要的參數之一,這是一個正則表達式,必須要匹配這個正則表達式(或正則表達式列表)的URL纔會被提取,如果沒有給出(或爲空), 它會匹配所有的鏈接。
deny : 用法同allow,只不過與這個正則表達式匹配的URL不會被提取)。它的優先級高於 allow 的參數,如果沒有給出(或None), 將不排除任何鏈接。
-
調用LinkExtractor實例的extract_links()方法查詢匹配結果:
page_lx.extract_links(response)
-
沒有查到:
[]
-
注意轉義字符的問題,繼續重新匹配:
page_lx = LinkExtractor(allow=('position\.php\?&start=\d+')) # page_lx = LinkExtractor(allow = ('start=\d+')) page_lx.extract_links(response)
4.4 CrawlSpider 版本
那麼,scrapy shell測試完成之後,修改以下代碼
#提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的鏈接
page_lx = LinkExtractor(allow = ('start=\d+'))
rules = [
#提取匹配,並使用spider的parse方法進行分析;並跟進鏈接(沒有callback意味着follow默認爲True)
Rule(page_lx, callback = 'parse', follow = True)
]
這麼寫對嗎?
不對!千萬記住 callback 千萬不能寫 parse,再次強調:由於CrawlSpider使用parse方法來實現其邏輯,如果覆蓋了 parse方法,crawl spider將會運行失敗。
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class TecentSpider(CrawlSpider):
name = 'tecent'
allowed_domains = ['hr.tencent.com']
start_urls = ['http://hr.tencent.com/position.php?&start=0']
page_lx = LinkExtractor(allow=r'start=\d+')
#position.php?&start=10#a
rules = (
Rule(page_lx, callback='parse_item', follow=True),
)
def parse_item(self, response):
items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
for item in items:
temp = dict(
position=item.xpath("./td[1]/a/text()").extract()[0],
detailLink="http://hr.tencent.com/" + item.xpath("./td[1]/a/@href").extract()[0],
type=item.xpath('./td[2]/text()').extract()[0] if len(
item.xpath('./td[2]/text()').extract()) > 0 else None,
need_num=item.xpath('./td[3]/text()').extract()[0],
location=item.xpath('./td[4]/text()').extract()[0],
publish_time=item.xpath('./td[5]/text()').extract()[0]
)
print(temp)
yield temp
# parse() 方法不需要重寫
# def parse(self, response):
# pass
運行: scrapy crawl tencent
5 Logging
Scrapy提供了log功能,可以通過 logging 模塊使用。
可以修改配置文件settings.py,任意位置添加下面兩行,效果會清爽很多。
LOG_FILE = "TencentSpider.log"
LOG_LEVEL = "INFO"
5.1 Log levels
- Scrapy提供5層logging級別:
CRITICAL
- 嚴重錯誤(critical)ERROR
- 一般錯誤(regular errors)WARNING
- 警告信息(warning messages)INFO
- 一般信息(informational messages)DEBUG
- 調試信息(debugging messages)
5.2 logging設置
通過在setting.py中進行以下設置可以被用來配置logging:
LOG_ENABLED
默認: True,啓用loggingLOG_ENCODING
默認: 'utf-8',logging使用的編碼LOG_FILE
默認: None,在當前目錄裏創建logging輸出文件的文件名LOG_LEVEL
默認: 'DEBUG',log的最低級別LOG_STDOUT
默認: False 如果爲 True,進程所有的標準輸出(及錯誤)將會被重定向到log中。例如,執行 print "hello" ,其將會在Scrapy log中顯示
6 中間件
6.1 分類和作用
-
分類
- 下載中間件
- 爬蟲中間件
-
作用-預處理
request
和reponse
對象- 對
header
和cookie
進行更換處理 - 使用代理ip
- 對請求進行定製化操作
- 對
兩種中間件都在middlewares.py
文件中
6.2 下載中間件(Downloader Middlewares)
下載中間件是處於引擎(crawler.engine)和下載器(crawler.engine.download())之間的一層組件,可以有多個下載中間件被加載運行。
- 當引擎傳遞請求給下載器的過程中,下載中間件可以對請求進行處理 (例如增加http header信息,增加proxy信息等);
- 在下載器完成http請求,傳遞響應給引擎的過程中, 下載中間件可以對響應進行處理(例如進行gzip的解壓等)
要激活下載器中間件組件,將其加入到 DOWNLOADER_MIDDLEWARES 設置中。 該設置是一個字典(dict),鍵爲中間件類的路徑,值爲其中間件的順序(order)。
這裏是一個例子:
DOWNLOADER_MIDDLEWARES = {
'mySpider.middlewares.MyDownloaderMiddleware': 543,
}
編寫下載器中間件十分簡單。每個中間件組件是一個定義了以下一個或多個方法的Python類:
class scrapy.contrib.downloadermiddleware.DownloaderMiddleware
6.2.1 process_request
process_request(self, request, spider)
當每個request通過下載中間件時,該方法被調用。
-
process_request() 必須返回以下其中之一:一個 None 、一個 Response 對象、一個 Request 對象或 raise IgnoreRequest:
-
返回 None值 :
Scrapy將繼續處理該request,執行其他的中間件的相應方法,直到合適的下載器(download handler)被調用, 該request被執行(其response被下載。
-
返回 Response 對象:
Scrapy將不會調用 任何 其他的 process_request() 或 process_exception() 方法,或相應地下載函數; 其將返回該response給引擎。
-
返回 Request 對象:
Scrapy則停止調用 process_request方法並通過引擎重新返回給調度器。當新返回的request被執行後, 相應地中間件鏈將會根據下載的response被調用。
-
如果其raise一個 IgnoreRequest 異常,則安裝的下載中間件的 process_exception() 方法會被調用。如果沒有任何一個方法處理該異常, 則request的errback(Request.errback)方法會被調用。如果沒有代碼處理拋出的異常, 則該異常被忽略且不記錄(不同於其他異常那樣)。
-
-
參數:
request (Request 對象)
– 處理的requestspider (Spider 對象)
– 該request對應的spider
6.2.2 process_response
process_response(self, request, response, spider)
當下載器完成http請求,傳遞響應給引擎的時候調用
-
process_response() 必須返回以下其中之一: 返回一個 Response 對象、 返回一個 Request 對象或raise一個 IgnoreRequest 異常。
-
返回 Response 對象:
可以與傳入的response相同,也可以是全新的對象, 該response會被在鏈中的其他中間件的 process_response() 方法處理。
-
返回 Request 對象:
中間件停止處理, 返回的request會通過引擎交給調度器,等待重新請求。處理類似於 process_request() 返回request所做的那樣。
-
如果其拋出一個 IgnoreRequest 異常,則調用request的errback(Request.errback)。 如果沒有代碼處理拋出的異常,則該異常被忽略且不記錄(不同於其他異常那樣)。
-
-
參數:
request (Request 對象)
– response所對應的requestresponse (Response 對象)
– 被處理的responsespider (Spider 對象)
– response所對應的spider
6.3 使用案例
1. 創建middlewares.py
文件。
Scrapy代理IP、Uesr-Agent的切換都是通過DOWNLOADER_MIDDLEWARES
進行控制,我們在settings.py
同級目錄下創建middlewares.py
文件,包裝所有請求。
# middlewares.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import random
import base64
from settings import USER_AGENTS
from settings import PROXIES
# 隨機的User-Agent
class RandomUserAgent(object):
def process_request(self, request, spider):
useragent = random.choice(USER_AGENTS)
request.headers.setdefault("User-Agent", useragent)
class RandomProxy(object):
def process_request(self, request, spider):
proxy = random.choice(PROXIES)
if proxy['user_passwd'] is None:
# 沒有代理賬戶驗證的代理使用方式
request.meta['proxy'] = "http://" + proxy['ip_port']
else:
# 對賬戶密碼進行base64編碼轉換
base64_userpasswd = base64.b64encode(proxy['user_passwd'])
# 對應到代理服務器的信令格式裏
request.headers['Proxy-Authorization'] = 'Basic ' + base64_userpasswd
request.meta['proxy'] = "http://" + proxy['ip_port']
爲什麼HTTP代理要使用base64編碼:
HTTP代理的原理很簡單,就是通過HTTP協議與代理服務器建立連接,協議信令中包含要連接到的遠程主機的IP和端口號,如果有需要身份驗證的話還需要加上授權信息,服務器收到信令後首先進行身份驗證,通過後便與遠程主機建立連接,連接成功之後會返回給客戶端200,表示驗證通過,就這麼簡單,下面是具體的信令格式:
CONNECT 59.64.128.198:21 HTTP/1.1
Host: 59.64.128.198:21
Proxy-Authorization: Basic bGV2I1TU5OTIz
User-Agent: OpenFetion
其中
Proxy-Authorization
是身份驗證信息,Basic後面的字符串是用戶名和密碼組合後進行base64編碼的結果,也就是對username:password進行base64編碼。
HTTP/1.0 200 Connection established
OK,客戶端收到收面的信令後表示成功建立連接,接下來要發送給遠程主機的數據就可以發送給代理服務器了,代理服務器建立連接後會在根據IP地址和端口號對應的連接放入緩存,收到信令後再根據IP地址和端口號從緩存中找到對應的連接,將數據通過該連接轉發出去。
2. 修改settings.py配置USER_AGENTS和PROXIES
- 添加USER_AGENTS:
USER_AGENTS = [
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
]
-
添加代理IP設置PROXIES:
免費代理IP可以網上搜索,或者付費購買一批可用的私密代理IP:
PROXIES = [
{'ip_port': '111.8.60.9:8123', 'user_passwd': 'user1:pass1'},
{'ip_port': '101.71.27.120:80', 'user_passwd': 'user2:pass2'},
{'ip_port': '122.96.59.104:80', 'user_passwd': 'user3:pass3'},
{'ip_port': '122.224.249.122:8088', 'user_passwd': 'user4:pass4'},
]
- 除非特殊需要,禁用cookies,防止某些網站根據Cookie來封鎖爬蟲。
COOKIES_ENABLED = False
- 設置下載延遲
DOWNLOAD_DELAY = 3
- 最後設置setting.py裏的DOWNLOADER_MIDDLEWARES,添加自己編寫的下載中間件類。
DOWNLOADER_MIDDLEWARES = {
#'mySpider.middlewares.MyCustomDownloaderMiddleware': 543,
'mySpider.middlewares.RandomUserAgent': 1,
'mySpider.middlewares.ProxyMiddleware': 100
}