淘宝直播弹幕爬虫

背景说明

公司有经过淘宝直播间短连接来爬取直播弹幕的需求, 奈何即使google上面也仅找到一个相关的话题, 尚未答案. 因此只能自食其力了.
爬虫的github仓库地址在文末, 咱们先看一下爬虫的最终效果:
html

下面咱们来抽丝剥茧, 重现一下调研过程.git

页面分析

直播间地址在分享直播时能够拿到:
github

弹幕通常不是websocket就是socket. 咱们打开dev tools过滤ws的请求便可看到websocket地址:
web

提一下斗鱼: 它走的是flash的socket, 咱们就算打开dev tools也是懵逼, 好在斗鱼官方直接开放了socket的API.正则表达式

咱们继续查看收到的消息, 发现消息的压缩类型compressType有两种: COMMON和GZIP. data的值确定就是目标消息了, 看起来像通过了base64编码, 解密过程后面再说.
编程

如今咱们首先要解决的问题是如何拿到websocket地址. 分析一下html source, 发现能够经过其中不变的部分查找到脚本:

然鹅, 拿到这块整个的脚本格式化以后发现, 原始代码明显是模块化开发的, 通过了打包压缩. 因此咱们只能分析模块内一小块代码, 这是没有意义的.api

可是咱们能够观察到不一样的直播间websocket地址惟一不一样的只有token, 因此咱们能够想办法拿到token. 固然这是很恶心的环节, 彻底没有头绪, 想到的各类可能性都失败了. 后面像无头苍蝇同样看页面发起的请求, 居然给找到了...
token是经过api请求获取的, api地址是:浏览器

http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/

好了那websocket地址的问题解决了, 咱们开始写爬虫吧.性能优化

编写爬虫

看看api的query string那一堆动态参数, 普通爬虫就别想了, 咱们祭出神器: puppeteer.websocket

puppeteer是谷歌推出的开放Node API的无头浏览器, 理论上能够可编程化地控制浏览器的各类行为, 对于咱们的场景来讲就是:
直播页面加载完以后, 拦截获取websocket token的api请求, 解析结果拿到token. 这部分的代码以下:

const browser = await puppeteer.launch()
    const page = (await browser.pages())[0]
    await page.setRequestInterception(true)
    const api = 'http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/'
    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会失效.

Github仓库

记得star啊?
https://github.com/xiaozhongliu/taobao-live-crawler