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()方法實現多線程下載

               
相關文章
相關標籤/搜索