一般來說爬蟲類框架抓取Ajax動態頁面都是通過一些第三方的webkit庫去手動執行html頁面中的js代碼, 最后將生產的html代碼交給spider分析。本篇文章則是通過瀏覽器提供的Debug工具分析Ajax頁面的具體請求內容,找到獲取數據的接口url,直接調用該接口獲取數據,省去了引入python-webkit庫的麻煩,而且由于一般ajax請求的數據都是結構化數據,這樣更省去了我們利用xpath解析html的痛苦。
這次我們要抓取的網站是淘女郎的頁面,全站都是通過Ajax獲取數據然后重新渲染生產的。
這篇文章的代碼已上傳至我的Github,由于后面有部分內容并沒有提供完整代碼,所以貼上地址供各位參考。
分析工作
用Chrome打開淘女郎的首頁中的美人庫,這個頁面毫無疑問是會展示所有的模特的信息,同時打開Debug工具,在network選項中查看瀏覽器發送了哪些請求?
在截圖的左下角可以看到總共產生了86個請求,那么有什么辦法可以快速定位到Ajax請求的鏈接了,利用Network當中提供的Filter功能,選中Filter,最后選擇右邊的XHR過濾(XHR時XMLHttpRequest對象,一般Ajax請求的數據都是結構化數據),這樣就剩下了為數不多的幾個請求,剩下的就靠我們自己一個一個的檢查吧
很幸運,通過分析每個接口返回的request和response信息,發現最后一個請求就是我們需要的接口url
Request中得參數很簡單,根據英文意思就可以猜出意義,由于我們要抓取所有模特的信息,所以不需要定制這些參數,后面直接將這些參數post給接口就行了
在Response中可以獲得到的有用數據有兩個:所有模特信息的列表searchDOList
、以及總頁數totolPage
searchDOList列表中得對象都有如上圖所示的json格式,它也正是我們需要的模特信息的數據
Scrapy編碼
- 定義Item
class tbModelItem(scrapy.Item):
avatarUrl = scrapy.Field()
cardUrl = scrapy.Field()
city = scrapy.Field()
height = scrapy.Field()
identityUrl = scrapy.Field()
modelUrl = scrapy.Field()
realName = scrapy.Field()
totalFanNum = scrapy.Field()
totalFavorNum = scrapy.Field()
userId = scrapy.Field()
viewFlag = scrapy.Field()
weight = scrapy.Field()
根據上面的分析得到的json格式,我們可以很輕松的定義出item
- Spider編寫
import urllib2
import os
import re
import codecs
import json
import sys
from scrapy import Spider
from scrapy.selector import Selector
from MySpider.items import tbModelItem,tbThumbItem
from scrapy.http import Request
from scrapy.http import FormRequest
from scrapy.utils.response import open_in_browser
reload(sys)
sys.setdefaultencoding('utf8')
class tbmmSpider(Spider):
name = "tbmm"
allow_domians = ["mm.taobao.com"]
custom_settings = {
"DEFAULT_REQUEST_HEADERS":{
'authority':'mm.taobao.com',
'accept':'application/json, text/javascript, */*; q=0.01',
'accept-encoding':'gzip, deflate',
'accept-language':'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4',
'origin':'https://mm.taobao.com',
'referer':'https://mm.taobao.com/search_tstar_model.htm?spm=719.1001036.1998606017.2.KDdsmP',
'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36',
'x-requested-with':'XMLHttpRequest',
'cookie':'cna=/oN/DGwUYmYCATFN+mKOnP/h; tracknick=adimtxg; _cc_=Vq8l%2BKCLiw%3D%3D; tg=0; thw=cn; v=0; cookie2=1b2b42f305311a91800c25231d60f65b; t=1d8c593caba8306c5833e5c8c2815f29; _tb_token_=7e6377338dee7; CNZZDATA30064598=cnzz_eid%3D1220334357-1464871305-https%253A%252F%252Fmm.taobao.com%252F%26ntime%3D1464871305; CNZZDATA30063600=cnzz_eid%3D1139262023-1464874171-https%253A%252F%252Fmm.taobao.com%252F%26ntime%3D1464874171; JSESSIONID=8D5A3266F7A73C643C652F9F2DE1CED8; uc1=cookie14=UoWxNejwFlzlcw%3D%3D; l=Ahoatr-5ycJM6M9x2/4hzZdp6so-pZzm; mt=ci%3D-1_0'
},
"ITEM_PIPELINES":{
'MySpider.pipelines.tbModelPipeline': 300
}
}
def start_requests(self):
url = "https://mm.taobao.com/tstar/search/tstar_model.do?_input_charset=utf-8"
requests = []
for i in range(1,60):
formdata = {"q":"",
"viewFlag":"A",
"sortType":"default",
"searchStyle":"",
"searchRegion":"city:",
"searchFansNum":"",
"currentPage":str(i),
"pageSize":"100"}
request = FormRequest(url,callback=self.parse_model,formdata=formdata)
requests.append(request)
return requests
def parse_model(self,response):
jsonBody = json.loads(response.body.decode('gbk').encode('utf-8'))
models = jsonBody['data']['searchDOList']
modelItems = []
for dict in models:
modelItem = tbModelItem()
modelItem['avatarUrl'] = dict['avatarUrl']
modelItem['cardUrl'] = dict['cardUrl']
modelItem['city'] = dict['city']
modelItem['height'] = dict['height']
modelItem['identityUrl'] = dict['identityUrl']
modelItem['modelUrl'] = dict['modelUrl']
modelItem['realName'] = dict['realName']
modelItem['totalFanNum'] = dict['totalFanNum']
modelItem['totalFavorNum'] = dict['totalFavorNum']
modelItem['userId'] = dict['userId']
modelItem['viewFlag'] = dict['viewFlag']
modelItem['weight'] = dict['weight']
modelItems.append(modelItem)
return modelItems
代碼不長,一點一點來分析:
1. 由于分析這個頁面并不需要遞歸遍歷網頁,所以就不要crawlSpider了,只繼承最簡單的spider
2. custome_setting可用于自定義每個spider的設置,而setting.py中的都是全局屬性的,當你的scrapy工程里有多個spider的時候這個custom_setting就顯得很有用了
3. ITEM_PIPELINES,自定義管道模塊,當item獲取到數據后會調用你指定的管道處理命令,這個后面會貼上代碼,因為這個不影響本文的內容,數據的處理可以因人而異。
4. 依然重寫start_request,帶上必要的參數請求我們分析得到的借口url,這里我省了一個懶,只遍歷了前60頁的數據,各位當然可以先調用1次借口確定總的頁數(totalPage)之后再寫這個for循環。
5. parse函數里利用json庫解析了返回來得數據,賦值給item的相應字段
3.數據后續處理
數據處理也就是我上面配置ITEM_PIPELINES的目的,這里,我將獲取到的item數據存儲到了本地的mysql數據中,各位也可以通過FEED_URL參數直接輸出json格式文本文件
import MySQLdb
class tbModelPipeline(object):
def process_item(self,item,spider):
db = MySQLdb.connect("localhost","用戶名","密碼","spider")
cursor = db.cursor()
db.set_character_set('utf8')
cursor.execute('SET NAMES utf8;')
cursor.execute('SET CHARACTER SET utf8;')
cursor.execute('SET character_set_connection=utf8;')
sql ="INSERT INTO tb_model(user_id,avatar_url,card_url,city,height,identity_url,model_url,real_name,total_fan_num,total_favor_num,view_flag,weight)\
VALUES('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')"%(item['userId'],item['avatarUrl'],item['cardUrl'],item['city'],item['height'],item['identityUrl'],\
item['modelUrl'],item['realName'],item['totalFanNum'],item['totalFavorNum'],item['viewFlag'],item['weight'])
try:
print sql
cursor.execute(sql)
db.commit()
except MySQLdb.Error,e:
print "Mysql Error %d: %s" % (e.args[0], e.args[1])
db.close()
return item
更重要的內容
獲取所有的淘女郎的基本信息并不是淘女郎這個網站的全部內容,還有一些更有意思的數據,比如:
點擊進入模特的頁面之后發現左側會有有個相冊選項卡,點擊后右邊出現了各種相冊,而每個相冊里面都是各種各樣的模特照片
通過network的分析,這些頁面的數據通通都是Ajax請求獲得的,具體的接口如下:
- 獲取相冊列表的接口是一個GET請求,其中只有一個很重要的user_id,而這個user_id在上面拿去模特的基本信息已經拿到了,還有個page參數用于標識獲取的是第幾頁數據(由于這個是第一頁,并沒有在url中顯現出來,可以通過返回的html中包含的totalPage元素獲得)不過這個接口的返回就不是標準的json格式了,而是一段html,這時候又到了利用scrapy中提供的強大的xpath功能了
def parse_album(self,response):
sel = Selector(response)
tbThumbItems = []
thumb_url_list = sel.xpath("http://div[@class='mm-photo-cell-middle']//h4//a/@href").extract()
thumb_name_list = sel.xpath("http://div[@class='mm-photo-cell-middle']//h4//a/text()").extract()
user_id = response.meta['user_id']
for i in range(0,len(thumb_url_list)-1):
thumbItem = tbThumbItem()
thumbItem['thumb_name'] = thumb_name_list[i].replace('\r\n','').replace(' ','')
thumbItem['thumb_url'] = thumb_url_list[i]
thumbItem['thumb_userId'] = str(user_id)
temp = self.urldecode(thumbItem['thumb_url'])
thumbItem['thumb_id'] = temp['album_id'][0]
tbThumbItems.append(thumbItem)
return tbThumbItems
- 獲取相冊里照片的接口就是一個完全的json格式的接口了,其中參數包括我們已經拿到的user_id以及album_id,page的最大范圍totalPage依然可以通過第一次返回的response中的totalPage字段獲得
總結
- 這種通過分析Ajax接口直接調用獲取原始數據應該是效率最高的抓取數據方式,但并不是所有的Ajax頁面都適用,還是要具體對待,比如我們上面獲取相冊列表當中就要去分析html來獲得相冊的基本信息。
- 獲取相冊和相冊里的照片列表寫的比較簡略,基本沒展示什么代碼,這樣寫是有原因的:一個是因為我已經掛了代碼的鏈接,而且后面這兩部分的原理和我主要講的第一部分獲取模特信息的原理基本類似,不想花太多的篇幅花在這種重復的內容上,另外一個我希望想掌握Scrapy的同學能在明白我第一部分的講解下自己能順利完成后面的工作,遇到不明白的時候可以看看我Github上的源碼,看看有什么不對的地方,只有自己寫一遍才能掌握,這是編程界的硬道理。