知乎直播弹幕抓取与解析
分析
抓取比较简单,不多说了…都是正常的操作
但是 拿到的数据却很奇怪
为了演示方便,我们以 rest接口示范,本质上和websocket接口是一样的。
我们以直播间11529为例子
拿取弹幕的接口是: https://www.zhihu.com/api/v4/drama/theaters/11529/recent-messages
可以看到弹幕数据应该在messages里面,但是数据好像经过了某种加密
js 大搜查
首先全局搜索 recent-messages
,找到需要的js文件(这边也就是查找哪个js请求了拿弹幕的网址)
把js文件下载到本地格式化后,搜索recent-messages
搜索 LOAD_RECENT_MESSAGES
找到了如何解析message的第一步 base64解密
并且 转换后的结果传给了函数 p
继续搜索 p
往上搜索(记得搜索模式选择全词匹配与区分大小写) 要不然搜索结果太多了…
运气好 上面第一个就是
为了验证可以替换知乎js 到你本地的js
加两行 console.log
就行了
代码如下…
function p(e) { console.log("before:", e); var t = d.EventMessage.decode(e), n = t.eventCode, r = t.event; console.log("after:", t);
可以发现这就是我们想要的
那么现在只要搞清楚 EventMessage.decode
这个方法干了啥就可以了…
然后搜到了具体的代码
就一步一步debug,发现好像是某种编码规范?
难道是知乎自己定义的吗…
在这边搞了一周…还没有搞明白
大概说下 我迷惑的点在哪
如上这个 Uint8Array
先是对第一位 >>>
操作,判断这个 字节表示的是 什么含义
然后后面的xxx 个字节表示具体的值,但是xxx个字节到底是多少个,是怎么区分的 我没有弄明白
特别是如下 这三个明明都是 int64,他们的字节长度却不一样
timestampMs
是 6个字节
theaterId
是 2个字节
dramaId
是 9个字节
我拿个小本本一边debug一边记…(本来字段少的话,看多了是可以直接找到规律 这样解决的,但是其中一个字段 event
是个字典,有40个key…
我看到代码的时候 就炸了…
所以我就想 算了 不了解它到底怎么实现的把,我直接吧这段js抠出来…然后搭个nodejs的服务得了
扣js
扣的时候 还比较简单,除了这一句的 s
e instanceof s || (e = s.create(e));
这个s到这我就找不到它到底从哪来的了
所以我就只能google.(搜了好多次)
真是惊喜!发现竟然是 protobuf
所以这个所谓的加密 是一种通用的协议…
至此,问题就简单了
Protocol Buffers
官方的定义如下:
Protocol buffers是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。
更多介绍可以去看 protocol-buffers官网
下面的内容来自 Burpsuite中protobuf数据流的解析
Varint编码
Protobuf的二进制使用Varint编码。Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010。
下图演示了protobuf如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为protobuf 字节序采用 little-endian 的方式。
所以我们上面那个疑惑解决了…
就是怎么确定某个字段到底应该几个字节(或者说现在能划分数据了)
数值类型
Protobuf经序列化后以二进制数据流形式存储,这个数据流是一系列key-Value对。Key用来标识具体的Field,在解包的时候,Protobuf根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 Field。
Key 的定义如下:
(field_number << 3) | wire_type
Key由两部分组成。第一部分是 field_number,比如消息 tutorial .Person中 field name 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型。Wire Type 可能的类型如下表所示:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
以数据流:08 96 01为例分析计算key-value的值:
#!bash 08 = 0000 1000b => 000 1000b(去掉最高位) => field_num = 0001b(中间4位), type = 000(后3位) => field_num = 1, type = 0(即Varint) 96 01 = 1001 0110 0000 0001b => 001 0110 0000 0001b(去掉最高位) => 1 001 0110b(因为是little-endian) => 128+16+4+2=150
最后得到的结构化数据为:
1:150
其中1表示为field_num,150为value。
手动反序列化
以上面例子中序列化后的二进制数据流进行反序列化分析:
#!bash 0A = 0000 1010b => field_num=1, type=2; 2E = 0010 1110b => value=46; 0A = 0000 1010b => field_num=1, type=2; 07 = 0000 0111b => value=7;
读取7个字符“Vincent”;
#!bash 10 = 0001 0000 => field_num=2, type=0; 09 = 0000 1001 => value=9; 1A = 0001 1010 => field_num=3, type=2; 10 = 0001 0000 => value=16;
读取10个字符“
[email protected]
”;
#!bash 22 = 0010 0010 => field_num=4, type=2; 0F = 0000 1111 => value=15; 0A = 0000 1010 => field_num=1, type=2; 0B = 0000 1011 => value=11;
读取11个字符“15011111111”;
#!bash 10 = 0001 0000 => field_num=2, type=0; 02 = 0000 0010 => value=2;
最后得到的结构化数据为:
#!bash
1 {
1: "Vincent"
2: 9
3: "[email protected]"
4 {
1: "15011111111"
2: 2
}
}
使用protoc反序列化
实现操作经常碰到较复杂、较长的流数据,手动分析确实麻烦,好在protoc加“decode_raw”参数可以解流数据,我实现了一个python脚本供使用:
import subprocess, sys import json import base64 def decode(data): process = subprocess.Popen( ["protoc", "--decode_raw"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) output = error = None try: output, error = process.communicate(data) except OSError: pass finally: if process.poll() != 0: process.wait() return output with open(sys.argv[1], "rb") as f: data = f.read() print('',decode(data))
回到知乎直播
那么就先测试解析一条吧
import subprocess, sys import json import base64 def decode(data): process = subprocess.Popen( ["protoc", "--decode_raw"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) output = error = None try: output, error = process.communicate(data) except OSError: pass finally: if process.poll() != 0: process.wait() return output a1 = "CAESpgMKowMKhgMKIDQwYjQ3Y2NiZmM0NDc1YjAxOGE1YTQxN2UxY2Y5ODk3EhLlsI/pgI/mmI7niLHlvrfljY4aDGR1LXlhby0xMy04NiJBaHR0cHM6Ly9waWM0LnpoaW1nLmNvbS92Mi1kMDYxNjFiMWQzOWNkNjRlYmRhNDBmOWMwNjVhNmNhNV94cy5qcGcqswEIiVoSCemFkueqneeqnRgBIAAoJTABOpoBCAEQABpAaHR0cHM6Ly9waWMxLnpoaW1nLmNvbS92Mi0xZDEyNTg1YzdhOTY2MTNkM2JlZjQxMTcyY2Q4ZWYxNV9yLnBuZyJAaHR0cHM6Ly9waWM0LnpoaW1nLmNvbS92Mi1iMDM1ZWRkNTA3NjgwNzU3MmJkNGU3YTg5MjRjZTEzYl9yLnBuZyoHIzcyQkJGRioHIzAwODRGRjJHCAkQjGAaQGh0dHBzOi8vcGljNC56aGltZy5jb20vdjItODI1NTRlYzgzYmViMzJlOWVjNDQxNGY0YzYyMmFjMmNfci5wbmcQARoM5oiR5Lmf6KeJ5b6XIICA8cTaz6SEERiplO2Pki4giVoogKDrtNOUlYQRMhUxLTEyMjczOTE5NjY4NTU4Mzk3NDQ4AQ==" message = base64.b64decode(a1) print(decode(message))
结果如下
Out[7]: b'1: 1\n2 {\n 1 {\n 1 {\n 1: "40b47ccbfc4475b018a5a417e1cf9897"\n 2: "\\345\\260\\217\\351\\200\\217\\346\\230\\216\\347\\210\\261\\345 \\276\\267\\345\\215\\216"\n 3: "du-yao-13-86"\n 4: "https://pic4.zhimg.com/v2-d06161b1d39cd64ebda40f9c065a6ca5_xs.jpg"\n 5 {\n 1: 1152 9\n 2: "\\351\\205\\222\\347\\252\\235\\347\\252\\235"\n 3: 1\n 4: 0\n 5: 37\n 6: 1\n 7 {\n 1: 1\n 2: 0\n 3: "https://pic1.zhimg.com/v2-1d12585c7a96613d3bef41172cd8ef15_r.png"\n 4: "https://pic4.zhimg.com/v2-b035edd5076807572bd4e7a8924c e13b_r.png"\n 5: "#72BBFF"\n 5: "#0084FF"\n }\n }\n 6 {\n 1: 9\n 2: 12300\n 3: "https://pic4.zhimg.com/v2-82554ec83beb32e9ec4414f4c622ac2c_r.png"\n }\n }\n 2: 1\n 3: "\\346\\210\\221\\344\\271\\237\\350\\247\\211\\345\\276\\227"\n 4: 1227391966855839744\n }\n}\n3: 1585413048873\n4: 11529\n5: 1227323967020912640\n6: "1-1227391966855839744"\n7: 1\n'
可以看到解析成功了,那么后面的工作就比较简单了…
只要按照知乎的js对应出某个位置的具体字段名字就好了…
最后看一个成功的截图
本文作者:高金
本文地址: https://igaojin.me/2020/03/29/知乎直播弹幕抓取与解析/
版权声明:转载请注明出处!