Scrapy
Scrapy,Python開發的一個快速、高層次的屏幕抓取和web抓取框架,用於抓取web站點並從頁面中提取結構化的數據。Scrapy用途廣泛,可以用於數據挖掘、監測和自動化測試。
Scrapy吸引人的地方在於它是一個框架,任何人都可以根據需求方便的修改。它也提供了多種類型爬蟲的基類,如BaseSpider、sitemap爬蟲等,最新版本又提供了web2.0爬蟲的支持。
一、scrapy使用入門
以之前的糗事百科爲列
1.、新建項目
- 在開始爬取之前,必須創建一個新的Scrapy項目。進入自定義的項目目錄中,運行下列命令:
scrapy startproject 項目名稱
#例如:
scrapy startproject myspider
- 其中, mySpider 爲項目名稱,可以看到將會創建一個 mySpider 文件夾,目錄結構大致如下:
下面來簡單介紹一下各個主要文件的作用:
scrapy.cfg :項目的配置文件
mySpider/ :項目的Python模塊,將會從這裏引用代碼
mySpider/items.py :項目的目標文件
mySpider/pipelines.py :項目的管道文件
mySpider/settings.py :項目的設置文件
mySpider/spiders/ :存儲爬蟲代碼目錄
打開setting文件進行相應的設置
#關閉roobox協議
ROBOTSTXT_OBEY = False
#設置請求頭
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36'
}
2、明確目標(mySpider/items.py)
目標:糗事百科(段子作者和內容)
- 打開mySpider目錄下的items.py
- Item 定義結構化數據字段,用來保存爬取到的數據,有點像Python中的dict,但是提供了一些額外的保護減少錯誤。
- 可以通過創建一個 scrapy.Item 類, 並且定義類型爲 scrapy.Field的類屬性來定義一個Item。
- 接下來,創建Item 類,和構建item模型(model)。
import scrapy
class QsbkItem(scrapy.Item):
author = scrapy.Field()
content = scrapy.Field()
3、製作爬蟲 (spiders/itcastSpider.py)
爬蟲功能要分兩步:
1. 爬數據
- 在當前目錄下輸入命令,將在
mySpider/spider
目錄下創建一個名爲itcast
的爬蟲,並指定爬取域的範圍:
scrapy genspider itcast "qiushibaike.com"
- 打開 mySpider/spider目錄裏的 qsbk.py,默認增加了下列代碼:
import scrapy
class QsbkSpider(scrapy.Spider):
name = 'qsbk'
allowed_domains = ['qiushibaike.com'] #域名限制,該爬蟲只會訪問該域名下的url地址
start_urls = ['http://www.qiushibaike.com/'] #啓動的url
def parse(self, response):
pass
要建立一個Spider, 你必須用scrapy.Spider類創建一個子類,並確定了三個強制的屬性 和 一個方法。
name = ""
:這個爬蟲的識別名稱,必須是唯一的,在不同的爬蟲必須定義不同的名字。allow_domains = []
是搜索的域名範圍,也就是爬蟲的約束區域,規定爬蟲只爬取這個域名下的網頁,不存在的URL會被忽略。start_urls = ()
:爬取的URL元祖/列表。爬蟲從這裏開始抓取數據,所以,第一次下載的數據將會從這些urls開始。其他子URL將會從這些起始URL中繼承性生成。parse(self, response)
:解析的方法,每個初始URL完成下載後將被調用,調用的時候傳入從每一個URL傳回的Response對象來作爲唯一參數,主要作用如下:- 負責解析返回的網頁數據(response.body),提取結構化數據(生成item)
- 生成需要下一頁的URL請求。
將start_urls的值修改爲需要爬取的第一個url
start_urls = [https://www.qiushibaike.com/text/page/1/']
然後運行項目
scrapy crawl qsbk
是的,就是 itcast,看上面代碼,它是 ItcastSpider 類的 name 屬性,也就是使用 scrapy genspider
命令的爬蟲名。
一個Scrapy爬蟲項目裏,可以存在多個爬蟲。各個爬蟲在執行時,就是按照 name 屬性來區分。
運行之後,如果打印的日誌出現 [scrapy] INFO: Spider closed (finished)
,代表執行完成。。
- 取數據
- 爬取整個網頁完畢,接下來的就是的取過程了,首先觀察頁面源碼:
import scrapy
from qsbk.items import QsbkItem
class QsbkSpiderSpider(scrapy.Spider):
name = 'qsbk_spider'
allowed_domains = ['qiushibaike.com']
start_urls = ['https://www.qiushibaike.com/text/page/1/']
def parse(self, response):
# SelectorList
duanzidivs = response.xpath("//div[@id='content-left']/div")
print(duanzidivs)
for duanzidiv in duanzidivs:
# Selector
author = duanzidiv.xpath(".//h2/text()").get().strip()
content = duanzidiv.xpath(".//div[@class='content']//text()").getall()
content = "".join(content).strip()
item = QsbkItem(author=author,content=content)
yield item
- 我們暫時先不處理管道,後面會詳細介紹。
4、保存數據(管道pipelines)
使用管道之前,先在stteings中開啓管道
#大概是在第68行
ITEM_PIPELINES = {
'qsbk.pipelines.QsbkPipeline': 300,
}
然後再pipelines中寫存儲的文件信息
class QsbkPipeline(object):
def process_item(self, item, spider):
self.fp = open("duanzi.txt",'w')
self.fb.write(item)
return item
創建啓動文件
from scrapy import cmdline
cmdline.execute(["scrapy",'crawl','qsbk_spider'])
二、Scrapy Shell
Scrapy終端是一個交互終端,我們可以在未啓動spider的情況下嘗試及調試代碼,也可以用來測試XPath或CSS表達式,查看他們的工作方式,方便我們爬取的網頁中提取的數據。
如果安裝了 IPython ,Scrapy終端將使用 IPython (替代標準Python終端)。 IPython 終端與其他相比更爲強大,提供智能的自動補全,高亮輸出,及其他特性。(推薦安裝IPython)
1、啓動Scrapy Shell
進入項目的根目錄,執行下列命令來啓動shell:
scrapy shell "https://www.qiushibaike.com/text/page/1/"
Scrapy Shell根據下載的頁面會自動創建一些方便使用的對象,例如 Response 對象,以及 Selector 對象 (對HTML及XML內容)
。
- 當shell載入後,將得到一個包含response數據的本地 response 變量,輸入
response.body
將輸出response的包體,輸出response.headers
可以看到response的包頭。 - 輸入
response.selector
時, 將獲取到一個response 初始化的類 Selector 的對象,此時可以通過使用response.selector.xpath()
或response.selector.css()
來對 response 進行查詢。 - Scrapy也提供了一些快捷方式, 例如
response.xpath()
或response.css()
同樣可以生效(如之前的案例)。
2、Selectors選擇器
Scrapy Selectors 內置 XPath 和 CSS Selector 表達式機制
Selector有四個基本的方法,最常用的還是xpath:
- xpath(): 傳入xpath表達式,返回該表達式所對應的所有節點的selector list列表
- extract(): 序列化該節點爲字符串並返回list
- css(): 傳入CSS表達式,返回該表達式所對應的所有節點的selector list列表,語法同 BeautifulSoup4
- re(): 根據傳入的正則表達式對數據進行提取,返回字符串list列表
XPath表達式的例子及對應的含義:
/html/head/title: 選擇<HTML>文檔中 <head> 標籤內的 <title> 元素
/html/head/title/text(): 選擇上面提到的 <title> 元素的文字
//td: 選擇所有的 <td> 元素
//div[@class="mine"]: 選擇所有具有 class="mine" 屬性的 div 元素
嘗試Selector
我們用騰訊社招的網站http://hr.tencent.com/position.php?&start=0#a舉例:
# 啓動
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
# 返回 xpath選擇器對象列表
response.xpath('//title')
[<Selector xpath='//title' data='<title>職位搜索 | 社會招聘 | Tencent 騰訊招聘</title'>]
# 使用 extract()方法返回字符串列表
response.xpath('//title').extract()
['<title>職位搜索 | 社會招聘 | Tencent 騰訊招聘</title>']
# 打印列表第一個元素,沒有則返回None
print response.xpath('//title').extract_first()
<title>職位搜索 | 社會招聘 | Tencent 騰訊招聘</title>
#contains的用法,or的用法,last()的含義
In [6]: response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]/td[last()]/text()').extract()
Out[6]:
['2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02',
'2017-06-02']
In [4]: response.xpath('//a[contains(@href,"position_detail.php?")]/text()').extract()
Out[4]:
['19407-移動遊戲平臺合作(上海)',
'19407-手遊商業化與本地化策劃(上海)',
'OMG236-騰訊視頻平臺高級產品經理(深圳)',
'OMG096-科技頻道記者(北京)',
'18402-項目管理',
'IEG-招聘經理(深圳)',
'OMG097-視覺設計師(北京)',
'OMG097-策略產品經理/產品運營(北京)',
'OMG097-策略產品經理/產品運營(北京)',
'OMG097-數據產品經理(北京)']
In [5]: response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]/td[last()-1]/text()').extract()
Out[5]: ['上海', '上海', '深圳', '北京', '深圳', '深圳', '北京', '北京', '北京', '北京']
以後做數據提取的時候,可以把現在Scrapy Shell中測試,測試通過後再應用到代碼中。
當然Scrapy Shell作用不僅僅如此,但是不屬於我們課程重點,不做詳細介紹。
官方文檔:http://scrapy-chs.readthedocs.io/zh_CN/latest/topics/shell.html
三、item pipeline
當Item在Spider中被收集之後,它將會被傳遞到Item Pipeline,這些Item Pipeline組件按定義的順序處理Item。
每個Item Pipeline都是實現了簡單方法的Python類,比如決定此Item是丟棄而存儲。以下是item pipeline的一些典型應用:
- 驗證爬取的數據(檢查item包含某些字段,比如說name字段)
- 查重(並丟棄)
- 將爬取結果保存到文件或者數據庫中
1、編寫item pipeline
編寫item pipeline很簡單,item pipiline組件是一個獨立的Python類,其中process_item()方法必須實現:
import something
class QsbkPipeline(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 (Spider 對象) – 被開啓的spider
# 可選實現,當spider被開啓時,這個方法被調用。
def close_spider(self, spider):
# spider (Spider 對象) – 被關閉的spider
# 可選實現,當spider被關閉時,這個方法被調用
啓用一個Item Pipeline組件
爲了啓用Item Pipeline組件,必須將它的類添加到 settings.py文件ITEM_PIPELINES 配置,就像下面這個例子:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
#'mySpider.pipelines.SomePipeline': 300,
"mySpider.pipelines.JsonPipeline":300
}
分配給每個類的整型值,確定了他們運行的順序,item按數字從低到高的順序,通過pipeline,通常將這些數字定義在0-1000範圍內(0-1000隨意設置,數值越低,組件的優先級越高)
2、Logging配置
Scrapy提供了log功能,可以通過 logging 模塊使用。
可以修改配置文件settings.py,任意位置添加下面兩行,效果會清爽很多。
LOG_FILE = "xxx.log" #指定login保存文件
LOG_LEVEL = "INFO" #指定login信息輸出等級
四、Spider
Spider類定義瞭如何爬取某個(或某些)網站。包括了爬取的動作(例如:是否跟進鏈接)以及如何從網頁的內容中提取結構化數據(爬取item)。 換句話說,Spider就是您定義爬取的動作及分析某個網頁(或者是有些網頁)的地方。
class scrapy.Spider
是最基本的類,所有編寫的爬蟲必須繼承這個類。
主要用到的函數及調用順序爲:
__init__()
: 初始化爬蟲名字和start_urls列表
start_requests() 調用make_requests_from url()
:生成Requests對象交給Scrapy下載並返回response
parse()
: 解析response,並返回Item或Requests(需指定回調函數)。Item傳給Item pipline持久化 , 而Requests交由Scrapy下載,並由指定的回調函數處理(默認parse()),一直進行循環,直到處理完所有的數據爲止。
源碼參考
#所有爬蟲的基類,用戶定義的爬蟲必須從這個類繼承
class Spider(object_ref):
#定義spider名字的字符串(string)。spider的名字定義了Scrapy如何定位(並初始化)spider,所以其必須是唯一的。
#name是spider最重要的屬性,而且是必須的。
#一般做法是以該網站(domain)(加或不加 後綴 )來命名spider。 例如,如果spider爬取 mywebsite.com ,該spider通常會被命名爲 mywebsite
name = None
#初始化,提取爬蟲名字,start_ruls
def __init__(self, name=None, **kwargs):
if name is not None:
self.name = name
# 如果爬蟲沒有名字,中斷後續操作則報錯
elif not getattr(self, 'name', None):
raise ValueError("%s must have a name" % type(self).__name__)
# python 對象或類型通過內置成員__dict__來存儲成員信息
self.__dict__.update(kwargs)
#URL列表。當沒有指定的URL時,spider將從該列表中開始進行爬取。 因此,第一個被獲取到的頁面的URL將是該列表之一。 後續的URL將會從獲取到的數據中提取。
if not hasattr(self, 'start_urls'):
self.start_urls = []
# 打印Scrapy執行後的log信息
def log(self, message, level=log.DEBUG, **kw):
log.msg(message, spider=self, level=level, **kw)
# 判斷對象object的屬性是否存在,不存在做斷言處理
def set_crawler(self, crawler):
assert not hasattr(self, '_crawler'), "Spider already bounded to %s" % crawler
self._crawler = crawler
@property
def crawler(self):
assert hasattr(self, '_crawler'), "Spider not bounded to any crawler"
return self._crawler
@property
def settings(self):
return self.crawler.settings
#該方法將讀取start_urls內的地址,併爲每一個地址生成一個Request對象,交給Scrapy下載並返回Response
#該方法僅調用一次
def start_requests(self):
for url in self.start_urls:
yield self.make_requests_from_url(url)
#start_requests()中調用,實際生成Request的函數。
#Request對象默認的回調函數爲parse(),提交的方式爲get
def make_requests_from_url(self, url):
return Request(url, dont_filter=True)
#默認的Request對象回調函數,處理返回的response。
#生成Item或者Request對象。用戶必須實現這個類
def parse(self, response):
raise NotImplementedError
@classmethod
def handles_request(cls, request):
return url_is_from_spider(request.url, cls)
def __str__(self):
return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))
__repr__ = __str__
1、主要屬性和方法
-
name
定義spider名字的字符串。
例如,如果spider爬取 mywebsite.com ,該spider通常會被命名爲 mywebsite
-
allowed_domains
包含了spider允許爬取的域名(domain)的列表,可選。
-
start_urls
初始URL元祖/列表。當沒有制定特定的URL時,spider將從該列表中開始進行爬取。
-
start_requests(self)
該方法必須返回一個可迭代對象(iterable)。該對象包含了spider用於爬取(默認實現是使用 start_urls 的url)的第一個Request。
當spider啓動爬取並且未指定start_urls時,該方法被調用。
-
parse(self, response)
當請求url返回網頁沒有指定回調函數時,默認的Request對象回調函數。用來處理網頁返回的response,以及生成Item或者Request對象。
-
log(self, message[, level, component])
使用 scrapy.log.msg() 方法記錄(log)message。 更多數據請參見 logging
2、案例:自動翻頁採集
- 創建一個新的爬蟲:
scrapy genspider tencent "tencent.com"
獲取職位名稱、詳細信息、
class TencentItem(scrapy.Item):
name = scrapy.Field()
detailLink = scrapy.Field()
positionInfo = scrapy.Field()
peopleNumber = scrapy.Field()
workLocation = scrapy.Field()
publishTime = scrapy.Field()
# tencent.py
from mySpider.items import TencentItem
import scrapy
import re
class TencentSpider(scrapy.Spider):
name = "tencent"
allowed_domains = ["hr.tencent.com"]
start_urls = [
"http://hr.tencent.com/position.php?&start=0#a"
]
def parse(self, response):
items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
for item in items:
temp = dict(
name=item.xpath("./td[1]/a/text()").extract()[0],
detailLink="http://hr.tencent.com/"+item.xpath("./td[1]/a/@href").extract()[0],
positionInfo=item.xpath('./td[2]/text()').extract()[0] if len(item.xpath('./td[2]/text()').extract())>0 else None,
peopleNumber=item.xpath('./td[3]/text()').extract()[0],
workLocation=item.xpath('./td[4]/text()').extract()[0],
publishTime=item.xpath('./td[5]/text()').extract()[0]
)
yield temp
now_page = int(re.search(r"\d+", response.url).group(0))
print("*" * 100)
if now_page < 216:
url = re.sub(r"\d+", str(now_page + 10), response.url)
print("this is next page url:", url)
print("*" * 100)
yield scrapy.Request(url, callback=self.parse)
- 編寫pipeline.py文件
import json
#class ItcastJsonPipeline(object):
class TencentJsonPipeline(object):
def __init__(self):
#self.file = open('teacher.json', 'wb')
self.file = open('tencent.json', 'wb')
def process_item(self, item, spider):
content = json.dumps(dict(item), ensure_ascii=False) + "\n"
self.file.write(content)
return item
def close_spider(self, spider):
self.file.close()
- 在 setting.py 裏設置ITEM_PIPELINES
ITEM_PIPELINES = {
#'mySpider.pipelines.SomePipeline': 300,
"mySpider.pipelines.TencentJsonPipeline":300
}
- 執行爬蟲:
scrapy crawl tencent
3、請思考 parse()方法的工作機制:
1. 因爲使用的yield,而不是return。parse函數將會被當做一個生成器使用。scrapy會逐一獲取parse方法中生成的結果,並判斷該結果是一個什麼樣的類型;
2. 如果是request則加入爬取隊列,如果是item類型則使用pipeline處理,其他類型則返回錯誤信息。
3. scrapy取到第一部分的request不會立馬就去發送這個request,只是把這個request放到隊列裏,然後接着從生成器裏獲取;
4. 取盡第一部分的request,然後再獲取第二部分的item,取到item了,就會放到對應的pipeline裏處理;
5. parse()方法作爲回調函數(callback)賦值給了Request,指定parse()方法來處理這些請求 scrapy.Request(url, callback=self.parse)
6. Request對象經過調度,執行生成 scrapy.http.response()的響應對象,並送回給parse()方法,直到調度器中沒有Request(遞歸的思路)
7. 取盡之後,parse()工作結束,引擎再根據隊列和pipelines中的內容去執行相應的操作;
8. 程序在取得各個頁面的items前,會先處理完之前所有的request隊列裏的請求,然後再提取items。
7. 這一切的一切,Scrapy引擎和調度器將負責到底。
五、Request對象
Request 部分源碼:
# 部分代碼
class Request(object_ref):
def __init__(self, url, callback=None, method='GET', headers=None, body=None,
cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None):
self._encoding = encoding # this one has to be set first
self.method = str(method).upper()
self._set_url(url)
self._set_body(body)
assert isinstance(priority, int), "Request priority not an integer: %r" % priority
self.priority = priority
assert callback or not errback, "Cannot use errback without a callback"
self.callback = callback
self.errback = errback
self.cookies = cookies or {}
self.headers = Headers(headers or {}, encoding=encoding)
self.dont_filter = dont_filter
self._meta = dict(meta) if meta else None
@property
def meta(self):
if self._meta is None:
self._meta = {}
return self._meta
其中,比較常用的參數:
url: 就是需要請求,並進行下一步處理的url
callback: 指定該請求返回的Response,由那個函數來處理。
method: 請求一般不需要指定,默認GET方法,可設置爲"GET", "POST", "PUT"等,且保證字符串大寫
headers: 請求時,包含的頭文件。一般不需要。內容一般如下:
# 自己寫過爬蟲的肯定知道
Host: media.readthedocs.org
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
Accept: text/css,*/*;q=0.1
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://scrapy-chs.readthedocs.org/zh_CN/0.24/
Cookie: _ga=GA1.2.1612165614.1415584110;
Connection: keep-alive
If-Modified-Since: Mon, 25 Aug 2014 21:59:35 GMT
Cache-Control: max-age=0
meta: 比較常用,在不同的請求之間傳遞數據使用的。字典dict型
request_with_cookies = Request(
url="http://www.example.com",
cookies={'currency': 'USD', 'country': 'UY'},
meta={'dont_merge_cookies': True}
)
encoding: 使用默認的 'utf-8' 就行。
dont_filter: 表明該請求不由調度器過濾。這是當你想使用多次執行相同的請求,忽略重複的過濾器。默認爲False。
errback: 指定錯誤處理函數
六、Response對象
# 部分代碼
class Response(object_ref):
def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
self.headers = Headers(headers or {})
self.status = int(status)
self._set_body(body)
self._set_url(url)
self.request = request
self.flags = [] if flags is None else list(flags)
@property
def meta(self):
try:
return self.request.meta
except AttributeError:
raise AttributeError("Response.meta not available, this response " \
"is not tied to any request")
大部分參數和上面的差不多:
status: #響應碼
_set_body(body): 響應體
_set_url(url):響應url
self.request = request
1、post模擬登錄
方式一:解析登錄請求參數發送請求登錄
可以使用 yield scrapy.FormRequest(url, formdata, callback)
方法發送POST請求。
# -*- coding: utf-8 -*-
import scrapy
import re
class GithubSpider(scrapy.Spider):
name = 'github'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def parse(self, response):
#構造請求參數
authenticity_token = response.xpath("//input[@name='authenticity_token']/@value").extract_first()
utf8 = response.xpath("//input[@name='utf8']/@value").extract_first()
commit = response.xpath("//input[@name='commit']/@value").extract_first()
post_data = dict(
login="[email protected]",
password="a546245426",
authenticity_token=authenticity_token,
utf8=utf8,
commit=commit
)
yield scrapy.FormRequest(
"https://github.com/session",
formdata=post_data,
callback=self.login_parse
)
def login_parse(self,response):
print(re.findall("zengyanzhi",response.body.decode()))
方式二:通過預定義表單數據登錄
使用FormRequest.from_response()方法模擬用戶登錄
通常網站通過 實現對某些表單字段(如數據或是登錄界面中的認證令牌等)的預填充。
使用Scrapy抓取網頁時,如果想要預填充或重寫像用戶名、用戶密碼這些表單字段, 可以使用 FormRequest.from_response() 方法實現。
import scrapy
import re
class Github2Spider(scrapy.Spider):
name = 'github2'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def parse(self, response):
yield scrapy.FormRequest.from_response(
response, #自動的從response中尋找from表單
formdata={"login":"[email protected]","password":"a546245426"},
callback = self.after_login
)
def after_login(self,response):
print(re.findall("zengyanzhi",response.body.decode()
方式三:通過重寫strat_request方法,加入cookie請求登錄後的頁面
如果希望程序執行一開始就發送POST請求,可以重寫Spider類的start_requests(self)
方法,並且不再調用start_urls裏的url。
class mySpider(scrapy.Spider):
name = 'github2'
allowed_domains = ['github.com']
start_urls = ['https://github.com/login']
def start_requests(self):
# FormRequest 是Scrapy發送POST請求的方法
cookies = {''} #cookies 字典的形式
yield scrapy.Request(
url = strat_url[0]
cookies=cookies
callback = self.parse_login
)
def parse_login(self, response):
print(re.findall("zengyanzhi",response.body.decode()
七、中間鍵
1、設置下載中間件
下載中間件(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
2、process_request(self, request, spider)
- 當每個request通過下載中間件時,該方法被調用。
- process_request() 必須返回以下其中之一:一個 None 、一個 Request 對象或 raise IgnoreRequest:
- 如果其返回 None ,Scrapy將繼續處理該request,執行其他的中間件的相應方法,直到合適的下載器處理函數(download handler)被調用, 該request被執行(其response被下載)。
- 如果其返回 Request 對象,Scrapy則停止調用 process_request方法並重新調度返回的request。當新返回的request被執行後, 相應地中間件鏈將會根據下載的response被調用。
- 如果其raise一個 IgnoreRequest 異常,則安裝的下載中間件的 process_exception() 方法會被調用。如果沒有任何一個方法處理該異常, 則request的errback(Request.errback)方法會被調用。如果沒有代碼處理拋出的異常, 則該異常被忽略且不記錄(不同於其他異常那樣)。
- 參數:
request (Request 對象)
– 處理的requestspider (Spider 對象)
– 該request對應的spider
2、process_response(self, request, response, spider)
當下載器完成http請求,傳遞響應給引擎的時候調用
- process_request() 必須返回以下其中之一: 返回一個 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
2、設置隨機代理和user-agnet
通過中間鍵設置代理
import random
class RandomUserAgentMiddleware:
def process_request(self,request,spider):
user_agnet = random.choice(spider.settings.get("USER_AGENTS_LIST"))
request.headers["User-Agent"] = user_agnet
class PrintUserAgent:
def process_response(self,request,response,spider):
#打印Useragent
print(request.headers["User-Agent"])
return response
通過中間鍵設置user-agent
class RandomProxyMiddleware:
def process_request(self,request,spider):
#通過meta屬性加入代理
request.meta['proxy'] = 'http://129.232.22.9:8800'
八、CrawlSpiders
通過下面的命令可以快速創建 CrawlSpider模板 的代碼:
scrapy genspider -t crawl tencent tencent.com
上一個案例中,我們通過正則表達式,製作了新的url作爲Request請求參數,現在我們可以換個花樣…
class scrapy.spiders.CrawlSpider
它是Spider的派生類,Spider類的設計原則是隻爬取start_url列表中的網頁,而CrawlSpider類定義了一些規則(rule)來提供跟進link的方便的機制,從爬取的網頁中獲取link並繼續爬取的工作更適合。
CrawlSpider繼承於Spider類,除了繼承過來的屬性外(name、allow_domains),還提供了新的屬性和方法:
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)
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共同作用過濾鏈接。
#通過xpath獲取到連接所在的標籤,或scrapy自動獲取
Rule(LinkExtractor(restrict_xpaths = '//table[@class="tablelist"]/tr/td/a'), callback='parse_item2', follow=False),
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、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、crawlspider案列
瓜子二手車數據分品牌爬取
爬取目標,所有地區,所有車輛
爬取數據
車量信息:vehicle_info
上牌時間:ttime
地區:addr
價格:price
里程:mileage
參考案列:京東圖書整站爬取
class JdSpider(scrapy.Spider):
name = 'jd'
allowed_domains = ['jd.com','p.3.cn']
start_urls = ['https://book.jd.com/booksort.html']
def parse(self, response):
dt_list = response.xpath("//div[@class='mc']/dl/dt") #大分類列表
for dt in dt_list:
item = {}
item["b_cate"] = dt.xpath("./a/text()").extract_first()
em_list = dt.xpath("./following-sibling::dd[1]/em") #小分類列表
for em in em_list:
item["s_href"] = em.xpath("./a/@href").extract_first()
item["s_cate"] = em.xpath("./a/text()").extract_first()
if item["s_href"] is not None:
item["s_href"] = "https:" + item["s_href"]
yield scrapy.Request(
item["s_href"],
callback=self.parse_book_list,
meta = {"item":deepcopy(item)}
)
def parse_book_list(self,response): #解析列表頁
item = response.meta["item"]
li_list = response.xpath("//div[@id='plist']/ul/li")
for li in li_list:
item["book_img"] = li.xpath(".//div[@class='p-img']//img/@src").extract_first()
if item["book_img"] is None:
item["book_img"] = li.xpath(".//div[@class='p-img']//img/@data-lazy-img").extract_first()
item["book_img"]="https:"+item["book_img"] if item["book_img"] is not None else None
item["book_name"] = li.xpath(".//div[@class='p-name']/a/em/text()").extract_first().strip()
item["book_author"] = li.xpath(".//span[@class='author_type_1']/a/text()").extract()
item["book_press"]= li.xpath(".//span[@class='p-bi-store']/a/@title").extract_first()
item["book_publish_date"] = li.xpath(".//span[@class='p-bi-date']/text()").extract_first().strip()
item["book_sku"] = li.xpath("./div/@data-sku").extract_first()
yield scrapy.Request(
"https://p.3.cn/prices/mgets?skuIds=J_{}".format(item["book_sku"]),
callback=self.parse_book_price,
meta = {"item":deepcopy(item)}
)
#列表頁翻頁
next_url = response.xpath("//a[@class='pn-next']/@href").extract_first()
if next_url is not None:
next_url = urllib.parse.urljoin(response.url,next_url)
yield scrapy.Request(
next_url,
callback=self.parse_book_list,
meta = {"item":item}
)
def parse_book_price(self,response):
item = response.meta["item"]
item["book_price"] = json.loads(response.body.decode())[0]["op"]
print(item)
九、scrapy對接selenium
在之前的案例中我們通過scrapy來爬取的都是靜態的html頁面,那麼如果遇到動態加載或者是發送請求無法直接獲取數據的時候我們應該怎麼辦,在之前我們學習過了通過selenium來抓取動態加載的頁面,如果我們要通過scrapy對接selenium,應該怎麼做呢?
這個時候我們可以通過下載中間鍵鍵來處理,我們可以在下載中間鍵中自定義請求頁面的方式,完成發起請求頁面獲取數據的過程,然後返回response,
創建好爬蟲之後,打開middewares文件
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from scrapy.http import HtmlResponse
from selenium.webdriver.chrome.options import Options
class SeleniumMiddleware(object):
def __init__(self):
chrome_option = Options()
#設爲無頭模式
chrome_option.add_argument('-headless')
#創建瀏覽器
browser = webdriver.Chrome(chrome_options=chrome_option)
def process_request(self, request, spider):
try:
#發送請求
self.browser.get(request.url)
#返回頁面數據
return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding='utf-8',status=200)
except TimeoutException:
#出現異常則返回異常信息
return HtmlResponse(url=request.url, status=500, request=request)
def __del__(self):
#關閉瀏覽器
self.browser.close()
十、setting相關配置項
# Crawl responsibly by identifying yourself (and your website) on the user-agent
#設置user-agent
USER_AGENT = 'github (+http://www.yourdomain.com)'
#是否開啓roboots協議
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
#設置最大的併發請求的數量
CONCURRENT_REQUESTS = 32
#設置同一個域名(網站)請求延遲時間
DOWNLOAD_DELAY = 3
#設置每個網站的最大併發請求數量
CONCURRENT_REQUESTS_PER_DOMAIN = 16
#設置每個IP的最大併發請求數量
CONCURRENT_REQUESTS_PER_IP = 16
#禁用cookie
COOKIES_ENABLED = False
#禁用遠程控制檯
TELNETCONSOLE_ENABLED = False
#設置默認的請求頭
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
}
#啓用爬蟲中間件
SPIDER_MIDDLEWARES = {
'github.middlewares.GithubSpiderMiddleware': 543,
}
#啓用下載中間件
DOWNLOADER_MIDDLEWARES = {
'github.middlewares.GitDownloaderMiddleware': 543,
'github.middlewares.GitProxy':566
}
#啓用item_pipelines管道
ITEM_PIPELINES = {
'github.pipelines.GithubPipeline': 300,
}
#初始下載延遲時間
AUTOTHROTTLE_START_DELAY = 5
#設置的最大下載延遲的時間
AUTOTHROTTLE_MAX_DELAY = 60
#Http緩存的配置
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
十一、scrapy-redis實現分佈式爬蟲
1、安裝scrapy-redis
pip install scrapy-redis
2、使用scrapy-redis實現分佈式
1、創建項目
scrapy startproject 項目名
2、創建爬蟲
scrapy genspider 爬蟲名 域名
3、修改settings中配置
#確保所有爬蟲通過redis共享相同的重複過濾器。
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
#在redis中啓用調度存儲請求隊列。
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
#不要清理redis隊列,允許暫停/恢復抓取。
SCHEDULER_PERSIST = True
#指定redir服務器的地址
REDIS_URL = "redis://192.168.0.103:6379"
4、修改爬蟲的繼承類
方式一:scrapy.Spider類的爬蟲
#從scrapy_redis.spiders導入RedisSpider這個類
from scrapy_redis.spiders import RedisSpider
#修改爬蟲的繼承類
class XXXSpider(RedisSpider):
name = 'xx'
allowed_domains = ['XX.com']
#把strat_url註釋
# start_urls = ['xxxxx']
#設置該爬蟲在redis中的鍵名,一般選擇用爬蟲名
redis_key = "爬蟲名"
#其他部分沒有任何變化還是和原來一樣的寫法
方式二:CrawlSpider類型爬蟲
#從scrapy_redis.spiders導入RedisSpider這個類
from scrapy_redis.spiders import RedisCrawlSpider
#修改爬蟲的繼承類
class AmazonSpider(RedisCrawlSpider):
name = 'XXX'
allowed_domains = ['xxx.cn']
#把strat_url註釋
# start_urls = ['xxxxxxxx']
#設置該爬蟲在redis中的鍵名,一般選擇用爬蟲名
redis_key = "xxx"
rules = (
Rule(LinkExtractor(restrict_xpaths=("",)), follow=True),
)
#其他部分沒有任何變化還是和原來一樣的寫法
5、啓動爬蟲
scrapy crawl 爬蟲名
6、在redis中插入爬蟲的start_url地址
啓動爬蟲之後爬蟲沒有start_url地址,並不會直接運行,需要我們在redis中往redis_key這個鍵中加入進去第一條url地址,這個時候爬蟲纔會開始運行
#進入redis中
lpush redis_key的值 初始的url地址
#例:爬蟲類中的redis_key爲:gauzi 第一個需要請求的url爲:https://www.guazi.com/cs/buy
127.0.0.1:6369> lpush guazi https://www.guazi.com/cs/buy/
當在redis中加入上述的url地址之後 我們的爬蟲會立馬開始執行
爬蟲啓動後 keys * 查看redis 數據庫中的鍵
192.168.0.103:6379> keys *
1) "guazi:dupefilter"
2) "guazi:requests"
guazi:dupefilter :集合類型(set)
裏面存放點的是所有往調度器中添加的所有url指紋數據
#查看所有元素
smembers guazi:dupefilter
#查看元素個數
scard guazi:dupefilter
guazi:requests:有序集合類型(zset)
裏面存放的是所有request請求的對象(經過處理過的)
#查看所有元素
zrange guazi:requests start stop
zcard guazi:requests
擴展:scrapy-redis關於url的去重原理
對於每一個url的請求,調度器都會根據請求得相關信息加密得到一個指紋信息,
並且將指紋信息和set()集合中的指紋信息進行比對,
如果set()集合中已經存在這個數據,就不在將這個Request放入隊列中。
如果set()集合中沒有存在這個加密後的數據,就將這個Request對象放入隊列中,等待被調度
十二、反反爬蟲相關機制
(有些些網站使用特定的不同程度的複雜性規則防止爬蟲訪問,繞過這些規則是困難和複雜的,有時可能需要特殊的基礎設施,如果有疑問,請聯繫商業支持。)
來自於Scrapy官方文檔描述:http://doc.scrapy.org/en/master/topics/practices.html#avoiding-getting-banned
通常防止爬蟲被反主要有以下幾個策略:
一.BAN IP
網頁的運維人員通過分析日誌發現最近某一個IP訪問量特別特別大,某一段時間內訪問了無數次的網頁,則運維人員判斷此種訪問行爲並非正常人的行爲,於是直接在服務器上封殺了此人IP(我剛爬取的網站的維護人員可能對我實施了這種手段…)。
解決方法:此種方法極其容易誤傷其他正常用戶,因爲某一片區域的其他用戶可能有着相同的IP,導致服務器少了許多正常用戶的訪問,所以一般運維人員不會通過此種方法來限制爬蟲。不過面對許多大量的訪問,服務器還是會偶爾把該IP放入黑名單,過一段時間再將其放出來,但我們可以通過分佈式爬蟲以及購買代理IP也能很好的解決,只不過爬蟲的成本提高了。
二.BAN USERAGENT
很多的爬蟲請求頭就是默認的一些很明顯的爬蟲頭python-requests/2.18.4,諸如此類,當運維人員發現攜帶有這類headers的數據包,直接拒絕訪問,返回403錯誤
解決方法:直接r=requests.get(url,headers={‘User-Agent’:‘XXXspider’})把爬蟲請求headers僞裝成其他爬蟲或者其他瀏覽器頭就行了。
案例:雪球網
三.BAN COOKIES
服務器對每一個訪問網頁的人都set-cookie,給其一個cookies,當該cookies訪問超過某一個閥值時就BAN掉該COOKIE,過一段時間再放出來,當然一般爬蟲都是不帶COOKIE進行訪問的,可是網頁上有一部分內容如新浪微博是需要用戶登錄才能查看更多內容(我已經中招了)。
解決辦法:控制訪問速度,或者某些需要登錄的如新浪微博,在某寶上買多個賬號,生成多個cookies,在每一次訪問時帶上cookies
案例:螞蜂窩
四.驗證碼驗證
當某一用戶訪問次數過多後,就自動讓請求跳轉到一個驗證碼頁面,只有在輸入正確的驗證碼之後才能繼續訪問網站
解決辦法:python可以通過一些第三方庫如(pytesser,PIL)來對驗證碼進行處理,識別出正確的驗證碼,複雜的驗證碼可以通過機器學習讓爬蟲自動識別複雜驗證碼,讓程序自動識別驗證碼並自動輸入驗證碼繼續抓取
五.javascript渲染
網頁開發者將重要信息放在網頁中但不寫入html標籤中,而瀏覽器會自動渲染
解決辦法:通過分析提取script中的js代碼來通過正則匹配提取信息內容或通過webdriver+phantomjs直接進行無頭瀏覽器渲染網頁。
案例:前程無憂網
隨便打開一個前程無憂工作界面,直接用requests.get對其進行訪問,可以得到一頁的20個左右數據,顯然得到的不全,而用webdriver訪問同樣的頁面可以得到50個完整的工作信息。
六.ajax異步傳輸
訪問網頁的時候服務器將網頁框架返回給客戶端,在與客戶端交互的過程中通過異步ajax技術傳輸數據包到客戶端,呈現在網頁上,爬蟲直接抓取的話信息爲空
解決辦法:通過fiddler或是wireshark抓包分析ajax請求的界面,然後自己通過規律仿造服務器構造一個請求訪問服務器得到返回的真實數據包。
案例:拉勾網
打開拉勾網的某一個工作招聘頁,可以看到許許多多的招聘信息數據,點擊下一頁後發現頁面框架不變化,url地址不變,而其中的每個招聘數據發生了變化,通過chrome開發者工具抓包找到了一個叫請求了一個叫做http://www.lagou.com/zhaopin/Java/2/?filterOption=3的網頁,打開改網頁發現爲第二頁真正的數據源,通過仿造請求可以抓取每一頁的數據。