本節中,我們以今日頭條為例來嘗試通過分析Ajax請求來抓取網頁資料的方法。這次要抓取的目标是今日頭條的街拍美圖,抓取完成之後,将每組圖檔分檔案夾下載下傳到本地并儲存下來。
1. 準備工作
很多人學習python,不知道從何學起。
很多人學習python,掌握了基本文法過後,不知道在哪裡尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那麼針對這三類人,我給大家提供一個好的學習平台,免費領取視訊教程,電子書籍,以及課程的源代碼!
QQ群:101677771
在本節開始之前,請確定已經安裝好requests庫。如果沒有安裝,可以參考第1章。
2. 抓取分析
在抓取之前,首先要分析抓取的邏輯。打開今日頭條的首頁http://www.toutiao.com/,如圖6-15所示。

圖6-15 首頁内容
右上角有一個搜尋入口,這裡嘗試抓取街拍美圖,是以輸入“街拍”二字搜尋一下,結果如圖6-16所示。
圖6-16 搜尋結果
這時打開開發者工具,檢視所有的網絡請求。首先,打開第一個網絡請求,這個請求的URL就是目前的連結http://www.toutiao.com/search/?keyword=街拍,打開Preview頁籤檢視Response Body。如果頁面中的内容是根據第一個請求得到的結果渲染出來的,那麼第一個請求的源代碼中必然會包含頁面結果中的文字。為了驗證,我們可以嘗試搜尋一下搜尋結果的标題,比如“路人”二字,如圖6-17所示。
圖6-17 搜尋結果
我們發現,網頁源代碼中并沒有包含這兩個字,搜尋比對結果數目為0。是以,可以初步判斷這些内容是由Ajax加載,然後用JavaScript渲染出來的。接下來,我們可以切換到XHR過濾頁籤,檢視一下有沒有Ajax請求。
不出所料,此處出現了一個比較正常的Ajax請求,看看它的結果是否包含了頁面中的相關資料。
點選
data
字段展開,發現這裡有許多條資料。點選第一條展開,可以發現有一個
title
字段,它的值正好就是頁面中第一條資料的标題。再檢查一下其他資料,也正好是一一對應的,如圖6-18所示。
圖6-18 對比結果
這就确定了這些資料确實是由Ajax加載的。
我們的目的是要抓取其中的美圖,這裡一組圖就對應前面
data
字段中的一條資料。每條資料還有一個
image_detail
字段,它是清單形式,這其中就包含了組圖的所有圖檔清單,如圖6-19所示。
圖6-19 圖檔清單資訊
是以,我們隻需要将清單中的
url
字段提取出來并下載下傳下來就好了。每一組圖都建立一個檔案夾,檔案夾的名稱就為組圖的标題。
接下來,就可以直接用Python來模拟這個Ajax請求,然後提取出相關美圖連結并下載下傳。但是在這之前,我們還需要分析一下URL的規律。
切換回Headers頁籤,觀察一下它的請求URL和Headers資訊,如圖6-20所示。
圖6-20 請求資訊
可以看到,這是一個GET請求,請求URL的參數有
offset
、
format
、
keyword
、
autoload
、
count
和
cur_tab
。我們需要找出這些參數的規律,因為這樣才可以友善地用程式構造出來。
接下來,可以滑動頁面,多加載一些新結果。在加載的同時可以發現,Network中又出現了許多Ajax請求,如圖6-21所示。
圖6-21 Ajax請求
這裡觀察一下後續連結的參數,發現變化的參數隻有
offset
,其他參數都沒有變化,而且第二次請求的
offset
值為20,第三次為40,第四次為60,是以可以發現規律,這個
offset
值就是偏移量,進而可以推斷出
count
參數就是一次性擷取的資料條數。是以,我們可以用
offset
參數來控制資料分頁。這樣一來,我們就可以通過接口批量擷取資料了,然後将資料解析,将圖檔下載下傳下來即可。
3. 實戰演練
我們剛才已經分析了一下Ajax請求的邏輯,下面就用程式來實作美圖下載下傳吧。
首先,實作方法
get_page()
來加載單個Ajax請求的結果。其中唯一變化的參數就是
offset
,是以我們将它當作參數傳遞,實作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import requests from urllib.parse import urlencode def get_page(offset): params = { 'offset': offset, 'format': 'json', 'keyword': '街拍', 'autoload': 'true', 'count': '20', 'cur_tab': '1', } url = 'http://www.toutiao.com/search_content/?' + urlencode(params) try: response = requests.get(url) if response.status_code == 200: return response.json() except requests.ConnectionError: return None |
這裡我們用
urlencode()
方法構造請求的GET參數,然後用requests請求這個連結,如果傳回狀态碼為200,則調用
response
的
json()
方法将結果轉為JSON格式,然後傳回。
接下來,再實作一個解析方法:提取每條資料的
image_detail
字段中的每一張圖檔連結,将圖檔連結和圖檔所屬的标題一并傳回,此時可以構造一個生成器。實作代碼如下:
1 2 3 4 5 6 7 8 9 10 | def get_images(json): if json.get('data'): for item in json.get('data'): title = item.get('title') images = item.get('image_detail') for image in images: yield { 'image': image.get('url'), 'title': title } |
接下來,實作一個儲存圖檔的方法
save_image()
,其中
item
就是前面
get_images()
方法傳回的一個字典。在該方法中,首先根據
item
的
title
來建立檔案夾,然後請求這個圖檔連結,擷取圖檔的二進制資料,以二進制的形式寫入檔案。圖檔的名稱可以使用其内容的MD5值,這樣可以去除重複。相關代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import os from hashlib import md5 def save_image(item): if not os.path.exists(item.get('title')): os.mkdir(item.get('title')) try: response = requests.get(item.get('image')) 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 Downloaded', file_path) except requests.ConnectionError: print('Failed to Save Image') |
最後,隻需要構造一個
offset
數組,周遊
offset
,提取圖檔連結,并将其下載下傳即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 = 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() |
這裡定義了分頁的起始頁數和終止頁數,分别為
GROUP_START
和
GROUP_END
,還利用了多線程的線程池,調用其
map()
方法實作多線程下載下傳。
這樣整個程式就完成了,運作之後可以發現街拍美圖都分檔案夾儲存下來了,如圖6-22所示。
圖6-22 儲存結果
最後,我們給出本節的代碼位址:https://github.com/Python3WebSpider/Jiepai。
通過本節,我們了解了Ajax分析的流程、Ajax分頁的模拟以及圖檔的下載下傳過程。
本節的内容需要熟練掌握,在後面的實戰中我們還會用到很多次這樣的分析和抓取。