Class 16 - 1 Ajax 数据爬取

Ajax简介:ajax

  • Ajax ,全称为 Asynchronous JavaScript and XML ,即异步的 JavaScript XML 它是利用 JavaScript 在保证页面不被刷新、页面连接不改变的状况下与服务器交换数据并更新部分网页的技术。页面在后台与服务器进行了数据交互,获取到数据以后,再利用 JavaScript 改变网页,这样网页内容就会更新了。  http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp。
    •  Ajax 加载数据 就是发送了一个 Ajax 请求,能够用 requests 来模拟 Ajax 请求,就能够成功抓取数据了。
  1. 基本原理
    • 发送 Ajax 请求到网页更新的这个过程能够简单分为如下 3 步:
    1. 发送请求;
    2. 解析内容;
    3. 渲染网页;
      • 发送请求
        • JavaScript 能够实现页面的各类交互功能,Ajax 也是由 JavaScript 实现的,实际上执行了以下代码:
          var xmlhttp;
          if (windows.XMLHttpRequest) {
          // code for IE7+, Firefox, Chrome, Opera, Safari xmlhttp =new XMLHttpRequest();
          } else {// code for IE6, IE5
              xmlhttp =new ActiveXObject("Microsoft.XMLHTTP");
          }
          xmlhttp.open("POST","/ajax/",true);
          xmlhttp.send();   

          JavaScript 对 Ajax 最底层的实现,实际上就是新建了 XMLHttpRequest 对象,而后调用 onreadystatechange 属性设置了监听,而后调用 open()和 send ()方法向某个连接(也就是服务器)送了请求。用 Python 实现请求发送以后,能够获得响应结果,但这里请求的发送变成 JavaScript 来完成。因为设置了监听,因此当服务器返回响应时, onreadystatechange 对应的方法便会被触发,而后在这个方法里面解析响应内容便可。数据库

      • 解析内容json

        • 获得响应以后,onreadystatechange 属性对应的方法便会被触发,此时利用 xmlhttp 的 responseText 属性即可取到响应内容。相似于 Python 中利用 requests 向服务器发起请求,而后获得响应的过程。那么返回内容多是 HTML,多是 JSON,接下来只须要在方法中用 JavaScript 进一步处理便可。如:若是是 JSON 的话,能够进行解析和转化   windows

      • 渲染网页
        • JavaScrip 有改变网页内容的能力,解析完响应内容以后,就能够调用 JavaScript 来针对解析完的 内容对网页进行下一步处理了。好比,经过 document.getElementByid().innerHTML 这样的操做,即可以对某个元素内的源代码进行更改,这样网页显示的内容就改变了,这样的操做也被称做 DOM 操做,即对 Document 网页文档进行操做,如更改、删除等。
        • document.getElementByid("myDiv").innerHTML=xmlhttp. responseText 便将 ID 为 myDiv 的节点内部的 HTML 代码更改成服务器返回的内容,这样 myDiv 元素内部便会呈现出服务器返回的数据,网页的部份内容看上去就更新了。
        • 这3个步骤其实都是由 JavaScript 完成的,它完成了整个请求 、解析和渲染的过程。
        • 再回想微博的下拉刷新,其实就是 JavaScript 向服务器发送了 Ajax 请求,而后获取新的微博数据,将其解析,并将其谊染在网页中。

1、Ajax分析方法            api

  1. 查看请求
    1. 须要借助浏览器的开发者工具,以 Chrome 浏览器为例介绍。(用 Chrome 浏览器打开 博的连接 https://m.weibo.cn/u/2830678474,选择 检查”选项)
    2. Ajax 有其特殊的请求类型 ,叫做 xhr。一个名称以 getlndex 头的请求,其 Type 为 xhr,就是 Ajax 请求。
    3. 点击请求后,其中 Request Headers 中有 个信息为 X-Requested-With:XMLHt Request ,这就标记了此请求是 Ajax 请求。
    4. 随后点击 Preview,便可看到响应的内容,它是 JSON 格式的 这里 Chrome 为咱们自动作 解析,点击箭头便可展开和收起相应内容。这里的返回结果是我的信息,如昵称、简介、头像等,这也是用来渲染我的 页所使用的数据 JavaScript 接收到这些数据以后,再执行相应的渲染方法,整个页面就渲染出来了。   也能够切换到 Response 选项卡,从中观察到真实的返回数据
    5. 切回到第一个请求,观察 Response :最原始的连接 https://m.weibo.cn/u/2830678474 返回的结果,其代码只有不到 50 行,结构也简单,只执行了一些 JavaScript。因此,咱们看到的微博页面的真实数据并非最原始的页面返回的,而是后来执行 JavaScript 后再次向后台发送了 Ajax 请求,浏览器到数据后再进一步渲染出来的
  2. 过滤请求
    1. 再利用 Chrome 开发者具的筛选功能筛选出全部的 Ajax 请求。点击 XHR。
    2. 滑动页面,能够看到页面底部有一条条新的微博被刷出,而开发者工具下方也一个个出现 Ajax 请求,
    3. 随意点开一个条目,均可以清楚地看到 Request URL、Request Headers、Response Headers、Response Body,此时要模拟请求和提取就很是简单了

2、Ajax 结果提取数组

  1. 分析请求
    1. 打开 Ajax 的 XHR 过滤器,加载新的微博内容。选定其中一个请求,分析它的参数信息。点击该请求,进入详情页面
    2. 这些是 GET 类型的请求,请求连接为 https://m.weibo.cn/api/container/getlndex?type=uid&value=2830678474&containerid= I 076032830678474&page=2。请求的参数有4个:type、value、containerid和page
    3. 他们的type、value、containerid 始终如一。type 始终为 uid, value 的值就是页面连接中的数字,这就是用户的 id。containerid 就是 107603 加上用户 id。改变值就是 page,这个参数是用来控制分页的, page=1 表明第 1 页, page=2 表明第二页,以此类推
  2. 分析响应
    • 观察响应内容
    1. 这个内容是 JSON 格式,浏览器开发者工具自动作了解析以方便咱们查看。最关键的两部分信息就是 cardlistlnfo 和 cardscardlistlnfo包含一个比较重要的信息 total,实际上是微博的总数量,能够根据这个数字来估算分页数;cards 是一个列表,它包含 10 个元素。浏览器

    2. 这个元素有一个比较重要的字段 mblog。 展开,可发现它包含的正是微博信息,如 attitudes count (赞数目)、comments_count(评论数目)、reposts_count(转发数目)created at(发布时间)、text(微博正文)等,并且都是些格式化的内容
    3. 咱们请求一个接口,就能够获得 10 条微博,并且请求时只须要改变 page 参数便可。只需一个简单循环,就能够获取全部微博了      
  3. 实例模拟Ajax请求
    1. 首先,定义一个方法来获取每次请求的结果。在请求时,page是一个可变参数,因此将它做为方法的参数传递进来,代码:
      from urllib.parse import urlencode
      import requests
      base_url = 'https://m.weibo.cn/api/container/getlndex?'
      
      headers = {
          'Host': 'm.weibo.cn',
          'Referer': 'https://m.weibo.cn/u/2830678474',
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',
          'X-Requested-With': 'XMLHttpRequest',
          }
      
      def get_page(page):
          params = {
                  'type': 'uid',
                  'value': '2830678474',
                  'containerid': '1076032830678474',
                  'page': page
              }
          url = base_url + urlencode(params)
          try:
              response = requests.get(url, headers=headers)
              if response.status_code == 200:
                  return response.json()
          except requests.ConnectionError as e:
              print( 'Error' , e.args)
      也可:
      from pyquery import PyQuery as pq
      import requests
      
      def getPage(page):
          url = 'https://m.weibo.cn/api/container/getIndex?'#在怎么来?分析Ajax请求
          hd = {"User-Agent": 'Mozilla'}  # 模仿浏览器
          params = {'type': 'uid',
                    'value': '2830678474',
                    'containerid': '1076032830678474',
                    'page': page}  # 做为参数增长到url中去
      
          try:
              response = requests.get(url, headers=headers, params=params)
              response.raise_for_status()
              return response.json()  # 解析为JSON返回
          
          --snip--
      View Code

      首先,定义 base_url 来表示请求的 URL 的前半部分。而后,构造参数字典,其中 type、value、containerid 是固定参数, page 是可变参数。调用 urlencode()方法将参数转化为 -- URL的 GET 请求参数,相似于 type=uid&value=2830678474&containerid=1076032830678474&page=2 这样的形式。base_url 与参数拼合造成一个新的 URL。判断响应的状态码,若是是 200 ,则直接调用 json()方法将内容解析为JSON 返回,不然不返回任何信息。若是出现异常,则捕获并输出其异常信息。服务器

    2. 须要定义一个解析方法,用来从结果中提取想要的信息,如此次想保存微博的 id、正文、赞数、评论数和转发数这几个内容,能够先遍历 cards,而后获取 mblog 中的各个信息,赋值为一个新的字典返回:
      from pyquery import PyQuery as pq
      
      def parse_page(json):
          if json:
              items = json.get('data').get('cards')
              for item in items:
                  item = item.get('mblog')
                  if item == None:      #注意:若是不添加返回None,可能由于有部分无返回值致使报错
                      continue
                  weibo = {}
                  weibo['id'] = item.get('id')
                  weibo['text'] = pq(item.get('text')).text()
                  weibo['attitudes'] = item.get('attitudes_count')
                  weibo['comments'] = item.get('comments_count')
                  weibo['reposts'] = item.get('reposts_count')
                  yield weibo

      借助 pyquery 将正文中的 HTML签去掉。网络

    3. 遍历 page,将提取到的结果输出:多线程

      if __name__ == '__main__':
          for page in range(1,11):
              json = get_page(page)
              results = parse_page(json)
              for result in results:
                  print(result)
    4. 将结果保存到 MongoDB 数据库:
      from pymongo import MongoClient
      client = MongoClient()
      db = client['weibo']
      collection =db['weibo']
      
      def save_to_mongo(result):
          if collection.insert(result):
              print('Save to mongo')

3、分析 Ajax爬取今日头条街拍美图

  1. 抓取分析
    • 打开第一个网络请求,这个请求的 URL 就是当前的连接 http://www.toutiao.com/search/?keyword=街拍,打开 Preview 选项卡查看 Response Body。 若是页面中的内容是根据第一个请求获得的结果渲染出来的,那么第一个请求的源代码中必然会包含页面结果中的文字。能够尝试搜索结果的标题,如“路人”。   若是网页源代码中并无包含这两个 ,搜索匹配结果数目为0。能够初步判断这 些内容是由 Ajax 加载,而后用 JavaScript 渲染出来的。接下来,能够切换到 XHR 过滤选项卡, 查看有没有 Ajax 请求。   
  2. 实战演练
    • 实现方法 get_page()来加载单个 Ajax 请求的结果。其中惟一变化的参数就是 offset ,因此将它看成参数传递:
      • 第二次请求的 offset 值为20,第三次为 40,第四次为 60 ,因此 offset 值就是偏移量。
        import requests
        from urllib.parse import urlencode
        
        def get_page(offset):
            params = {
                'offset': offset,
                'format': 'json',
                'keyword': '街拍',
                'autoload': 'true',
                'count': '20',
                'cur_tab': '1',
                'from': 'search_tab'
            }
            base_url = 'https://www.toutiao.com/search_content/?'
            url = base_url + urlencode(params)
            try:
                resp = requests.get(url)
                if codes.ok == resp.status_code:
                    return resp.json()
            except requests.ConnectionError:
                return None

        这里用 urlencode()方法构造请求的 GET 参数,而后用 requests 请求这个连接,若是返回状 态码为 200(ok),则调用 response 的 json()方法将结果转为 JSON 格式,而后返回。

      • 再实现一个解析方法:提取每条数据的 image_list 字段中的每一张图片连接,将图片连接和图片所属的标题一并返回,此时能够构造一个生成器。实现代码:

        def get_images(json):
            if json.get('data'):
                data = json.get('data')
                for item in data:
                    if item.get('cell_type') is not None:
                        continue
                    title = item.get('title')
                    images = item.get('image_list')for image in images:
                        yield {
                            'image': 'https:' + image.get('url'),
                            'title': title
                        }

         

      • 接下来,实现一个保存图片的方法 save_image(),其中 item 是前面 get_images()方法返回的字典。首先根据 item 的 title 来建立文件夹,而后请求图片连接,获取图片的二进制数据,以二进制的形式写入文件。图片的名称可使用其内容的 MD5 值,这样能够去除重复。代码:

        import os
        from hashlib import md5
        
        def save_image(item):
            img_path = 'img' + os.path.sep + item.get('title')
            if not os.path.exists(img_path):
                os.makedirs(img_path)
            try:
                resp = requests.get(item.get('image'))
                if codes.ok == resp.status_code:
                    file_path = img_path + os.path.sep + '{file_name}.{file_suffix}'.format(
                        file_name=md5(resp.content).hexdigest(),
                        file_suffix='jpg')
                    if not os.path.exists(file_path):
                        with open(file_path, 'wb') as f:
                            f.write(resp.content)
                        print('Downloaded image path is %s' % file_path)
                    else:
                        print('Already Downloaded', file_path)
            except requests.ConnectionError:
                print('Failed to Save Image,item %s' % item)

         

      • 最后,只须要构造一个时fset 数组,遍历 offset ,提取图片连接,并将其下载:

        from multiprocessing.pool import Pool
        
        def main(offset):
            json = get_page(offset)
            for item in get_images(json):
                print(item)
                save_image(item)
        
        GROUP_START = 0
        GROUP_END = 7
        
        if __name__ == '__main__':
            pool = Pool()
            groups = ([x * 20 for x in range(GROUP_START, GROUP_END + 1)])
            pool.map(main, groups)
            pool.close()
            pool.join()

        定义分页的起始页数和终止页数,分别为 GROUP_START和 GROUP_END ,利用了多线程的线程池,调用其 map()方法实现多线程下载

               
相关文章
相关标签/搜索