抓取抖音直播的评论数据的python代码 抓取直播间评论
背景
因为想拿到一些知乎弹幕的数据 以及做一个直播播报机器人,所以最近在研究知乎直播的弹幕
分析
抓取比较简单,不多说了...都是正常的操作
但是 拿到的数据却很奇怪
为了演示方便,我们以 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 解密
js 中 atob 函数解释[1]
并且 转换后的结果传给了函数 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[2]
所以这个所谓的加密 是一种通用的协议...
至此,问题就简单了
Protocol Buffers
官方的定义如下:
Protocol buffers 是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。
更多介绍可以去看protocol-buffers 官网[3]
下面的内容来自 Burpsuite 中 protobuf 数据流的解析[4]
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 个字符“Vincent@test.com”;
#!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: "Vincent@test.com"
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 2 { 1 { 1 { 1: "40b47ccbfc4475b018a5a417e1cf9897" 2: "\\345\\260\\217\\351\\200\\217\\346\\230\\216\\347\\210\\261\\345
\\276\\267\\345\\215\\216" 3: "du-yao-13-86" 4: "https://pic4.zhimg.com/v2-d06161b1d39cd64ebda40f9c065a6ca5_xs.jpg" 5 { 1: 1152
9 2: "\\351\\205\\222\\347\\252\\235\\347\\252\\235" 3: 1 4: 0 5: 37 6: 1 7 { 1: 1
2: 0 3: "https://pic1.zhimg.com/v2-1d12585c7a96613d3bef41172cd8ef15_r.png" 4: "https://pic4.zhimg.com/v2-b035edd5076807572bd4e7a8924c
e13b_r.png" 5: "#72BBFF" 5: "#0084FF" } } 6 { 1: 9 2: 12300 3: "https://pic4.zhimg.com/v2-82554ec83beb32e9ec4414f4c622ac2c_r.png" } } 2: 1 3: "\\346\\210\\221\\344\\271\\237\\350\\247\\211\\345\\276\\227" 4: 1227391966855839744 } } 3: 1585413048873 4: 11529 5: 1227323967020912640 6: "1-1227391966855839744" 7: 1 '
复制代码
可以看到解析成功了,那么后面的工作就比较简单了...
只要按照知乎的 js 对应出某个位置的具体字段名字就好了...
最后看一个成功的截图
参考资料
[1]
js中atob函数解释: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowBase64/atob
[2]
protobuf: https://github.com/protobufjs/protobuf.js
[3]
protocol-buffers官网: https://developers.google.com/protocol-buffers
[4]
Burpsuite中protobuf数据流的解析: https://wooyun.js.org/drops/Burpsuite%E4%B8%ADprotobuf%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E8%A7%A3%E6%9E%90.html
本文使用 mdnice 排版