爬取斗鱼弹幕大致分为以下几个主要步骤
代码地址:https://github.com/Recru1t000/douyuCrawler
斗鱼弹幕推送是通过websocket进行的消息推送。
Websocket简介:WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信。
那么想要获取斗鱼所推送的弹幕信息第一步就应该连接上斗鱼弹幕推送的websocket。
我所运用的是通过maven获取的Java-WebSocket的1.5.1版本进行的连接的。以下为maven中的设置。
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.1</version>
</dependency>
首先要新建一个WebSocketClient对象,需要赋予WebSocketClient一个新的URI对象,一个新的Draft对象,并且重写onOpen、onMessage、onClose、onError四种方法。
接下来运行websocketclient.connect()
,看到打开连接就表示我们已经成功连接到服务器。以下为java代码。
WebSocketClient websocketclient = new WebSocketClient(new URI("wss://danmuproxy.douyu.com:8506/"
), new Draft_6455()) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("打开连接");
}
@Override
public void onMessage(String message) {
System.out.println(message);
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("连接关闭");
}
@Override
public void onError(Exception e) {
System.out.println("发生错误");
}};
在成功连接服务器后,我们却还不能收到服务器所推送的信息。这是因为我们还需要发送登录请求和入组请求,详情请看斗鱼开发协议https://open.douyu.com/source/api/63
斗鱼消息协议格式如下所示,其中字段说明如下:
在发送登录请求和入组请求前,需要将发送信息变成符合斗鱼协议的格式。
登录请求内容:type@=loginreq/roomid@=123456/
分组请求内容:type@=joingroup/rid@=123456/gid@=-9999/
下面为代码:
public byte[] login(String roomId) throws IOException {
String message = "type@=loginreq/roomid@=123456/";
return douyuRequestEncode(message);
}
//加入群组请求
public byte[] joinGroup(String roomId) throws IOException{
String message ="type@=joingroup/rid@=123456/gid@=-9999/";
return douyuRequestEncode(message);
}
//心跳
public byte[] heartBeat() throws IOException{
String message = "type@=mrkl/";
return douyuRequestEncode(message);
}
//将传入的数据变成符合斗鱼协议要求的字节流返回
public byte[] douyuRequestEncode(String message) throws IOException {
int dataLen1 = message.length() + 9;//4 字节小端整数,表示整条消息(包括自身)长度(字节数)。
int dataLen2 = message.length() + 9;//消息长度出现两遍,二者相同。
int send = 689;//689 客户端发送给弹幕服务器的文本格式数据,暂时未用,默认为 0。保留字段:暂时未用,默认为 0。
byte[] msgBytes= message.getBytes(StandardCharsets.UTF_8);
int end = 0;
byte[] endBytes = new byte[1];
endBytes[0] = (byte) (end & 0xFF);;//结尾必须为‘\0’。详细序列化、反序列化
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(intToBytesLittle(dataLen1));
bytes.write(intToBytesLittle(dataLen2));
bytes.write(intToBytesLittle(send));
bytes.write(msgBytes);
bytes.write(endBytes);
//返回byte[]
return bytes.toByteArray();
}
//将整形转化为4位小端字节流
public byte[] intToBytesLittle(int value) {
return new byte [] {
(byte) (value & 0xFF),
(byte) ((value >> 8) & 0xFF),
(byte) ((value >> 16) & 0xFF),
(byte) ((value >> 24) & 0xFF)
};}
完成这步后,在websocket中发送完消息理论应该是可以收到信息的,但却出现了两个问题onMessage中的System.out.println(message);
却没有打印出信息。并且在一段时间后连接就会自动断开。
第一个问题:因为我们接受到的斗鱼推送信息是以字节流方式存在的,所以直接用重写的onMessage(String message)方法是无法输出信息的,点开websocketClient的源码,我们看到public void onMessage( ByteBuffer bytes ) {//To overwrite}
这样一条代码。我们应该在新建websocket时重写该方法,使我们能够正常输出String类型的消息。
第二个问题:想要保持斗鱼弹幕推送的长连接需要定时发送心跳,心跳的内容为:type@=mrkl/。斗鱼的要求是每隔45秒发送心跳。所以在创建连接时直接并发一个每隔45秒发送一次消息的线程。
基础可运行代码如下所示。
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
public class test {
@Test
public void crawl() throws URISyntaxException {
WebSocketClient websocketclient = new WebSocketClient(new URI("wss://danmuproxy.douyu.com:8506/"
), new Draft_6455()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
try {
send(login());//发送登录请求
send(joinGroup());//发送加入群组请求
send(heartBeat());//发送心跳
Thread heartBeatThread = new Thread(() -> {
while (true)
{
try {
send(heartBeat());
System.out.println("发送心跳");
Thread.sleep(45000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
});
heartBeatThread.start();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("打开连接");
}
@Override
public void onMessage(String message) {
}
public void onMessage(ByteBuffer bytes)
{
Charset charset = StandardCharsets.UTF_8;
CharBuffer charBuffer = charset.decode(bytes);
String s = charBuffer.toString();
System.out.println(s);
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("连接关闭");
}
@Override
public void onError(Exception e) {
System.out.println("发生错误");
}};
websocketclient.run();
}
public byte[] login() throws IOException {
String message = "type@=loginreq/roomid@=5189167/";
return douyuRequestEncode(message);
}
//加入群组请求
public byte[] joinGroup() throws IOException{
String message ="type@=joingroup/rid@=123456/gid@=-9999/";
return douyuRequestEncode(message);
}
//心跳
public byte[] heartBeat() throws IOException{
String message = "type@=mrkl/";
return douyuRequestEncode(message);
}
//将传入的数据变成符合斗鱼协议要求的字节流返回
public byte[] douyuRequestEncode(String message) throws IOException {
int dataLen1 = message.length() + 9;//4 字节小端整数,表示整条消息(包括自身)长度(字节数)。
int dataLen2 = message.length() + 9;//消息长度出现两遍,二者相同。
int send = 689;//689 客户端发送给弹幕服务器的文本格式数据,暂时未用,默认为 0。保留字段:暂时未用,默认为 0。
byte[] msgBytes= message.getBytes(StandardCharsets.UTF_8);
int end = 0;
byte[] endBytes = new byte[1];
endBytes[0] = (byte) (end & 0xFF);;//结尾必须为‘\0’。详细序列化、反序列化
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(intToBytesLittle(dataLen1));
bytes.write(intToBytesLittle(dataLen2));
bytes.write(intToBytesLittle(send));
bytes.write(msgBytes);
bytes.write(endBytes);
//返回byte[]
return bytes.toByteArray();
}
//将整形转化为4位小端字节流
public byte[] intToBytesLittle(int value) {
return new byte [] {
(byte) (value & 0xFF),
(byte) ((value >> 8) & 0xFF),
(byte) ((value >> 16) & 0xFF),
(byte) ((value >> 24) & 0xFF)
};
}
}