【晚风摇叶之其他】抖音直播弹幕解析,连接websocket解析弹幕内容

发布于:2024-05-21 ⋅ 阅读:(465) ⋅ 点赞:(0)

目录

一.生成websocket的url

1.查看网络面板拿到url,分析url

2.分析url

1.获取room_id,user_unique_id

2.获取signature

3.拼接url

二.连接websocket解析弹幕内容

1.查找消息体序列化方式

2.编写proto对象信息

3.python连接websocket

问题1:cookie问题

问题2:连上不久自动断开了

4.完整测试连接代码


一.生成websocket的url

1.查看网络面板拿到url,分析url

打开一个直播间,F12查看网络面板,可以看到websocket的url,简单分析三个关键参数room_id,user_unique_id,signature,如下图

2.分析url

1.获取room_id,user_unique_id

这两个参数都在房间号首页请求的html里,直接正则表达式获取就行了,如下图

然而postman直接get首页,却返回的一段加密的html,还有两个cookie:__ac_nonce和ttwid,如下图

尝试寻找原因:浏览器无痕模式保留日志跟踪一下请求,发现访问了两次首页,如下图

        第一次返回加密的html,返回了两个的Cookie:__ac_nonce和ttwid

        第二次返回了真实房间数据,请求时,发送了__ac_nonce和__ac_signature两个Cookie,很明显,应该是第一次返回的html有相关的js,生成了__ac_signature这个cookie

查看加密的html里的js,果然是根据__ac_nonce生成__ac_signature的cookie,如下图。

关键就是解密window.byted_acrawler.sign()函数计算结果值就可以了。__ac_nonce应该是个随机数,实测用一个固定值就行,但每次生成的__ac_signature不一样

2.获取signature

js里查找生成signature的代码,如下图,简单概括:e里存着数据,t是数组,循环t从e里按顺序取值然后拼接一个字符串o,用o进行MD5得到a,然后调用window.byted_acrawler.frontierSign()函数计算出signature的值

其中数组t的值如下,关键值room_id,user_unique_id在上面已经已经获取到,很容易生成MD5
["live_id", "aid", "version_code", "webcast_sdk_version", "room_id", "sub_room_id", "sub_channel_id", "did_rule", "user_unique_id", "device_platform", "device_type", "ac", "identity"]

关键就是解密window.byted_acrawler.frontierSign()函数计算结果值就可以了。其中sub_room_id和sub_channel_id可能跟副房间相关,暂不考虑

3.拼接url

获取到room_id,user_unique_id,signature三个的值之后直接拼接url就可以了。实测url里的browser_version和cursor不加也可以访问通,去掉这两个url就短了很多。

直接把这个做成一个接口,根据web_rid返回url,供下面的程序调用,需要接口地址私我

二.连接websocket解析弹幕内容

1.查找消息体序列化方式

跟踪websocket解析内容,如下图,使用的是protobuf协议,有headers,payload,使用gzip压缩了payload等信息

2.编写proto对象信息

根据js编写proto对象信息,简单编写聊天和礼物的信息,其他的信息不写了,把以下对象信息保存到douyin.proto文件里

syntax = "proto3";

// protoc --python_out=./ .\douyin.proto
package douyin;

message PushHeader{
  string key = 1;
  string value = 2;
}

message PushFrame{
  uint64 seqid = 1;
  uint64 logid = 2;
  uint64 service = 3;
  uint64 method = 4;
  repeated PushHeader headersList = 5;
  string payloadEncoding = 6;
  string payloadType = 7;
  bytes payload = 8;
}

message Message{
  string method = 1;
  bytes payload = 2;
  int64 msgId = 3;
  int32 msgType = 4;
  int64 offset = 5;
  bool needWrdsStore = 6;
  int64 wrdsVersion = 7;
  string wrdsSubKey = 8;
  map<string, string> messageExtraMap = 9;
}

message Response{
  repeated Message messagesList = 1;
  string cursor = 2;
  int64 fetchInterval = 3;
  int64 now = 4;
  string internalExt = 5;
  int32 fetchType = 6;
  map<string, string> routeParamsMap = 7;
  int64 heartbeatDuration = 8;
  bool  needAck = 9;
  string pushServer = 10;
  string liveCursor = 11;
  bool historyNoMore = 12;
}

// 用户
message User{
  int64 id = 1;
  int64 shortId = 2;
  string nickname = 3;
  int32 gender = 4;
  int32 level = 6;
  string sec_uid = 46;
}

// 弹幕
message ChatMessage{
  User user = 2;
  string content = 3;
  bool visibleToSender = 4;
}

// 礼物
message GiftMessage{
  int64 giftId = 2;
  int64 fanTicketCount = 3;
  int64 groupCount = 4;
  int64 repeatCount = 5;
  int64 comboCount = 6;
  User user = 7;
  int64 groupId = 11;
  GiftStruct gift = 15;
  int64 sendType = 16;
  int64 totalCount = 29;
}
message GiftStruct{
  int64 id = 5;
  string name = 16;
  bool combo = 10;
  string goldEffect = 24;
  int64 goldenBeans = 26;
  int32 itemType = 28;
  int32  type = 11;
}

3.python连接websocket

python安装包

pip install protobuf
pip install websocket-client

下载proroc程序,注意版本和python的版本。解压之后加入环境变量

Releases · protocolbuffers/protobuf · GitHub

使用protoc生成python代码文件,会生成一个douyin_pb2.py文件

protoc --python_out=./ .\douyin.proto

使用websocket-client连接生成的url,启动之后立马就被关闭了连接,失败了

问题1:cookie问题

尝试直接复制浏览器里的url,还是一样失败了,这种情况一般是时间戳要么header,cookie的问题,加上header和cookie试一下,成功了

最终测试加上了User-Agent头和cookie的ttwid值

问题2:连上不久自动断开了

持续链接保持心跳问题,使用ack保持连接,如下图

4.完整测试连接代码

from websocket import WebSocketApp
from douyin_pb2 import PushFrame, Response, ChatMessage, GiftMessage
import gzip


def decode_message(ws, msg):
    frame = PushFrame()
    frame.ParseFromString(msg)
    ori_byte = gzip.decompress(frame.payload)

    response = Response()
    response.ParseFromString(ori_byte)

    if ws is not None and response.needAck:
        s = PushFrame()
        s.payloadType = "ack"
        s.payload = response.internalExt.encode('utf-8')
        s.logid = frame.logid
        ws.send(s.SerializeToString())

    for item in response.messagesList:
        if item.method == "WebcastGiftMessage":
            message = GiftMessage()
            message.ParseFromString(item.payload)
            info = f"【礼物】{message.user.nickname} 赠送: {message.totalCount}个{message.gift.name} secid:{message.user.sec_uid}"
            print(info)
        elif item.method == "WebcastChatMessage":
            message = ChatMessage()
            message.ParseFromString(item.payload)
            info = f"【弹幕】{message.user.nickname} 说: {message.content} secid:{message.user.sec_uid}"
            print(info)
        else:
            # print(item.method)
            continue


def on_open(ws):
    print("on_open")


def on_message(ws, message):
    decode_message(ws, message)


def on_error(ws, message):
    print("error", message)


def on_close(ws, code, message):
    print("close", code, message)


def run_ws():
    wss_url = ''
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36",
    }
    ttwid = ""
    ws = WebSocketApp(
        url=wss_url,
        header=headers,
        cookie=f"ttwid={ttwid}",
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.run_forever()


# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
    run_ws()