Scrapy抓取Ajax動態(tài)頁面

一般來說爬蟲類框架抓取Ajax動態(tài)頁面都是通過一些第三方的webkit庫去手動執(zhí)行html頁面中的js代碼, 最后將生產(chǎn)的html代碼交給spider分析。本篇文章則是通過瀏覽器提供的Debug工具分析Ajax頁面的具體請求內(nèi)容,找到獲取數(shù)據(jù)的接口url,直接調(diào)用該接口獲取數(shù)據(jù),省去了引入python-webkit庫的麻煩,而且由于一般ajax請求的數(shù)據(jù)都是結(jié)構(gòu)化數(shù)據(jù),這樣更省去了我們利用xpath解析html的痛苦。

這次我們要抓取的網(wǎng)站是淘女郎的頁面,全站都是通過Ajax獲取數(shù)據(jù)然后重新渲染生產(chǎn)的。

這篇文章的代碼已上傳至我的Github,由于后面有部分內(nèi)容并沒有提供完整代碼,所以貼上地址供各位參考。

分析工作

用Chrome打開淘女郎的首頁中的美人庫,這個頁面毫無疑問是會展示所有的模特的信息,同時打開Debug工具,在network選項中查看瀏覽器發(fā)送了哪些請求?

2016-07-04_16:11:01.jpg

在截圖的左下角可以看到總共產(chǎn)生了86個請求,那么有什么辦法可以快速定位到Ajax請求的鏈接了,利用Network當(dāng)中提供的Filter功能,選中Filter,最后選擇右邊的XHR過濾(XHR時XMLHttpRequest對象,一般Ajax請求的數(shù)據(jù)都是結(jié)構(gòu)化數(shù)據(jù)),這樣就剩下了為數(shù)不多的幾個請求,剩下的就靠我們自己一個一個的檢查吧

2016-07-04_16:22:18.jpg

很幸運,通過分析每個接口返回的request和response信息,發(fā)現(xiàn)最后一個請求就是我們需要的接口url

2016-07-04_16:25:56.jpg

Request中得參數(shù)很簡單,根據(jù)英文意思就可以猜出意義,由于我們要抓取所有模特的信息,所以不需要定制這些參數(shù),后面直接將這些參數(shù)post給接口就行了

2016-07-04_16:29:06.jpg

在Response中可以獲得到的有用數(shù)據(jù)有兩個:所有模特信息的列表searchDOList、以及總頁數(shù)totolPage

2016-07-04_16:35:05.jpg

searchDOList列表中得對象都有如上圖所示的json格式,它也正是我們需要的模特信息的數(shù)據(jù)

Scrapy編碼

  1. 定義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()

根據(jù)上面的分析得到的json格式,我們可以很輕松的定義出item

  1. 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. 由于分析這個頁面并不需要遞歸遍歷網(wǎng)頁,所以就不要crawlSpider了,只繼承最簡單的spider
2. custome_setting可用于自定義每個spider的設(shè)置,而setting.py中的都是全局屬性的,當(dāng)你的scrapy工程里有多個spider的時候這個custom_setting就顯得很有用了
3. ITEM_PIPELINES,自定義管道模塊,當(dāng)item獲取到數(shù)據(jù)后會調(diào)用你指定的管道處理命令,這個后面會貼上代碼,因為這個不影響本文的內(nèi)容,數(shù)據(jù)的處理可以因人而異。
4. 依然重寫start_request,帶上必要的參數(shù)請求我們分析得到的借口url,這里我省了一個懶,只遍歷了前60頁的數(shù)據(jù),各位當(dāng)然可以先調(diào)用1次借口確定總的頁數(shù)(totalPage)之后再寫這個for循環(huán)。
5. parse函數(shù)里利用json庫解析了返回來得數(shù)據(jù),賦值給item的相應(yīng)字段

3.數(shù)據(jù)后續(xù)處理

數(shù)據(jù)處理也就是我上面配置ITEM_PIPELINES的目的,這里,我將獲取到的item數(shù)據(jù)存儲到了本地的mysql數(shù)據(jù)中,各位也可以通過FEED_URL參數(shù)直接輸出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

更重要的內(nèi)容

獲取所有的淘女郎的基本信息并不是淘女郎這個網(wǎng)站的全部內(nèi)容,還有一些更有意思的數(shù)據(jù),比如:

點擊進入模特的頁面之后發(fā)現(xiàn)左側(cè)會有有個相冊選項卡,點擊后右邊出現(xiàn)了各種相冊,而每個相冊里面都是各種各樣的模特照片

2016-07-04_17:04:22.jpg
2016-07-04_17:04:49.jpg

通過network的分析,這些頁面的數(shù)據(jù)通通都是Ajax請求獲得的,具體的接口如下:

2016-07-04_17:09:51.jpg
2016-07-04_17:10:16.jpg
  1. 獲取相冊列表的接口是一個GET請求,其中只有一個很重要的user_id,而這個user_id在上面拿去模特的基本信息已經(jīng)拿到了,還有個page參數(shù)用于標(biāo)識獲取的是第幾頁數(shù)據(jù)(由于這個是第一頁,并沒有在url中顯現(xiàn)出來,可以通過返回的html中包含的totalPage元素獲得)不過這個接口的返回就不是標(biāo)準(zhǔn)的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
  1. 獲取相冊里照片的接口就是一個完全的json格式的接口了,其中參數(shù)包括我們已經(jīng)拿到的user_id以及album_id,page的最大范圍totalPage依然可以通過第一次返回的response中的totalPage字段獲得
2016-07-04_17:25:23.jpg
2016-07-04_17:25:46.jpg

總結(jié)

  1. 這種通過分析Ajax接口直接調(diào)用獲取原始數(shù)據(jù)應(yīng)該是效率最高的抓取數(shù)據(jù)方式,但并不是所有的Ajax頁面都適用,還是要具體對待,比如我們上面獲取相冊列表當(dāng)中就要去分析html來獲得相冊的基本信息。
  2. 獲取相冊和相冊里的照片列表寫的比較簡略,基本沒展示什么代碼,這樣寫是有原因的:一個是因為我已經(jīng)掛了代碼的鏈接,而且后面這兩部分的原理和我主要講的第一部分獲取模特信息的原理基本類似,不想花太多的篇幅花在這種重復(fù)的內(nèi)容上,另外一個我希望想掌握Scrapy的同學(xué)能在明白我第一部分的講解下自己能順利完成后面的工作,遇到不明白的時候可以看看我Github上的源碼,看看有什么不對的地方,只有自己寫一遍才能掌握,這是編程界的硬道理。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容