75

知乎直播弹幕抓取与解析

 4 years ago
source link: https://igaojin.me/2020/03/29/%E7%9F%A5%E4%B9%8E%E7%9B%B4%E6%92%AD%E5%BC%B9%E5%B9%95%E6%8A%93%E5%8F%96%E4%B8%8E%E8%A7%A3%E6%9E%90/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

分析

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

但是 拿到的数据却很奇怪

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

我们以直播间11529为例子

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

11529.png

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

js 大搜查

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

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

2.png

搜索 LOAD_RECENT_MESSAGES

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

js中atob函数解释

1.png

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

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

运气好 上面第一个就是

3.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 这个方法干了啥就可以了…

然后搜到了具体的代码

4.png

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

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

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

大概说下 我迷惑的点在哪

5.png

如上这个 Uint8Array

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

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

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

timestampMs 是 6个字节

theaterId 是 2个字节

dramaId 是 9个字节

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

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

6.png

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

扣js

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

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

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

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

8.png

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

9.jpg

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

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

数值类型

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。

手动反序列化

10.jpg

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

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

最后看一个成功的截图

11.png

本文作者:高金

本文地址: https://igaojin.me/2020/03/29/知乎直播弹幕抓取与解析/

版权声明:转载请注明出处!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK