以今日頭條為例分析Ajax請求抓取網頁數據。本次抓取今日頭條的街拍關鍵字對應的圖片,並保存到本地
一,分析
打開今日頭條主頁,在搜索框中輸入街拍二字,打開開發者工具,發現瀏覽器顯示的數據不在其源碼里面。這樣可以出初步判斷這些內容是由
Ajax加載,然后使用JavaScript渲染出來的。
切換到XHR過濾選項卡,查看其Ajax請求。點擊其中一條進去,進入data展開,發現其中一個title字段對應的值正好是頁面中的某條數據的標題。再查看其他數據,正好也是一一對應的,這說明這些數據確實是由Ajax加載的。
本次的目的是抓取其中的圖片內容,data中每個元素就是一篇文章,元素中的image_list字段包含了該文章的圖片內容。它是一個列表形式,包含了所有的圖片列表。我們只需要將列表中的url字段下載下來就好了,每篇文章都創建一個文件夾,文件夾名稱即文章標題。
在使用Python爬取之前還需要分析一下URL的規律。切換到Headers選項卡,查看Headers信息。可以看到,這是一個GET請求,請求的參數有aid,app_name,offset,format,keyword,autoload,count,en_qc,cur_tab,from,pd,timestamp。繼續往下滑動,多加載一些數據,找出其中的規律。
經過觀察,可以發現變化的參數只有offset,timestamp。第一次請求的offset的值為0,第二次為20,第三次為40,key推斷出這個offset就是偏移量,count為每次請求的數據量,而timestamp為時間戳。這樣一來,我們就可以使用offset參數控制分頁了,通過模擬Ajax請求獲取數據,最后將數據解析后下載即可。
二,爬取
剛才已經分析完了整個Ajax請求,接下來就是使用代碼來實現這個過程。
# _*_ coding=utf-8 _*_ import requests import time import os from hashlib import md5 from urllib.parse import urlencode from multiprocessing.pool import Pool def get_data(offset): """ 構造URL,發送請求 :param offset: :return: """ timestamp = int(time.time()) params = { 'aid': '24', 'app_name': 'web_search', 'offset': offset, 'format': 'json', 'autoload': 'true', 'count': '20', 'en_qc': '1', 'cur_tab': '1', 'from': 'search_tab', 'pd': 'synthesis', 'timestamp': timestamp } base_url = 'https://www.toutiao.com/api/search/content/?keyword=%E8%A1%97%E6%8B%8D' url = base_url + urlencode(params) try: res = requests.get(url) if res.status_code == 200: return res.json() except requests.ConnectionError: return '555...' def get_img(data): """ 提取每一張圖片連接,與標題一並返回,構造生成器 :param data: :return: """ if data.get('data'): page_data = data.get('data') for item in page_data: # cell_type字段不存在的這類文章不爬取,它沒有title,和image_list字段,會出錯 if item.get('cell_type') is not None: continue title = item.get('title').replace(' |', ' ') # 去掉某些可能導致文件名錯誤而不能創建文件的特殊符號,根據具體情況而定 imgs = item.get('image_list') for img in imgs: yield { 'title': title, 'img': img.get('url') } def save(item): """ 根據title創建文件夾,將圖片以二進制形式寫入, 圖片名稱使用其內容的md5值,可以去除重復的圖片 :param item: :return: """ img_path = 'img' + '/' + item.get('title') if not os.path.exists(img_path): os.makedirs(img_path) try: res = requests.get(item.get('img')) if res.status_code == 200: file_path = img_path + '/' + '{name}.{suffix}'.format( name=md5(res.content).hexdigest(), suffix='jpg') if not os.path.exists(file_path): with open(file_path, 'wb') as f: f.write(res.content) print('Successful') else: print('Already Download') except requests.ConnectionError: print('Failed to save images') def main(offset): data = get_data(offset) for item in get_img(data): print(item) save(item) START = 0 END = 10 if __name__ == "__main__": pool = Pool() offsets = ([n * 20 for n in range(START, END + 1)]) pool.map(main, offsets) pool.close() pool.join()
這里定義了起始頁START和結束頁END,可以自定義設置。然后利用多進程的進程池,調用map()方法實現多進程下載。運行之后發現圖片都報存下來了。