matlab弹幕抓取,知乎直播弹幕抓取与解析

背景

因为想拿到一些知乎弹幕的数据 以及做一个直播播报机器人,所以最近在研究知乎直播的弹幕

分析

抓取比较简单,不多说了...都是正常的操作

但是 拿到的数据却很奇怪

为了演示方便,我们以 rest 接口示范,本质上和 websocket 接口是一样的。

我们以直播间 11529 为例子

拿取弹幕的接口是: https://www.zhihu.com/api/v4/drama/theaters/11529/recent-messages

1680ddcde8ec5bab26293e6439eb1f51.png直播间弹幕返回数据截图

可以看到弹幕数据应该在 messages 里面,但是数据好像经过了某种加密

js 大搜查

首先全局搜索 recent-messages,找到需要的 js 文件(这边也就是查找哪个 js 请求了拿弹幕的网址)

把 js 文件下载到本地格式化后,搜索 recent-messages

ddda7e7e7c9c9bdd3dc3e163ec952a83.png

搜索LOAD_RECENT_MESSAGES

找到了如何解析 message 的第一步 base64 解密

js 中 atob 函数解释[1]

a2d3cc33628936da0a005ded1bf95da5.png

并且 转换后的结果传给了函数 p

继续搜索p 往上搜索(记得搜索模式选择全词匹配与区分大小写) 要不然搜索结果太多了...

运气好 上面第一个就是

89e4a6c68a56504818de11f7c8a8f553.png

为了验证可以替换知乎 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 这个方法干了啥就可以了...

然后搜到了具体的代码

0324d80294e130f5e55804f8dad14086.png

就一步一步 debug,发现好像是某种编码规范?

难道是知乎自己定义的吗...

在这边搞了一周...还没有搞明白

大概说下 我迷惑的点在哪

aff52263182a873d9777ca1406003b2c.png

如上这个 Uint8Array

先是对第一位 >>> 操作,判断这个 字节表示的是 什么含义

然后后面的 xxx 个字节表示具体的值,但是 xxx 个字节到底是多少个,是怎么区分的 我没有弄明白

特别是如下 这三个明明都是 int64,他们的字节长度却不一样

timestampMs 是 6 个字节

theaterId 是 2 个字节

dramaId 是 9 个字节

我拿个小本本一边 debug 一边记...(本来字段少的话,看多了是可以直接找到规律 这样解决的,但是其中一个字段event是个字典,有 40 个 key...

我看到代码的时候 就炸了...

b6382e3cae28bfa0ba602d1cd96ea963.png

所以我就想 算了 不了解它到底怎么实现的把,我直接吧这段 js 抠出来...然后搭个 nodejs 的服务得了

扣 js

扣的时候 还计较简单,除了这一句的s

e instanceof s || (e = s.create(e));

复制代码

c95a88a9f5a2fd90e9ea7983c5885eca.png

这个 s 到这我就找不到它到底从哪来的了

所以我就只能 google.(搜了好多次)

b5f79f88e8ee694568ee0d9e493d00b9.png

真是惊喜!发现竟然是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 的方式。

d9642a36af89bfd3417a4b77d5c97496.png

所以我们上面那个疑惑解决了...

就是怎么确定某个字段到底应该几个字节(或者说现在能划分数据了)

数值类型

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

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。

手动反序列化

1f8b3103e5c6cae6c5849d7e98cf3745.png

以上面例子中序列化后的二进制数据流进行反序列化分析:

#!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\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 对应出某个位置的具体字段名字就好了...

最后看一个成功的截图

92263f312215494c44204a3553fc0065.png

参考资料

[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 排版

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值