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("//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.jpg
2016-07-04_17:25:46.jpg

总结

  1. 这种通过分析Ajax接口直接调用获取原始数据应该是效率最高的抓取数据方式,但并不是所有的Ajax页面都适用,还是要具体对待,比如我们上面获取相册列表当中就要去分析html来获得相册的基本信息。
  2. 获取相册和相册里的照片列表写的比较简略,基本没展示什么代码,这样写是有原因的:一个是因为我已经挂了代码的链接,而且后面这两部分的原理和我主要讲的第一部分获取模特信息的原理基本类似,不想花太多的篇幅花在这种重复的内容上,另外一个我希望想掌握Scrapy的同学能在明白我第一部分的讲解下自己能顺利完成后面的工作,遇到不明白的时候可以看看我Github上的源码,看看有什么不对的地方,只有自己写一遍才能掌握,这是编程界的硬道理。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容