乾貨:手把手教你——某寶直播彈幕爬蟲
前言
公司有通過淘寶直播間短連結來爬取直播彈幕的需求, 奈何即便google上面也僅找到一個相關的話題, 還沒有答案. 所以只能自食其力了.
我們先看一下爬蟲的最終效果:
下面我們來抽絲剝繭, 重現一下調研過程.
不知道該如何做的朋友,我還是推薦下java學習群:587372254,首先你要有基礎,其次不管你是小牛還是大牛,都挺歡迎,群裡每天都會分享java相關乾貨。
頁面分析
直播間地址在分享直播時可以拿到:
彈幕一般不是websocket就是socket. 我們開啟dev tools過濾ws的請求即可看到websocket地址:
提一下鬥魚: 它走的是flash的socket, 我們就算開啟dev tools也是懵逼, 好在鬥魚官方直接開放了socket的API.
我們繼續檢視收到的訊息, 發現訊息的壓縮型別compressType有兩種: COMMON和GZIP. data的值肯定就是目標訊息了, 看起來像經過了base64編碼, 解密過程後面再說.
現在我們首先要解決的問題是如何拿到websocket地址. 分析一下html source, 發現可以通過其中不變的部分查詢到指令碼:
然鵝, 拿到這塊整個的指令碼格式化之後發現, 原始程式碼明顯是模組化開發的, 經過了打包壓縮. 所以我們只能分析模組內一小塊程式碼, 這是沒有意義的.
但是我們可以觀察到不同的直播間websocket地址唯一不同的只有token, 所以我們可以想辦法拿到token. 當然這是很噁心的環節, 完全沒有頭緒, 想到的各種可能性都失敗了. 後面像無頭蒼蠅一樣看頁面發起的請求, 竟然給找到了...
token是通過api請求獲取的, api地址是:
http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/
好了那websocket地址的問題解決了, 我們開始寫爬蟲吧.
編寫爬蟲
看看api的query string那一堆動態引數, 普通爬蟲就別想了, 我們祭出神器: puppeteer.
puppeteer是谷歌推出的開放Node API的無頭瀏覽器, 理論上可以可程式設計化地控制瀏覽器的各種行為, 對於我們的場景來說就是:
直播頁面載入完之後, 攔截獲取websocket token的api請求, 解析結果拿到token. 這部分的程式碼如下:
const browser
= await puppeteer.launch() const page
= (await browser.pages())[0] await page.setRequestInterception(true) const api
= const { url }
= message // intercept request obtaining the web
socket token page.on('request',
req => { if (req.url.includes(api))
{ console.log(`[${url}]
getting token`) } req.continue() }) page.on('response', async res
=> { if (!res.url.includes(api)) return const data
= await res.text() const token
= data.match(/"result":"(.*?)"/)[1] const url = `ws://acs.m.taobao.com/accs/auth?token=${token}` }) //
open the taobao live page await page.goto(url,
{ timeout: 0 }) console.log(`[${url}]
page loaded`)
這裡有個性能優化的小技巧. puppeteer官方示例中獲取page例項會開啟一個新頁面:
const page = await browser.newPage()
, 實際上瀏覽器啟動本來就預設有個about:blank頁面開啟, 我們的程式碼中直接是獲取這個開啟的例項來跳轉直播頁面, 這樣就可以少一個程序.可以ps ax|grep puppeteer觀察啟動的程序數來進行對比, 預設有兩個主程序, 剩餘的都是頁面程序.
獲取到websocket地址就可以建立連線拉取訊息了:
const url = `ws://acs.m.taobao.com/accs/auth?token=${token}`
const ws = new WebSocket(url)
ws.on('open', () => { console.log(`\nOPEN:
${url}\n`) }) ws.on('close', () => { console.log('DISCONN')
}) ws.on('message',
msg => { console.log(msg)
})
訊息解密
現在我們能持續拉取訊息了, 這樣會方便分析. 前面我們分析頁面的時候發現compressType有兩種: COMMON和GZIP. 經過嘗試, COMMON的可以直接得到明文, 而GZIP的需要再經過一次gunzip解碼. 解碼結果大致如下, 裡面已經可以看到暱稱和彈幕內容了:
然鵝, 一切才剛剛開始...內容裡面是有亂碼的, 基於這樣的內容做正則匹配無果. 如果嘗試直接儲存buffer
或者buffer.toString()
到檔案會發現檔案根本打不開,
內容是無法解析的:
沒辦法, 我們只能分析原始buffer array的utf8編碼了. 這裡開了腦洞, 直接將buffer array做join得到的string拿來分析其規律 (分析程式碼見analyze.js檔案):
幾個樣本的分析結果如下, 其中不變的部分做了高亮:
這些值可能是由有效字元編碼按一定規則換算過來, 但誰又能猜得到呢, 也沒必要.
這樣我們就可以通過一個正則表示式解析出nick和barrage了:
/.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/
當然這個pattern同樣能匹配到關注主播的彈幕, 這不是我們想要的. 我們可以通過一串確定的buffer字串提前過濾掉這種訊息:
const followedPattern = '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119'
至此我們已經可以解析出乾乾淨淨的暱稱+彈幕了. 完整解密程式碼如下:
function decode(msg) { //
base64 decode let buffer
= Buffer.from(msg.data, 'base64') if (msg.compressType
=== 'GZIP') { //
gzip decode buffer = zlib.gunzipSync(buffer) } const bufferStr
= buffer.join(',') //
[followed] notifications are ignored const followedPattern
= '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119' if (bufferStr.includes(followedPattern))
{ return } //
// print for debugging // console.log(bufferStr) //
console.log(buffer.toString()) //
first match is nick name and second match is barrage content const barragePattern
= /.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/ const matched
= bufferStr.match(barragePattern) if (matched)
{ const nick = parseStr(matched[1]) const barrage
= parseStr(matched[2]) console.log(`${nick}: ${barrage}`)
}}
當然可能還存在一個問題, 是關於上面分析結果表裡的barrage前
, 有連續的5位固定不變,
實際上剛開始是連同前面一位共6位不變的, 結果過了一天之後前面那位從130變到了131, 而再往前的幾位變化頻率則特別高. 所以我懷疑這些值有可能是跟當前時間有關.
可能不確定的一段時間之後這5位固定值也會變掉吧, 到時正則就得調整了, 但應該可以正常執行很久了. 如有哪些同仁感興趣, 可以找找規律.
程序維護
實際使用時流程大致應該是這樣的: 收到請求之後主程序fork一個爬蟲子程序來獲取websocket url, 子程序返回結果給主程序, 在使用方建立websocket連線(搶過連線)之後, 子程序便可自殺釋放資源, 自殺的同時browser.close()
殺死puppeteer相關程序.
之所以這樣做是因為測試下來: websocket斷開連線不久token會失效.
尾言
好了,我介紹完給你們了,以上的部分就是我想說的內容,如果你也想在IT行業拿高薪,想學習的,想就業前景好的,想跟別人競爭能取得優勢的,想進阿里面試但擔心面試不過的,你都可以來JAVA交流群:587372254 ,群裡網盤也有各種免費資源共享,不懂的知識點可以問群主管理員