爬蟲練習四:爬取b站番劇字幕
由於個人經常在空閒時間在b站看些小視訊歡樂一下,這次就想到了爬取b站視訊的彈幕。
這裡就以番劇《我的妹妹不可能那麼可愛》第一季為例,抓取這一番劇每一話對應的彈幕。
1. 分析頁面
這部番劇的第一季就有15話,所以我們首先需要找到每一話對應的url,然後再去爬取每一話的彈幕。
1.1 找到每一話對應的url
開啟番劇的首頁,可以看到每一話的資訊就展示在圖中位置。
照慣例,我們首先對當前請求網頁返回的資料進行檢視,發現請求該url返回的只有一點簡略的番劇資訊,根本沒有每一話的資訊。
但是我們在瀏覽器中又確實能夠看到每一話的資訊,所以推測,這些資訊應該是通過AJAX非同步載入
當前這一網頁中XHR標籤內的網路請求並不多,最簡單的方法就是每一個網路請求都檢視一番。但是我們可以發現,這裡的每個網路請求看起來都有一定的命名規則,像info/nav/review/recommend這些,似乎都很容易理解。我們發現其中有一個請求命名為'section?season_id=...',那就先看它了。
(對於非同步載入的網頁,相比於隨機命名,規範命名有助於程式設計師進行高效開發和維護。所以我們從網路請求的命名入手,一定程度上能夠提高找到對應資料來源的速度和準確度)
點選Preview一看,這個網路請求返回的是一個json檔案,而內容恰好能夠對應到這個番劇每一話的資訊,開啟這個請求的網頁也確實發現了每一話的名稱、url等資訊。
那麼我們就已經找到了這部番劇每一話的播放地址url。
1.2 找到當前視訊對應的彈幕來源
以第一話為例,我們開啟第一話的url。毫無懸念的是,請求當前url的返回資訊裡並沒有彈幕資訊。
所以我們繼續檢視"XHR"標籤。這時候,問題來了,"XHR"標籤內少說也有好幾十個網路請求,它們的命名好像也不是很清晰,那我們豈不是要把每個網路請求都檢視一遍才能找到彈幕對應的來源。
這裡要介紹一個小技巧:不管當前頁面有多少個網路請求,不管這些數量繁多的網路請求是為了反爬蟲還是為了頁面複雜功能的實現,我們只需要記住一個宗旨:程式設計師是不會捨得把資源過多浪費在無關緊要的地方的。
所以這裡我們將各個請求按Size進行倒敘排列,從上往下嘗試幾次就可以發現,彈幕是來源於這個url的:https://api.bilibili.com/x/v1/dm/list.so?oid=17737533
而通過檢視這個網路請求的headers資訊可以發現,彈幕是通過請求介面獲取的,請求引數是17737533,它是這個視訊的編號。
但是,當前視訊的url裡面明明是ep65128,這兩個引數是很明顯對不上的。
既然載入彈幕的請求是訪問主url後才會繼續的,那麼通過訪問主url就肯定能拿到對應的視訊編號,然後載入彈幕的請求才可以通過這個編號進行載入彈幕。
那我們接下來就分析當前頁面的原始碼,搜尋17737533,它屬於cid欄位。現在我們就找到了每個視訊請求彈幕時它所對應的編號。
(回頭檢視時發現,cid這個引數在1.1中通過'section?season_id=...'也可以獲取到)
1.3 抓取流程
1)通過番劇首頁獲取每一話對應的url和cid編號
'根據1.2,其實cid引數可以從兩個地方獲取到。這裡我們就用簡單的方法,直接在'section?season_id=...'中獲取。
2)將cid編號放入“https://api.bilibili.com/x/v1/dm/list.so?oid=”後,構建url,儲存每一話的彈幕資訊。
2. 程式碼實現
import requests from bs4 import BeautifulSoup header = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' } # 通過番劇首頁獲取每一話對應的cid編號,存入section_sid def get_cid(index_url): try: index_response = requests.get(index_url, headers=header) index_json = index_response.json() # 返回一個json檔案 section_cid = [] for each in index_json['result']['main_section']['episodes']: section_cid.append({ 'title': each['long_title'], 'cid': each['cid'], 'id': each['id'], 'url': each['share_url'] }) except: print('index pass') return section_cid # 通過cid編號獲取這一話的所有彈幕資訊 def get_danmu(cid): try: danmu_response = requests.get('https://api.bilibili.com/x/v1/dm/list.so?oid='+str(cid), headers=header) danmu_soup = BeautifulSoup(danmu_response.content, 'lxml') for each in danmu_soup.findAll('d'): danmu_info = each['p'].split(",") danmu_detail.append({ 'cid': cid, # 彈幕對應視訊cid 'danmu': each.get_text(), # 彈幕內容 'time': danmu_info[0], # 彈幕出現時間(秒) 'type': danmu_info[1], # 彈幕模式 'size': danmu_info[2], # 字號 'color': danmu_info[3], # 顏色 'timestamp': danmu_info[4], # 時間戳 'pool': danmu_info[5], # 彈幕池 'sender': danmu_info[6], # 傳送者ID 'row': danmu_info[7] # 彈幕rowID,用於“歷史彈幕”功能 }) print(cid, 'done') except: print(cid, 'pass') danmu_detail = [] # 所有彈幕存入danmu_detail section_cid = get_cid(index_url) [get_danmu(each_section['cid']) for each_section in section_cid]