一日一技:Scrapy如何發起假請求?
在使用Scrapy的時候,我們可以通過在pipelines.py里面定義一些數據處理流程,讓爬蟲在爬到數據以后,先處理數據再儲存。這本來是一個很好的功能,但容易被一些垃圾程序員拿來亂用。
我看到過一些Scrapy爬蟲項目,它的代碼是這樣寫的:
...
def start_requests(self):
yield scrapy.Request('https://baidu.com')
def parse(self, response):
import pymongo
handler = pymongo.MongoClient().xxdb.yycol
rows = handler.find()
for row in rows:
yield row
這種垃圾代碼之所以會出現,是因為有一些垃圾程序員想偷懶,想復用Pipeline里面的代碼,但又不想單獨把它抽出來。于是他們沒有皺褶的腦子一轉,想到在Scrapy里面從數據庫讀取現成的數據,然后直接yield出來給Pipeline。但因為Scrapy必須在start_requests里面發起請求,不能直接yield數據,因此他們就想到先隨便請求一個url,例如百度,等Scrapy的callback進入了parse方法以后,再去讀取數據。
雖然請求百度,不用擔心反爬問題,響應大概率也是HTTP 200,肯定能進入parse,但這樣寫代碼怎么看怎么蠢。
有沒有什么辦法讓代碼看起來,即便蠢也蠢得高級一些呢?有,那就是發送假請求。讓Scrapy看起來發起了HTTP請求,但實際上直接跳過。
方法非常簡單,就是把URL寫成:data:,,注意末尾這個英文逗號不能省略。
于是你的代碼就會寫成:
def start_requests(self):
yield scrapy.Request('data:,')
def parse(self, response):
import pymongo
handler = pymongo.MongoClient().xxdb.yycol
rows = handler.find()
for row in rows:
yield row
這樣寫以后,即使你沒有外網訪問權限也沒問題,因為它不會真正發起請求,而是直接一晃而過,進入parse方法中。我把這種方法叫做發送假請求。
這個方法還有另外一個應用場景。看下面這個代碼:
def start_requests(self):
while True:
yield scrapy.Request('https://kingname.info/atom.xml', callback=self.parse, dont_filter=True)
time.sleep(60)
def parse(self, response):
...對rss接口返回的數據進行處理...
for item in xxx['items']:
url = row['url']
yield scrapy.Request(url, callback=self.parse_detail)
假如你需要讓爬蟲每分鐘監控一個URL,你可能會像上面這樣寫代碼。但由于Scrapy是基于Twisted實現的異步并發,因此time.sleep這種同步阻塞等待會把爬蟲卡住,導致在sleep的時候,parse里面發起的子請求全都會被卡住,于是爬蟲的并發數基本上等于1.
可能有同學知道Scrapy支持asyncio,于是想這樣寫代碼:
import asyncio
async def start_requests(self):
while True:
yield scrapy.Request('https://kingname.info/atom.xml', callback=self.parse, dont_filter=True)
asyncio.sleep(60)
def parse(self, response):
...對rss接口返回的數據進行處理...
for item in xxx['items']:
url = row['url']
yield scrapy.Request(url, callback=self.parse_detail)
但這樣寫會報錯,如下圖所示:
圖片
這個問題的原因就在于start_requests這個入口方法不能使用async來定義。他需要至少經過一次請求,進入任何一個callback以后,才能使用async來定義。
這種情況下,也可以使用假請求來解決問題。我們可以把代碼改為:
求來解決問題。我們可以把代碼改為:
def start_requests(self):
yield scrapy.Request('data:,', callback=self.make_really_req)
async def make_really_req(self, _):
while True:
yield scrapy.Request(url="https://kingname.com", callback=self.parse)
await asyncio.sleep(60)
def parse(self, response):
print(response.text)
這樣一來,使用了asyncio.sleep,既能實現60秒請求一次,又不會阻塞子請求了。
當然,最新版的Scrapy已經廢棄了start_requests方法,改為start方法了,這個方法天生就是async方法,可以直接在里面asyncio.sleep,也就不會再有上面的問題了。不過如果你使用的還是老版本的Scrapy,上面這個假請求的方法還是有點用處。