Python | 分析 Ajax 爬取今日頭條街拍美圖

微信公衆號:一個優秀的廢人
如有問題或建議,請後臺留言,我會盡力解決你的問題。

安吉麗娜朱莉

前言

許久不見,我是阿狗。我的主業是 java web 開發,學 Python 只是單純地認爲這是一個風口,我不是豬,也想飛起來。所以,我選擇在業餘時間先學爲敬。今天給你們帶來的是今日頭條街拍美圖的爬取:分析 Ajax 爬取今日頭條街拍美圖。

環境

這次實戰採取的是 wi10 + python3.6 + PyCharm 的開發環境。另外,還用到了 requests 、urllib、hashlib、multiprocessing 這幾個庫。其中,requests 用於網絡請求;urllib 的 urlencode 模塊用於構造請求參數;hashlib 的 md5 模塊用於構建一個唯一的圖片名,防止重複,造成出錯;multiprocessing 的 pool 模塊用於開啓多線程,加快爬取速度。

思路

爬蟲之前,第一就是明確我們的爬取對象,也就是我的目的是爬取這個網站的什麼東西,那麼這裏我們爬取的是搜索結果前二十頁,每一頁每一項的組圖以及對應每組的標題。

首先是打開今日頭條網頁版首頁 https://www.toutiao.com/。在搜索框輸入街拍,之後打開開發者工具(在瀏覽器當前頁面按F12)分析網頁請求參數。

搜索結果

在開發者工具可以看見,在 Network 下的 All 選項卡中的請求是非常多的,我們無法分辨哪一個是真正的網頁 Ajax 請求。這時切換到 XHR 選項卡,這個選項卡里面出現的就是 Ajax 請求。那我們嘗試打開請求的參數以及返回的內容是否與頁面匹配。

查找爬取對象

點擊該請求,切換到 preview 選項下,這裏就是 chrome 開發者工具 json 格式化的該請求的返回結果。結果中有一個 data 字段,這個字段包含了當前頁面的所有美圖,展開第一個之後發現它的 title 字段內容就跟我們頁面上渲染出來的內容相匹配。而每一項都有一個 image_list 字段,這個字段包含了這一項的所有圖片。 如上圖所示,title 表示第一項的標題,image_list 表示這一項的組圖。繼續展開 image_list 分析。

image_list分析

如上圖,看見 image_list 展開後的 url 就是我們要爬取的圖片所在。也就是說我們打開搜索頁面之後,還要獲取 image_list 裏面包含的 url ,再次訪問這些 url 才能得到我們想要的圖片,image_list 裏面的每一個 url 就代表該組內的每一張圖片。那請求參數是什麼呢?

請求參數及規律

點擊該請求 https://www.toutiao.com/search_content/?offset=0&format=json&keyword=%E8%A1%97%E6%8B%8D&autoload=true&count=20&cur_tab=1&from=search_tab,切換到 header 選項下,發現請求參數如圖,這是一個 get 請求,請求參數有 offset、 format、keyword、autoload、count、cur_tab、from;而每次下拉加載只有 offset 是變化的。每次請求 +20。也就是每次下拉請求當前頁面就會多加載出 20 組圖片。至此真相大白,我們要爬取的內容找到了,請求參數的規律也有了。下面就進入實戰演練。

加載單個 Ajax 請求

實現 get_page 方法用於加載單個 Ajax 請求,其中 offset 是變化的。所以把它當做參數傳遞進來。代碼如下:

def get_page(offset):
    #構造參數
    params = {
        'offset': offset,
        'format': 'json',
        'keyword': '街拍',
        'autoload': 'true',
        'count': '20',
        'cur_tab': '1',
        'from': 'search_tab',
    }
    # headers, 僞裝成瀏覽器
    headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                    'AppleWebKit/537.36 (KHTML, like Gecko) '
                    'Chrome/68.0.3440.106 Safari/537.36'
    }
    url = 'https://www.toutiao.com/search_content/?' + urlencode(params)
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
    except requests.ConnectionError:
        return None

其中要注意的就是,header 參數一定要加上。否則的話,瀏覽器會認爲你是非法請求從而報錯。header 在你當前請求的 header 窗口可以直接複製,不懂直接網上搜就懂了。

解析方法

實現一個解析方法,用於提取每條數據的 image_list 字段的每一張圖片鏈接,將圖片鏈接以及圖片所屬標題一併返回,此時可以構造一個生成器(不懂的,看前面的 Python 基礎文章,或者看菜鳥教程)。代碼如下:

def get_images(json):
    if json.get('data'):
        for item in json.get('data'):
            title = item.get('title', "nasus")
            images = item.get('image_list', [])
            for image in images:
                yield{
                  'image': 'http:'+image.get('url'),
                  'title': title
                }

這裏需要提一下的是第五行後面的 [ ]。這裏加上是防止某些 image_list 爲空類型,造成無法生成迭代器而報錯。

保存圖片

實現一個保存圖片的方法,其中 item 就是前面 get_images 方法返回的一個字典,在該方法中以該 item 的 title 來創建文件夾,然後請求該圖片鏈接,獲取其二進制數據並寫入文件。圖片名稱使用其內容的 md5 值防止重複。代碼如下:

def save_image(item):
    if not os.path.exists(item.get('title')):
        os.mkdir(item.get('title'))
    try:
        # headers, 僞裝成瀏覽器
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) '
                          'Chrome/68.0.3440.106 Safari/537.36'
        }
        response = requests.get(item.get('image'), headers=headers)
        if response.status_code == 200:
            file_path = '{0}/{1}.{2}'.format(item.get('title'), md5(response.content).hexdigest(), 'jpg')
            if not os.path.exists(file_path):
                with open(file_path, 'wb') as f:
                    f.write(response.content)
            else:
                print('Already Download', file_path)
    except requests.ConnectionError:
        print('Fail to Save Image')

啓動函數

只需要構造一個 offset 數組,開啓多線程遍歷 offset ,提取圖片鏈接,訪問並將其下載即可。
代碼如下:

def main(offset):
    json = get_page(offset)
    for item in get_images(json):
        print(item)
        save_image(item)


GROUP_START = 1
GROUP_END = 20

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()
    # for i in range(GROUP_END):
    #     main(i*GROUP_END)

這樣整個程序就完成了。運行之後發現每組街拍美圖都按標題分文件夾保存下來了。

爬取結果
最後附上完整代碼:

import requests
import os
from urllib.parse import urlencode
from hashlib import md5
from multiprocessing.pool import Pool

def get_page(offset):
    #構造參數
    params = {
        'offset': offset,
        'format': 'json',
        'keyword': '街拍',
        'autoload': 'true',
        'count': '20',
        'cur_tab': '1',
        'from': 'search_tab',
    }
    # headers, 僞裝成瀏覽器
    headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                    'AppleWebKit/537.36 (KHTML, like Gecko) '
                    'Chrome/68.0.3440.106 Safari/537.36'
    }
    url = 'https://www.toutiao.com/search_content/?' + urlencode(params)
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
    except requests.ConnectionError:
        return None

def get_images(json):
    if json.get('data'):
        for item in json.get('data'):
            title = item.get('title', "nasus")
            images = item.get('image_list', [])
            for image in images:
                yield{
                  'image': 'http:'+image.get('url'),
                  'title': title
                }

def save_image(item):
    if not os.path.exists(item.get('title')):
        os.mkdir(item.get('title'))
    try:
        # headers, 僞裝成瀏覽器
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                          'AppleWebKit/537.36 (KHTML, like Gecko) '
                          'Chrome/68.0.3440.106 Safari/537.36'
        }
        response = requests.get(item.get('image'), headers=headers)
        if response.status_code == 200:
            file_path = '{0}/{1}.{2}'.format(item.get('title'), md5(response.content).hexdigest(), 'jpg')
            if not os.path.exists(file_path):
                with open(file_path, 'wb') as f:
                    f.write(response.content)
            else:
                print('Already Download', file_path)
    except requests.ConnectionError:
        print('Fail to Save Image')

def main(offset):
    json = get_page(offset)
    for item in get_images(json):
        print(item)
        save_image(item)

GROUP_START = 1
GROUP_END = 20

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()
    # for i in range(GROUP_END):
    #     main(i*GROUP_END)

通過這篇文章,初步瞭解了 Ajax 的分析過程,以及 Ajax 的分頁模擬和圖片下載過程,代碼非常的簡單,但是也建議初學者自己動手實踐一下,雖然很簡單,但千萬不能有所見即所得的想法,有時你可能會遇到意想不到的坑,所謂大神也是踩坑、填坑不斷循環這個過程鍛鍊而來的。

最後,如果對 Python 、Java 感興趣請長按二維碼關注一波,我會努力帶給你們價值,讚賞就不必了,能力沒到,受之有愧。我會告訴你關注之後回覆 爬蟲 可以領取一份最新的爬蟲教學視頻嗎?


一個優秀的廢人

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章