1. 程式人生 > >Scrapy抓取Ajax動態頁面

Scrapy抓取Ajax動態頁面

https://www.jianshu.com/p/1e35bcb1cf21

這次我們要抓取的網站是淘女郎的頁面,全站都是通過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.jpg2016-07-04_17:04:49.jpg

通過network的分析,這些頁面的資料通通都是Ajax請求獲得的,具體的介面如下:

2016-07-04_17:09:51.jpg2016-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("//div[@class='mm-photo-cell-middle']//h4//a/@href").extract()       
   thumb_name_list = sel.xpath("//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.jpg2016-07-04_17:25:46.jpg

總結

  1. 這種通過分析Ajax介面直接呼叫獲取原始資料應該是效率最高的抓取資料方式,但並不是所有的Ajax頁面都適用,還是要具體對待,比如我們上面獲取相簿列表當中就要去分析html來獲得相簿的基本資訊。
  2. 獲取相簿和相簿裡的照片列表寫的比較簡略,基本沒展示什麼程式碼,這樣寫是有原因的:一個是因為我已經掛了程式碼的連結,而且後面這兩部分的原理和我主要講的第一部分獲取模特資訊的原理基本類似,不想花太多的篇幅花在這種重複的內容上,另外一個我希望想掌握Scrapy的同學能在明白我第一部分的講解下自己能順利完成後面的工作,遇到不明白的時候可以看看我Github上的原始碼,看看有什麼不對的地方,只有自己寫一遍才能掌握,這是程式設計界的硬道理。