Python3網絡爬蟲實戰---3五、 Ajax數據爬取

上一篇文章: Python3網絡爬蟲實戰---3四、數據存儲:非關係型數據庫存儲:Redis
下一篇文章:

有時候咱們在用 Requests 抓取頁面的時候,獲得的結果可能和在瀏覽器中看到的是不同的,在瀏覽器中能夠看到正常顯示的頁面數據,可是使用 Requests 獲得的結果並無,這其中的緣由是 Requests 獲取的都是原始的 HTML 文檔,而瀏覽器中的頁面則是頁面又通過 JavaScript 處理數據後生成的結果,這些數據的來源有多種,多是經過 Ajax 加載的,多是包含在了 HTML 文檔中的,也多是通過 JavaScript 通過特定算法計算後生成的。git

對於第一種狀況,數據的加載是一種異步加載方式,原始的頁面最初不會包含某些數據,原始頁面加載完後會會再向服務器請求某個接口獲取數據,而後數據再被處理才呈現到網頁上,這其實就是發送了一個 Ajax 請求。github

照 Web 發展的趨勢來看,這種形式的頁面愈來愈多,網頁原始 HTML 文檔不會包含任何數據,數據都是經過 Ajax 來統一加載而後再呈現出來,這樣在 Web 開發上能夠作到先後端分離,並且下降服務器直接渲染頁面帶來的壓力。ajax

因此若是咱們遇到這樣的頁面,若是咱們再利用 Requests 等庫來抓取原始頁面是沒法獲取到有效數據的,這時咱們須要作的就是分析網頁的後臺向接口發送的 Ajax 請求,若是咱們能夠用 Requests 來模擬 Ajax 請求,那就能夠成功抓取了。算法

因此本章咱們的主要目的是瞭解什麼是 Ajax 以及如何去分析和抓取 Ajax 請求。數據庫

一、什麼是Ajax

Ajax,全稱爲 Asynchronous JavaScript and XML,即異步的 JavaScript 和 XML。
Ajax 不是一門編程語言,而是利用 JavaScript 在保證頁面不被刷新、頁面連接不改變的狀況下與服務器交換數據並更新部分網頁的技術。
對於傳統的網頁,若是想更新其內容,那麼必需要刷新整個頁面,但有了 Ajax,咱們即可以實如今頁面不被所有刷新的狀況下更新其內容。在這個過程當中,頁面實際是在後臺與服務器進行了數據交互,獲取到數據以後,再利用 JavaScript 改變網頁,這樣網頁內容就會更新了。
能夠到 W3School 上體驗幾個 Demo 來感覺一下:http://www.w3school.com.cn/aj...編程

1. 實例引入

咱們在瀏覽網頁的時候會發現不少網頁都有上滑查看更多的選項,好比拿微博來講,咱們以馬雲的主頁爲例:https://m.weibo.cn/u/2145291155,切換到微博頁面,一直下滑,能夠發現下滑幾個微博以後,再向下就沒有了,轉而會出現一個加載的動畫,不一下子下方就繼續出現了新的微博內容,那麼這個過程其實就是 Ajax 加載的過程,如圖 6-1 所示:json

evernotecid://D603D29C-DFBA-4C04-85E9-CCA3C33763F6/appyinxiangcom/23852268/ENResource/p142
clipboard.pngsegmentfault

圖 6-1 頁面加載過程
咱們注意到頁面其實並無整個刷新,也就意味着頁面的連接是沒有變化的,可是這個過程網頁中卻又多了新的內容,也就是後面刷出來的新的微博。這就是經過 Ajax 獲取新的數據並呈現而實現的過程。後端

2. 基本原理

初步瞭解了 Ajax 以後咱們再來詳細瞭解一下它的基本原理,發送 Ajax 請求到網頁更新的這個過程能夠簡單分爲三步:
發送請求解析內容渲染網頁下面咱們分別來詳細介紹一下這幾個過程。api

發送請求

咱們知道 JavaScript 能夠實現頁面的各類交互功能,那麼 Ajax 也不例外,它也是由 JavaScript 來實現的,實際它就是執行了相似以下的代碼:

var xmlhttp;
if (window.XMLHttpRequest) {
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xmlhttp=new XMLHttpRequest();
} else {// code for IE6, IE5
    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4 && xmlhttp.status==200) {
        document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
}
xmlhttp.open("POST","/ajax/",true);
xmlhttp.send();

這是 JavaScript 對 Ajax 最底層的實現,實際上就是新建了 XMLHttpRequest 對象,而後調用了onreadystatechange 屬性設置了監聽,而後調用 open() 和 send() 方法向某個連接也就是服務器發送了一個請求,咱們在前面用 Python 實現請求發送以後是能夠獲得響應結果的,只不過在這裏請求的發送變成了 JavaScript 來完成,因爲設置了監聽,因此當服務器返回響應時,onreadystatechange 對應的方法便會被觸發,而後在這個方法裏面解析響應內容便可。

解析內容

獲得響應以後,onreadystatechange 屬性對應的方法便會被觸發,此時利用 xmlhttp 的 responseText 屬性即可以取到響應的內容。這也就是相似於 Python 中利用 Requests 向服務器發起了一個請求,而後獲得響應的過程。那麼返回內容多是 HTML,多是 Json,接下來只須要在方法中用 JavaScript 進一步處理便可。好比若是是 Json 的話,能夠進行解析和轉化。

渲染網頁

JavaScript 有改變網頁內容的能力,解析完響應內容以後,就能夠調用 JavaScript 來針對解析完的內容對網頁進行下一步的處理了。好比經過document.getElementById().innerHTML 這樣的操做即可以對某個元素內的源代碼進行更改,這樣網頁顯示的內容就改變了,這樣的操做也被稱做 DOM 操做,即對 Document網頁文檔進行操做,如更改、刪除等。
如上例中,document.getElementById("myDiv").innerHTML=xmlhttp.responseText 便將 ID 爲 myDiv 的節點內部的 HTML 代碼更改成服務器返回的內容,這樣 myDiv 元素內部便會呈現出服務器返回的新數據,網頁的部份內容看上去就更新了。
以上就是Ajax的三個步驟。
咱們觀察到,以上的步驟其實都是由 JavaScript 來完成的,它完成了整個請求、解析、渲染的過程。
因此再回想微博的下拉刷新,這其實就是 JavaScript 向服務器發送了一個 Ajax 請求,而後獲取新的微博數據,將其解析,並將其渲染在網頁中。

3. 結語

所以咱們能夠知道,真實的數據其實都是一次次 Ajax 請求獲得的,咱們若是想要抓取這些數據,就須要知道這些請求究竟是怎麼發送的,發往哪裏,發了哪些參數。若是咱們知道了這些,不就能夠用 Python 來模擬這個發送操做,獲取到這其中的結果了嗎?
因此在下一節咱們就來了解下到哪能夠看到這些後臺 Ajax 操做,去了解它究竟是怎麼發送的,發送了什麼參數。

2 Ajax分析方法

仍是以上文中的微博爲例,咱們已經知道了拖動刷新的內容是由 Ajax 加載的,並且頁面的 URL 沒有變化,那麼咱們應該到哪去查看這些 Ajax 請求呢?

1. 查看請求

在這裏咱們仍是須要藉助於瀏覽器的開發者工具,咱們以 Chrome 瀏覽器爲例來看一下怎樣操做。
首先用 Chrome 瀏覽器打開微博的連接:https://m.weibo.cn/u/2145291155,隨後在頁面中點擊鼠標右鍵,會出現一個檢查的選項,點擊它便會彈出開發者工具,如圖 6-2 所示:

clipboard.png

圖 6-2 開發者工具
那麼在 Elements 選項卡便會觀察到網頁的源代碼,右側即是節點的樣式。
不過這不是咱們想要尋找的內容,咱們切換到 Network 選項卡,隨後從新刷新頁面,能夠發如今這裏出現了很是多的條目,如圖 6-3 所示:

clipboard.png

圖 6-3 Network 面板結果
前文咱們也提到過,這裏其實就是在頁面加載過程當中瀏覽器與服務器之間發送 Request 和接收 Response 的全部記錄。
Ajax其實有其特殊的請求類型,它叫作 xhr,在上圖中咱們能夠發現一個名稱爲 getIndex 開頭的請求,其 Type 爲 xhr,這就是一個 Ajax 請求,咱們鼠標點擊這個請求,能夠查看這個請求的詳細信息,如圖 6-4 所示:

clipboard.png

圖 6-4 詳細信息
咱們在右側能夠觀察到其 Request Headers、URL 和 Response Headers 等信息,如圖 6-5 所示:

clipboard.png

圖 6-5 詳細信息
其中 Request Headers 中有一個信息爲 X-Requested-With:XMLHttpRequest,這就標記了此請求是 Ajax 請求。
隨後咱們點擊一下 Preview,便可看到響應的內容,響應內容是 Json 格式,在這裏 Chrome 爲咱們自動作了解析,咱們能夠點擊箭頭來展開和收起相應內容,如圖 6-6 所示:

clipboard.png

圖 6-6 Json 結果
觀察能夠發現,這裏的返回結果是馬雲的我的信息,如暱稱、簡介、頭像等等,這也就是用來渲染我的主頁所使用的數據,JavaScript 接收到這些數據以後,再執行相應的渲染方法,整個頁面就被渲染出來了。
另外也能夠切換到 Response 選項卡,能夠觀察到真實的返回數據,如圖 6-7 所示:

clipboard.png

圖 6-7 Response 內容
接下來咱們切回到第一個請求,觀察一下它的 Response 是什麼,如圖 6-8 所示:

clipboard.png

圖 6-8 Response 內容
這是最原始的連接 https://m.weibo.cn/u/2145291155 返回的結果,其代碼只有五十行,結構也很是簡單,只是執行了一些 JavaScript。
因此說,咱們所看到的微博頁面的真實數據並非最原始的頁面返回的,而是後來執行 JavaScript 後再次向後臺發送了 Ajax 請求,拿到數據後再進一步渲染出來的。

2. 過濾請求

接下來咱們再利用 Chrome 開發者工具的篩選功能篩選出全部的 Ajax 請求,在請求的上方有一層篩選欄,咱們能夠點擊 XHR,這樣在下方顯示的全部請求便都是 Ajax 請求了,如圖 6-9 所示:

clipboard.png

圖 6-9 Ajax 請求
再接下來咱們咱們不斷滑動頁面,能夠看到在頁面底部有一條條新的微博被刷出,而開發者工具下方也一個個地出現 Ajax 請求,這樣咱們就能夠捕獲到全部的 Ajax 請求了。
隨意點開一個條目均可以清楚地看到其 Request URL、Request Headers、Response Headers、Response Body等內容,想要模擬請求和提取就很是簡單了。
如圖所示內容即是馬雲某一頁微博的列表信息,如圖 6-10 所示:

clipboard.png

圖 6-10 微博列表信息

3. 結語

到如今爲止咱們已經能夠分析出來 Ajax 請求的一些詳細信息了,接下來咱們只須要用程序來模擬這些 Ajax 請求就能夠輕鬆提取咱們所須要的信息了。
因此在下一節咱們來用 Python 實現 Ajax 請求的模擬,從而實現數據的抓取。

3 Ajax結果提取

仍然是拿微博爲例,咱們接下來用 Python 來模擬這些 Ajax 請求,把馬雲發過的微博爬取下來。

1. 分析請求

咱們打開 Ajax 的 XHR 過濾器,而後一直滑動頁面加載新的微博內容,能夠看到會不斷有Ajax請求發出。
咱們選定其中一個請求來分析一下它的參數信息,點擊該請求進入詳情頁面,如圖 6-11 所示:

clipboard.png

圖 6-11 詳情頁面
能夠發現這是一個 GET 類型的請求,請求連接爲:https://m.weibo.cn/api/contai...;value=2145291155&containerid=1076032145291155&page=2,請求的參數有四個:type、value、containerid、page。
隨後咱們再看一下其餘的請求,觀察一下這些請求,發現它們的 type、value、containerid 始終如一。type 始終爲 uid,value 的值就是頁面的連接中的數字,其實這就是用戶的 id,另外還有一個 containerid,通過觀察發現它就是 107603 而後加上用戶 id。因此改變的值就是 page,很明顯這個參數就是用來控制分頁的,page=1 表明第一頁,page=2 表明第二頁,以此類推。
以上的推斷過程能夠實際觀察參數的規律便可得出。

2. 分析響應

隨後咱們觀察一下這個請求的響應內容,如圖 6-12 所示:

clipboard.png

圖 6-12 響應內容
它是一個 Json 格式,瀏覽器開發者工具自動爲作了解析方便咱們查看,能夠看到最關鍵的兩部分信息就是 cardlistInfo 和 cards,將兩者展開,cardlistInfo 裏面包含了一個比較重要的信息就是 total,通過觀察後發現其實它是微博的總數量,咱們能夠根據這個數字來估算出分頁的數目。
cards 則是一個列表,它包含了 10 個元素,咱們展開其中一個來看一下,如圖 6-13 所示:

clipboard.png

圖 6-13 列表內容
發現它又有一個比較重要的字段,叫作 mblog,繼續把它展開,發現它包含的正是微博的一些信息。好比 attitudes_count 贊數目、comments_count 評論數目、reposts_count 轉發數目、created_at 發佈時間、text 微博正文等等,得來全不費功夫,並且都是一些格式化的內容,因此咱們提取信息也更加方便了。
這樣咱們能夠請求一個接口就獲得 10 條微博,並且請求的時候只須要改變 page 參數便可,目前總共 138 條微博那麼只須要請求 14 次便可,也就是 page 最大能夠設置爲14。
這樣咱們只須要簡單作一個循環就能夠獲取到全部的微博了。

3. 實戰演練

在這裏咱們就開始用程序來模擬這些 Ajax 請求,將馬雲的全部微博所有爬取下來。
首先咱們定義一個方法,來獲取每次請求的結果,在請求時page 是一個可變參數,因此咱們將它做爲方法的參數傳遞進來,代碼以下:

from urllib.parse import urlencode
import requests
base_url = 'https://m.weibo.cn/api/container/getIndex?'

headers = {
    'Host': 'm.weibo.cn',
    'Referer': 'https://m.weibo.cn/u/2145291155',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest',
}

def get_page(page):
    params = {
        'type': 'uid',
        'value': '2145291155',
        'containerid': '1076032145291155',
        '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)

首先在這裏咱們定義了一個 base_url 來表示請求的 URL 的前半部分,接下來構造了一個參數字典,其中 type、value、containerid 是固定的參數,只有 page 是可變參數,接下來咱們調用了 urlencode() 方法將參數轉化爲 URL 的 GET請求參數,即相似於type=uid&value=2145291155&containerid=1076032145291155&page=2 這樣的形式,隨後 base_url 與參數拼合造成一個新的 URL,而後咱們用 Requests 請求這個連接,加入 headers 參數,而後判斷響應的狀態碼,若是是200,則直接調用 json() 方法將內容解析爲 Json 返回,不然不返回任何信息,若是出現異常則捕獲並輸出其異常信息。
隨後咱們須要定義一個解析方法,用來從結果中提取咱們想要的信息,好比咱們此次想保存微博的 id、正文、贊數、評論數、轉發數這幾個內容,那能夠先將 cards 遍歷,而後獲取 mblog 中的各個信息,賦值爲一個新的字典返回便可。

from pyquery import PyQuery as pq

def parse_page(json):
    if json:
        items = json.get('cards')
        for item in items:
            item = item.get('mblog')
            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 標籤去除掉。
最後咱們遍歷一下 page,一共 14 頁,將提取到的結果打印輸出便可。

if __name__ == '__main__':
    for page in range(1, 15):
        json = get_page(page)
        results = parse_page(json)
        for result in results:
            print(result)

另外咱們還能夠加一個方法將結果保存到 MongoDB 數據庫。

from pymongo import MongoClient

client = MongoClient()
db = client['weibo']
collection = db['weibo']

def save_to_mongo(result):
    if collection.insert(result):
        print('Saved to Mongo')

最後整理一下,最後的程序以下:

import requests
from urllib.parse import urlencode
from pyquery import PyQuery as pq
from pymongo import MongoClient

base_url = 'https://m.weibo.cn/api/container/getIndex?'
headers = {
    'Host': 'm.weibo.cn',
    'Referer': 'https://m.weibo.cn/u/2145291155',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest',
}
client = MongoClient()
db = client['weibo']
collection = db['weibo']
max_page = 14


def get_page(page):
    params = {
        'type': 'uid',
        'value': '2145291155',
        'containerid': '1076032145291155',
        '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)


def parse_page(json):
    if json:
        items = json.get('cards')
        for item in items:
            item = item.get('mblog')
            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


def save_to_mongo(result):
    if collection.insert(result):
        print('Saved to Mongo')


if __name__ == '__main__':
    for page in range(1, max_page + 1):
        json = get_page(page)
        results = parse_page(json)
        for result in results:
            print(result)
            save_to_mongo(result)

運行程序後樣例輸出結果以下:

{'id': '3938863363932540', 'text': '咱們也許不能解決全部的問題,但咱們能夠盡本身的力量去解決一些問題。移動互聯網不能只是讓留守孩子多了一個隔空說話的手機,移動互聯網是要讓父母和孩子一直在一塊兒。過年了,回家吧…… 農村淘寶2016團圓賀歲片《福與李》 ', 'attitudes': 21785, 'comments': 40232, 'reposts': 2561}
Saved to Mongo
{'id': '3932968885900763', 'text': '跟來自陝甘寧雲貴川六省的100位優秀鄉村教師共度了難忘的兩天,接下來我又得出遠門了。。。爲了4000萬就讀於鄉村學校的孩子,因此有了這麼一羣堅毅可愛的老師,有了這麼多關注鄉村教育的各界人士,這兩天感動、欣喜、振奮!咱們在各自的領域裏,一直堅持和努力吧!', 'attitudes': 32057, 'comments': 7916, 'reposts': 2332}
Saved to Mongo

查看一下 MongoDB,相應的數據也被保存到 MongoDB,如圖 6-14 所示:

clipboard.png

圖 6-14 保存結果

4. 本節代碼

本節代碼地址:https://github.com/oldmarkfac...

5. 結語

本節實例的目的是爲了演示 Ajax 的模擬請求過程,爬取的結果不是重點,該程序仍有不少能夠完善的地方,如頁碼的動態計算、微博查看全文等,如感興趣能夠嘗試一下。經過這個實例咱們主要是爲了學會怎樣去分析 Ajax 請求,怎樣用程序來模擬抓取 Ajax 請求,瞭解了相關抓取原理以後,下一節的 Ajax 實戰演練會更加駕輕就熟。

相關文章
相關標籤/搜索