爬蟲:越滑越多的動態網頁列表流資料(透過 Ajax 獲取微博個性化推薦內容)

去碼頭整點薯條發表於2022-11-24

在瀏覽社交媒體時,我們所看的內容彷彿是無窮無盡的。

我們常常滑動到頁面底端,以為沒有內容了,卻發現新的內容又一下子重新整理出來。內容越滑越多,這種資料被稱作列表流資料。

有趣的是,當頁面不斷為我們提供新的內容時,網頁卻還是原來的網頁——URL 並沒有改變。這是怎麼回事?

列表流資料示例:微博熱門內容

1 Ajax

在同一個頁面中,網頁是如何源源不斷的展現新內容的呢?

如果開啟瀏覽器的開發者模式,當我們滑動到頁面底端時,我們可以在 “網路” 選項卡中觀測到一些新生成的 xhr 型別條目。這類條目中包含的就是 Ajax 請求。根據崔慶才老師的介紹:

Ajax,全稱為 Asynchronous JavaScript and XML,即非同步的 JavaScript 和 XML。它不是一門程式語言,而是利用 JavaScript 在保證頁面不被重新整理、頁面連結不改變的情況下與伺服器交換資料並更新部分網頁的技術。

對於傳統的網頁,如果想更新其內容,那麼必須要重新整理整個頁面,但有了 Ajax,便可以在頁面不被全部重新整理的情況下更新其內容。在這個過程中,頁面實際上是在後臺與伺服器進行了資料互動,獲取到資料之後,再利用 JavaScript 改變網頁,這樣網頁內容就會更新了。

開發者模式下的 xhr 條目

簡單來說,當我們向下滑動到頁面底端,JavaScript 會向網頁後臺的伺服器傳送一個請求,告訴伺服器我們想要更多的內容。伺服器返回相應的響應內容,JavaScript 又對響應內容(不論是 HTML 格式,還是 JSON 格式)進行了解析,並渲染成為我們看到的新內容。

2 列表流資料的爬取:以微博為例

瞭解了列表流資料是如何源源不斷產生的後,我們就有了獲取這種資料的思路:模擬 JavaScript 向網頁伺服器傳送 Ajax 請求,並解析獲取到的響應資料。

下面來演示爬取過程——以爬取微博個性化推薦內容為例。

2.1 觀察 Ajax 請求

我們找到上文提到的 xhr 檔案,點選它,我們可以看到檔案包含的標頭資訊。其中相關的資訊包括:

  • 請求 URL:如果你觀察多條 xhr 條目的話,你會發現它們的 URL 幾乎完全一樣,唯一不同之處在於 max_id 的值,從 1 開始,每從底部重新整理一次,新的 xhr 條目的 max_id 的值就會增加 1。
  • 請求方法
  • 狀態程式碼
  • 請求標頭中的 cookie
  • 請求標頭中的 user-agent
  • 請求標頭中的 x-requested-with,它的值為 XMLHttpRequest,這標記了該條請求是 Ajax 請求

xhr 標頭

除了標頭以外,我們還可以觀察到該條 Ajax 請求的預覽,這是一個 JSON 檔案。

xhr 預覽

可以看到,該條請求中包含 10 條內容,如果深入觀察一條內容內部,我們可以找到這條內容對應的微博資訊,包括微博 id、釋出使用者、微博文字內容、微博圖片內容、釋出該條微博的 IP 地址資訊等。

xhr 預覽-內部

2.2 爬取 Ajax 請求獲取的 JSON 資料

現在我們可以嘗試爬取上述資料了。首先匯入我們所需的包,並定義一些後續用得到的變數:

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

base_url = 'https://weibo.com/ajax/feed/hottimeline?'  # 本專案要爬取的 url 都是以此為開頭的
headers = {
    'User-Agent': '你的 User-Agent',  # 點選你要爬取的 xhr 檔案,在標頭中可以找到相關資訊。
    'X-Requested-With': 'XMLHttpRequest',
    'Cookie': '你的 Cookie'
}

我們來構建一個函式,用於返回我們要傳送模擬請求的 URL:

def get_hottimeline_url(max_id):
    '''
    返回一個 xhr 型別 "hottimeline (熱榜時間流)" 的 url。
    :param max_id: 每個 hottimeline url 中特殊的 max_id (1, 2, 3...)
    '''
    params = {
        'refresh': '2',
        'group_id': '你 URL 中的 group_id',
        'containerid': '你 URL 中的 group_id containerid',
        'extparam': 'discover%7Cnew_feed',
        'max_id': max_id,  # URL 中只有該值是變化的
        'count': '10'
    }
    url = base_url + urlencode(params, safe='%')  # 此處指明 safe 引數,使 "%" 不被轉義為 "%25",不然會拼接成錯誤的 URL
    return url

接下來,我們像獲取靜態頁面的資料一樣,透過 request 傳送請求,獲取 JSON 資料:

def get_response_json(url):  
    '''
    返回一個 url 的 json 檔案。
    '''
    response = requests.get(url, headers=headers)
    json = response.json()
    return json

2.3 解析 JSON 資料

獲取到 JSON 資料後,我們需要對資料進行解析,獲取我們所需的資訊。一個 URL 能返回 10 條微博內容,我們希望能迴圈得到每一條微博內容的資料,存為一個字典。再將這個字典,存入微博內容組成的列表中。

在獲取一條微博的文字內容時,我們需要注意,當這條微博的文字內容過長時,文字段會被摺疊。如果我們想看到完整的內容,需要在瀏覽器介面點選“展開”按鈕,這使得我們無法在現有的 JSON 資料中獲得完整的文字資料。但是,當我們點選展開時,可以看到開發者模式的網路選項卡中,又多出了名為 “longtext?id=xxxxxxxxxx” 的xhr條目,我們可以透過該條目的 URL 獲取到完整的長文字資料。

def parse_hottimeline(list_recommendation, json):
    '''
    解析一條 hottimeline 的 json 檔案,並將包含的 10 條熱榜推薦內容追加到內容列表中。
    '''
    for item in json.get('statuses'):
        weibo = {}  # 建立一個臨時的用於儲存一條微博資訊的字典

        # user information
        user_info = item.get('user')
        weibo['user_id'] = user_info.get('id')  # 釋出者 id
        weibo['user_name'] = user_info.get('screen_name')  # 釋出者暱稱

        # weibo information
        weibo['id'] = item.get('id')  # 微博 id
        weibo['isLongText'] = item.get('isLongText')  # 該變數為 True 時,這個微博的文字為長文字(文字段會被摺疊)
        weibo['mblogid'] = item.get('mblogid')  # 可以透過該變數,索引到存有長文字的 JSON 檔案的 URL
        if weibo['isLongText'] is True:
            url = "https://weibo.com/ajax/statuses/longtext?id=" + weibo['mblogid']
            json_longtext = get_response_json(url)
            weibo['text'] = json_longtext.get('data').get('longTextContent')
        else:
            weibo['text'] = pq(item.get('text')).text()
        weibo['pic_num'] = item.get('pic_num')  # 該條微博包含的圖片數
        weibo['pic'] = []  # 用於儲存該條微博圖片的 url
        if weibo['pic_num'] > 0:
            pic_dict = item.get('pic_infos')
            for pic in pic_dict:
                pic_url = pic_dict[pic]['original']['url']  
                weibo['pic'].append(pic_url)
        else:
            pass
        weibo['attitudes'] = item.get('attitudes_count')  # 點贊數
        weibo['comments'] = item.get('comments_count')  # 評論數
        weibo['reposts'] = item.get('reposts_count')  # 轉發數
        region = item.get('region_name')  # 釋出時的 IP 地址
        if region is None:
            weibo['region'] = region
        else: weibo['region'] = region.strip('釋出於 ')
        print(weibo)
        list_recommendation.append(weibo)  # 將解析出的一條微博資料,加入一個列表中

2.4 儲存資料

我們已經實現了頁面底部重新整理資料的 URL 獲取、模擬請求、解析資料的功能。最後,我們將新建一個列表,將每條微博資訊儲存進去。主函式的程式碼如下:

if __name__ == '__main__':
    list_recommendation = []
    for max_id in range(1, 11):  # 模擬爬取 10 次重新整理結果,最終能獲取到 100 條個性化推薦熱門微博資料
        hottimeline_url = get_hottimeline_url(max_id)
        print('hottimeline_url = ', hottimeline_url)
        response_json = get_response_json(hottimeline_url)
        parse_hottimeline(list_recommendation, response_json)

參考

注:轉載請註明出處。

相關文章