当前位置: 首页 > 后端技术 > Node.js

淘宝直播弹幕爬虫

时间:2023-04-03 12:19:39 Node.js

后台显示公司需要通过淘宝直播间的短链接抓取直播弹幕,但连google也只能找到相关话题,没有任何答案。所以只能养活自己。爬虫的github仓库地址在文末,我们先来看看爬虫的最终效果:先来看看爬虫的最终效果吧:先看看茧并重现研究过程。分享直播时可以获取页面解析直播间的地址:弹幕一般要么是websocket,要么是socket。我们可以打开devtools来过滤ws请求看websocket地址:提到斗鱼:它用的是flashsocket,打开devtools我们也是一头雾水,还好斗鱼官方直接开放了socketAPI。我们继续查看收到的消息,发现消息有两种压缩类型:COMMON和GZIP。数据的值必须是目标消息。看起来是经过base64编码的。解密过程将在后面讨论。现在我们要解决的第一个问题就是如何获取websocket地址。分析一下html源码,发现可以通过未改动的部分找到脚本:但是拿到整个脚本格式化后,发现原来的代码明显是模块化开发的,打包压缩了.所以我们只能分析模块内部的一小段代码,这是没有意义的。但是我们可以观察到,不同直播间的websocket地址唯一的区别就是token,所以我们可以想办法获取token。当然,这是非常恶心的部分。所有的可能性都失败了。无头鸡一样的查看页面发起的请求,发现...token是通过api请求获取的,api地址为:http://h5api.m.taobao。com/h5/mtop.mediaplatform.live.encryption/1.0/好了,websocket地址的问题解决了,我们开始写爬虫吧。写个爬虫看看api的查询字符串的动态参数,普通的爬虫别想了,我们提供神器:puppeteer.puppeteer是Google推出的开放NodeAPI的无头浏览器。理论上,它可以通过编程方式控制浏览器的各种行为。对于我们的场景来说是:在直播页面加载完成后,拦截获取websockettoken的api请求,从解析结果中获取token。这部分代码如下:constbrowser=awa它puppeteer.launch()constpage=(awaitbrowser.pages())[0]awaitpage.setRequestInterception(true)constapi='http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/'const{url}=message//拦截获取websockettoken的请求page.on('request',req=>{if(req.url.includes(api)){console.log(`[${url}]获取令牌`)}req.continue()})page.on('response',asyncres=>{if(!res.url.includes(api))returnconstdata=awaitres.text()consttoken=data.match(/"result":"(.*?)"/)[1]consturl=`ws://acs.m.taobao.com/accs/auth?token=${token}`})//打开淘宝直播页面awaitpage.goto(url,{timeout:0})console.log(`[${url}]pageloaded`)这里有一个性能优化技巧。获取puppeteer官方例子中的page实例会打开一个新的页面:constpage=awaitbrowser.newPage(),其实浏览器启动时默认打开了一个about:blank页面,我们的代码直接获取这个openedinstance跳转到直播页面,这样可以少一个流程。你可以psax|grepPuppeteer观察启动的进程数以进行比较。默认有两个主进程,其余都是页面进程。获取websocket地址后,可以建立连接拉取消息:consturl=`ws://acs.m.taobao.com/accs/auth?token=${token}`constws=newWebSocket(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解码。解码结果大致如下,已经可以看到里面的昵称和弹幕内容了:但是鹅,一切才刚刚开始……内容里面有乱码,根据这样的内容正则匹配是行不通的。如果您尝试直接保存缓冲区或缓冲区。toString()到文件中,你会发现根本打不开文件,也无法解析内容:没办法,我们只能分析原buffer数组的utf8编码。这里脑洞大开,直接joinbuffer数组得到字符串分析其规律(分析代码见analyze.js文件):几个样本的分析结果如下,不变部分高亮显示:这些values可能会根据一定的规则从有效的字符代码转换而来,但谁能猜到呢,但不是必须的。这样我们就可以通过一个正则表达式解析出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/当然这个模式也可以匹配跟随主播的弹幕,这不是我们想要的.我们可以通过一系列确定的缓冲字符串提前过滤掉这类消息:constfollowedPattern='226,129,130??,226,136,176,226,143,135,102,111,108,108,111,119'至此我们已经能够解析出干净的昵称+弹幕了。完整的解密代码如下:functiondecode(msg){//base64解码letbuffer=Buffer.from(msg.data,'base64')if(msg.compressType==='GZIP'){//gzip解码buffer=zlib.gunzipSync(buffer)}constbufferStr=buffer.join(',')//[followed]通知被忽略constfollowedPattern='226,129,130??,226,136,176,226,143,135,102,111,108,108,111,119'if(bufferStr.includes(followedPattern)//调试打印//console.log(bufferStr)//console.log(buffer.toString())//第一个匹配昵称第二个匹配弹幕内容constbarragePattern=/.*,[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/constmatched=bufferStr.match(弹幕模式)如果(匹配){constnick=parseStr(matched[1])constbarrage=parseStr(matched[2])console.log(`${nick}:${barrage}`)}}当然可能还有一个问题,就是关于以上分析结果表中弹幕前,有5位连续数字保持不变。其实一开始的时候,加上前面的数字,一共是6位,没有变化。过了一天,前一位数字从130变成了131,前一位数字从130变成了131,几个数字的变化频率特别高。所以我怀疑这些值可能和当前时间有关。也许这5个固定值也会在不确定的一段时间后发生变化。到时候规律性就要调整了,不过应该是可以正常运行很久的。有同行有兴趣的可以找找规则。进程维护的实际过程应该是这样的:主进程收到请求后,fork一个爬虫子进程获取websocketurl,子进程将结果返回给主进程,用户建立websocket后connection(抢连接),子进程可以自杀释放资源,同时browser.close()杀死puppeteer相关进程。之所以这样是因为测试:websocket断开连接后不久token就会失效。你还记得Github存储库中的星标吗?https://github.com/xiaozhongliu/taobao-live-crawler