Scrapy抓取Ajax動態頁面

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

這次我們要抓取的網站是淘女郎的頁面,全站都是通過Ajax獲取數據然后重新渲染生產的。

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

分析工作

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

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

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

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

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

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

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

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

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

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

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

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()

根據上面的分析得到的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. 由于分析這個頁面并不需要遞歸遍歷網頁,所以就不要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

更重要的內容

獲取所有的淘女郎的基本信息并不是淘女郎這個網站的全部內容,還有一些更有意思的數據,比如:

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

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

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

2016-07-04_17:09:51.jpg
2016-07-04_17:10:16.jpg
  1. 獲取相冊列表的接口是一個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
  1. 獲取相冊里照片的接口就是一個完全的json格式的接口了,其中參數包括我們已經拿到的user_id以及album_id,page的最大范圍totalPage依然可以通過第一次返回的response中的totalPage字段獲得
2016-07-04_17:25:23.jpg
2016-07-04_17:25:46.jpg

總結

  1. 這種通過分析Ajax接口直接調用獲取原始數據應該是效率最高的抓取數據方式,但并不是所有的Ajax頁面都適用,還是要具體對待,比如我們上面獲取相冊列表當中就要去分析html來獲得相冊的基本信息。
  2. 獲取相冊和相冊里的照片列表寫的比較簡略,基本沒展示什么代碼,這樣寫是有原因的:一個是因為我已經掛了代碼的鏈接,而且后面這兩部分的原理和我主要講的第一部分獲取模特信息的原理基本類似,不想花太多的篇幅花在這種重復的內容上,另外一個我希望想掌握Scrapy的同學能在明白我第一部分的講解下自己能順利完成后面的工作,遇到不明白的時候可以看看我Github上的源碼,看看有什么不對的地方,只有自己寫一遍才能掌握,這是編程界的硬道理。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容