目录
一.生成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的版本。解压之后加入环境变量
使用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()