Python爬取斗鱼的弹幕,看看奇葩网友都说了些什么

2021-10-05 20:15

阅读:925

标签:不清楚   info   分布式   http   流量   request   cli   模拟   mat   0.前言 前几天(寒假前咯)闲着无聊,看到舍友们都在看斗鱼TV,虽然我对那些网络游戏都不是非常感兴趣,但是我突然间想到,如果我可以获取上面的弹幕内容,不就有点意思了么? 1.分析阶段 如果我想要抓取网页上面的东西,无非就是两种方法 使用浏览器,手工(自己点击)或者非手工(使用JS脚本),存取我想要的东西。 编写HTTP客户端(斗鱼无HTTPS通讯) 第一种方法是万能的,但显然是不行的, 原因如下: 学习Python中有不明白推荐加入交流裙                  号:653466668                  群里有志同道合的小伙伴,互帮互助,                  群里有免费的视频学习教程和PDF! 手动保存实在是不可行,程序员不为也。 浏览器与本地交互有限,换而言之,也就是即使我抓取了对应的弹幕,我也没有办法解决持久化的问题。 假设你选择的是Chrome或者firefox浏览器,也不是不能实现持久化,但这需要写扩展,Chrome扩展没有写过,也不是很感兴趣。 第二种方法显然是一个正常的程序员的做法。 语言选用Ruby 写一个客户端,也就是写一个小爬虫,使用的场景: 用户在终端执行命令 gem install danmu danmu douyu [room_id/url] #比如 danmu douyu qiuri danmu douyu http://www.douyutv.com/13861 然后就可以在终端欣赏弹幕咯. Screen Shot 2016-02-09 at 12.23.15 PM.png 回想一下抓取网站的方法 四步走:请求网页(原始数据) - 提取数据(提纯数据) - 保存数据 - 分析数据 很显然,只要解决了请求网页,其他的也就无非解析和SQL语句什么的。 1.1.斗鱼TV弹幕抓取的思路确定 如果是像我上面说的那么简单,也就不必再写一篇文章。毕竟,网页小爬虫没有什么技术含量。分布式爬虫才有。 通常情况下的网页小爬虫无非要解决如下问题: 请求,如果对方有一定策略的反爬虫,那需要反反爬虫。比如, header带上host,带上refer,带上其他 需要验证,那就申请用户名和密码,然后登陆 如果在登录时期有防跨站机制,那就先获取一次登录页面,然后解析出token,带上对应的token然后登陆。 在程序中加入Log,并且存到本地。防止出现各种各样的反爬虫机制ban掉了程序,从而方便进行下一步防反爬虫对策。 并且,由于请求响应机制的存在,通常情况下,每一个请求对应一个响应,如果出错了,要么超时,要么有状态码,所以普通的web爬虫也相对而言比较容易些。 那么,斗鱼TV的站点是不是这样子的就能够容易爬取呢? 你猜到了,答案是“不是”。 由于弹幕具有实时性,就决定了斗鱼TV的弹幕无法通过保存完整指定时间端弹幕的XML(比如BILIBILI的一个视频弹幕是存在一段xml中的)或者Json数据来显示弹幕。要不然的话,那主播操作很出色的时候,观众的弹幕岂不是无法实时显示了么? 那么,肯定就是WebSocket了,于是,我一如既往的打开F12,查看网络流量。 正如你想到的那样,没有任何的弹幕流量来往。一个WebSocket的消息都没有。 那么,消息肯定是有的,但是消息并不是通过HTTP协议或者WebSocket协议传输的,那么问题会出在哪呢? 分析前端的代码,找出获取弹幕的JS代码,苦于代码太多,找了很久没有找到。那也就是执行逻辑可能在flash里面。 于是祭出大杀器WireShark,抓一下流量。终于看到弹幕的样子了。 是这样的。 douyutveachmsg.png 原来使用的是Flash的Socket功能。 那么,我们只需要模拟Socket的每一条消息就好了. 多分析几组数据,但还是对发送消息内容缺乏把握,特别是在用户认证,用户接收弹幕这一块。在搜索引擎上搜索了一阵,发现知乎上有个帖子,读完终于解了我的疑惑。 地址为: www.zhihu.com/question/29… 在此基础上,省略若干消息分析过程。 总结后得出斗鱼TV网站的服务器分布。 douyutvinfo.jpg 1.2.房间信息和弹幕认证服务器获取 首先我们拿随便一个主播房间来说,比如,qiuri Ta的房间链接分为两种 www.douyutv.com/qiuri www.douyutv.com/[房间id] 对这个主播房间页面请求,正常,所有的有用信息都不是放在HTML中渲染出来,而是有一条放在HTML中内置的JS脚本中,这是为了减少服务器渲染HTML的压力?可是渲染放在JS里面不也一样需要渲染?(不明白)总之,就是程序先加载没有具体数据填充页面,然后JS更新数据。 内置的两段JS脚本,JS脚本中有两个变量,该变量很容易转换成JSON数据,也就是两段JSON数据,一个是关于主播的个人信息,另一个是关于弹幕认证服务器的列表(该列表中的任意一个服务器均可以认证,但每一次请求主播页面得到的认证服务器列表都不一样) Sc Screen Shot 2016-02-09 at 1.01.51 PM.png Screen Shot 2016-02-09 at 12.44.01 PM.png 通过这步,我们就拿到了主播的信息以及弹幕服务器的认证地址,端口。 1.3.发送Socket消息的流程简介 我们通过抓包,分析那一大坨数据包,可以确定以下通过以下的流程便可以获取弹幕消息。(分析过程比较繁琐) 首先建立两个Socket。一个用于认证(@danmu_auth_socket),另一个用户获取弹幕(@danmu_client)。 步骤1: @danmu_auth_socket 发送消息登陆,获取消息1解析出匿名用户的用户名,再获取消息2解析出gid 步骤2: @danmu_auth_socket 发送qrl消息,获取两个没有什么用的消息 步骤3: @danmu_auth_socket 发送keeplive消息 步骤4: @danmu_socket 发送伪登陆消息(所有匿名用户都一样只需要输入步骤一中用户名就行了,因为认证已经在上面做过了) 步骤5: @danmu_socket 发送join_group消息需要步骤一中国的gid 步骤6: @danmu_socket 不断的recv消息就可以获取弹幕消息了 后面会详细解释 2.1.消息Socket消息格式以及发送一条消息 既然是发消息,那么每条消息总是有些格式的。 斗鱼的消息格式大致如下: douyutveachmsg.png 每一条消息并遵循下面的格式: 1.通信协议长度,后四个部分的长度,四个字节2.第二部分与第一部分一样3.请求代码,发送给斗鱼的话,内容为0xb1,0x02, 斗鱼返回的代码为0xb2,0x024.发送内容5.末尾字节 # -*- encoding : utf-8 -*- class Message # 向斗鱼发送的消息 # 1.通信协议长度,后四个部分的长度,四个字节 # 2.第二部分与第一部分一样 # 3.请求代码,发送给斗鱼的话,内容为0xb1,0x02, 斗鱼返回的代码为0xb2,0x02 # 4.发送内容 # 5.末尾字节 #pack(‘c*‘)是字节数组转字符串的一种诡异的转化方式 def initialize(content) @length = [content.size + 9,0x00,0x00,0x00].pack(‘c*‘) @code = @length.dup @magic = [0xb1,0x02,0x00,0x00].pack(‘c*‘) @content = content @end = [0x00].pack(‘c*‘) end def to_s @length + @code + @magic + @content + @end end end 经过封装,我们仅仅关注那些可见的字符串,也就是Content部分就可以了。content部分,也就是发送消息的内容,在文章后面将会详解。 开启两个Socket,一个用户认证,另一个用于弹幕的获取。 用于用户弹幕认证的,是2.1中所说的认证服务器列表中任意一个。挑选出来一组ip和端口 @danmu_auth_socket = TCPSocket.new @auth_dst_ip,@auth_dst_port 用户获取弹幕的只要为 danmu.douyutv.com:8601 danmu.douyutv.com:8602 danmu.douyutv.com:12601 danmu.douyutv.com:12602 四组域名:端口均可以作为如下的DANMU_SERVER和PORT @danmu_socket = TCPSocket.new DANMU_SERVER,DANMU_PORT 发送一条消息只需如此 data = "type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" + @room_id.to_s + "/" all_data = message(data) @danmu_socket.write all_data 把需要传输的字符串放进去就好了. 接下来,我们需处理上面说的六个步骤 2.2.发送消息详细流程之步骤一 发送消息内容为: type@=loginreq/username@=/ct@=0/password@=/roomid@=156277/devid@=DF9E4515E0EE766B39F8D8A2E928BB7C/rt@=1453795822/vk@=4fc6e613fc650a058757331ed6c8a619/ver@=20150929/ 我们需要注意的内容如下: type 表示消息的类型登陆消息为loginreq username 不需要,请求登陆以后系统会自动的返回对应的游客账号。 ct 不清楚什么意思,默认为0并无影响 password 不需要 roomid 房间的id devid 为设备标识,无所谓,所以我们使用随机的UUID生成 rt 应该是runtime吧,时间戳 vk 为时间戳+"7oE9nPEG9xXV69phU31FYCLUagKeYtsF"+devid的字符串拼接结果的MD5值(这个是参考了一篇文章,关于这一处我也不大明白怎么探究出来的) ver 默认 通过这一步,我们可以获取两条消息,并从消息中使用正则表达式获取对应的用户名以及gid str = @danmu_auth_socket.recv(4000) @username= str[/\/username@=(.+)\/nickname/,1] str = @danmu_auth_socket.recv(4000) @gid = str[/\/gid@=(\d+)\//,1] 2.3.发送消息详细流程之步骤二 发送的消息内容为 "type@=qrl/rid@=" + @room_id.to_s + "/" 无需多说,类型为qrl,rid为roomid,直接发送这条消息就好。返回的两条消息也没有什么价值。 data = "type@=qrl/rid@=" + @room_id.to_s + "/" msg = message(data) @danmu_auth_socket.write msg str = @danmu_auth_socket.recv(4000) str = @danmu_auth_socket.recv(4000) 2.4.发送消息详细流程之步骤三 发送的消息内容为 "type@=keeplive/tick@=" + timestamp + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/" 直接发送。无太大意义。 data = "type@=keeplive/tick@=" + timestamp + "/vbw@=0/k@=19beba41da8ac2b4c7895a66cab81e23/" msg = message(data) @danmu_auth_socket.write msg str = @danmu_auth_socket.recv(4000) 前三步,也就是2.2-2.3-2.4三步骤,也就是使用@danmu_auth_socket 完成获取username和gid的重要步骤。获取这两个字段以后,也就完成了它存在的使命。 接下来的就是@danmu_socket获取弹幕的时候了! 2.5.发送消息详细流程之步骤四 消息内容为:"type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" + @room_id.to_s + "/" 和上面2.2中略有不同。但是,需要注意的是 username 为2.2中所得到的username password 的值得变化 data = "type@=loginreq/username@="+@username+"/password@=1234567890123456/roomid@=" + @room_id.to_s + "/" all_data = message(data) @danmu_socket.write all_data str = @danmu_socket.recv(4000) 2.6.发送消息详细流程之步骤五 接下来就是完成认证的最后一步了,join_group的消息内容为 "type@=joingroup/rid@=" + @room_id.to_s + "/gid@="+@gid+"/" gid为2.2中所得到的gid。 data = "type@=joingroup/rid@=" + @room_id.to_s + "/gid@="+@gid+"/" msg = message(data) @danmu_socket.write msg 2.7.发送消息详细流程之步骤六 获取弹幕,并且打印出来。 danmu_data = @danmu_socket.recv(4000) type = danmu_data[danmu_data.index("type@=")..-3] puts type.gsub(‘sui‘,‘‘).gsub(‘@S‘,‘/‘).gsub(‘@A=‘,‘:‘).gsub(‘@=‘,‘:‘).split(‘/‘) 后三步,则是@danmu_socket 获取弹幕的步骤。 于是,通过这些步骤,就可以完成了简单的danmu的核心代码,接下来的步骤就是完善,重构这些代码了。 总结 痛点一,至今还没有解决rtmp地址的获取 找了很久没有办法解决rtmp地址的自动获取: 路径如下 www.douyutv.com/swf_api/roo… 这一处的请求不是XHR,也就是不是JS脚本通过XMLHttpRequest异步加载;那么,八成是flash通过http协议获取的。我估计八成执行逻辑应该是在flash之中。也就不方便获取其中的sign值.故,暂时无法解析rtmp视频流地址了Python爬取斗鱼的弹幕,看看奇葩网友都说了些什么标签:不清楚   info   分布式   http   流量   request   cli   模拟   mat   原文地址:https://www.cnblogs.com/520lmx/p/10224684.html


评论


亲,登录后才可以留言!