【笔记/学习】c++实现b站弹幕姬

【笔记/学习】c++实现b站弹幕姬

panedioic
2019-11-26 / 0 评论 / 4 阅读 / 正在检测是否收录...
差不多是为了后续某个功能插件的开发,于是开了这么个坑。之后还可以学习下相关知识,同时由于考试腾不出太多时间学习新知识所以拿旧项目顶一下,于是就有了这篇文章。。

Step1.查找b站弹幕的http请求

随便点开一个b站的直播间,打开f12,点击网络,刷新下,找有没有弹幕的相关请求包。

之后可以发现“msg”这个包

点开,从内容看,应该就是我们要找的弹幕包了(这里先略去具体的分析过程)
然后就是http请求包的具体分析了。


从浏览器中可以看到请求的网址,消息头和相关参数,下一步我们就要用c++去模拟请求了。

step2.WinInet库的简单使用

虽然用c++模拟请求时可以直接用底层的socket去发送请求,但为了方便,所以还是去直接使用相关库了。wininet库有点类似python的request库,这里就简单介绍使用wininet库去请求了。
首先是使用wininet库必须包含的头文件:

#include <Windows.h>
#include <wininet.h>

#pragma comment(lib,"wininet.lib")

这里本来想贴一个之前学习时对我帮助挺大的一个网站的,结果找不到了,只能凭着自己的记忆写了。。
首先,我们看一眼刚才的请求包,得知请求的网址是http://api.live.bilibili.com/ajax/msg,接下来我们用wininet的函数将网址分解。
这里简单贴一段示例代码,看完应该就知道这个函数怎么用了:


展开查看

void CrackUrl()
{
    URL_COMPONENTS uc;
    char Scheme[1000];
    char HostName[1000];
    char UserName[1000];
    char Password[1000];
    char UrlPath[1000];
    char ExtraInfo[1000];
 
    uc.dwStructSize = sizeof(uc);
    uc.lpszScheme = Scheme;
    uc.lpszHostName = HostName;
    uc.lpszUserName = UserName;
    uc.lpszPassword = Password;
    uc.lpszUrlPath = UrlPath;
    uc.lpszExtraInfo = ExtraInfo;
 
    uc.dwSchemeLength = 1000;
    uc.dwHostNameLength = 1000;
    uc.dwUserNameLength = 1000;
    uc.dwPasswordLength = 1000;
    uc.dwUrlPathLength = 1000;
    uc.dwExtraInfoLength = 1000;
 
    InternetCrackUrl("http://hoge:henyo@www.cool.ne.jp:8080/masapico/api_sample.index", 0, 0, &uc);
 
    printf("scheme: '%s'\n", uc.lpszScheme);
    printf("host name: '%s'\n", uc.lpszHostName);
    printf("port: %d\n", uc.nPort);
    printf("user name: '%s'\n", uc.lpszUserName);
    printf("password: '%s'\n", uc.lpszPassword);
    printf("url path: '%s'\n", uc.lpszUrlPath);
    printf("extra info: '%s'\n", uc.lpszExtraInfo);
 
    printf("scheme type: ");
    switch(uc.nScheme)
    {
    case INTERNET_SCHEME_PARTIAL:
        printf("partial.\n");
        break;
    case INTERNET_SCHEME_UNKNOWN:
        printf("unknown.\n");
        break;
    case INTERNET_SCHEME_DEFAULT:
        printf("default.\n");
        break;
    case INTERNET_SCHEME_FTP:
        printf("FTP.\n");
        break;
    case INTERNET_SCHEME_GOPHER:
        printf("GOPHER.\n");
        break;
    case INTERNET_SCHEME_HTTP:
        printf("HTTP.\n");
        break;
    case INTERNET_SCHEME_HTTPS:
        printf("HTTPS.\n");
        break;
    case INTERNET_SCHEME_FILE:
        printf("FILE.\n");
        break;
    case INTERNET_SCHEME_NEWS:
        printf("NEWS.\n");
        break;
    case INTERNET_SCHEME_MAILTO:
        printf("MAILTO.\n");
        break;
    default:
        printf("%d\n", uc.nScheme);
    }
}


然后是代码:

#define URL_STRING L"http://api.live.bilibili.com/ajax/msg"//Bilive API

TCHAR szHostName[128];
TCHAR szUrlPath[256];
URL_COMPONENTS crackedURL = { 0 };
crackedURL.dwStructSize = sizeof(URL_COMPONENTS);
crackedURL.lpszHostName = szHostName;
crackedURL.dwHostNameLength = ARRAYSIZE(szHostName);
crackedURL.lpszUrlPath = szUrlPath;
crackedURL.dwUrlPathLength = ARRAYSIZE(szUrlPath);
InternetCrackUrl(URL_STRING, (DWORD)URL_STRING, 0, &crackedURL);

之后是和服务器建立连接:

HINTERNET hInternet = InternetOpen(L"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/15.0.849.0 Safari/535.1", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    if (hInternet == NULL)
        return -1;

    HINTERNET hHttpSession = InternetConnect(hInternet, crackedURL.lpszHostName, crackedURL.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    if (hHttpSession == NULL) {
        InternetCloseHandle(hInternet);
        std::cout << GetLastError();
        return -2;
    }

    HINTERNET hHttpRequest = HttpOpenRequest(hHttpSession, L"POST", crackedURL.lpszUrlPath, NULL, L"", NULL, 0, 0);
    if (hHttpRequest == NULL) {
        InternetCloseHandle(hHttpSession);
        InternetCloseHandle(hInternet);
        return -3;
    }

这三个函数从名字应该就能基本理解它们的作用了,我自己也不是特别精通就不讲了,感觉学过socket编程的话应该不难理解emm。。
然后就是向服务器发送请求了。

#define _HTTP_ARAC L"Content-Type: application/x-www-form-urlencoded\r\n"
char _HTTP_POST[] = "roomid=5322&csrf_token=&csrf=&visit_id=";//roomid parameters.

DWORD dwRetCode = 0;
DWORD dwSizeOfRq = sizeof(DWORD);
if (!HttpSendRequest(hHttpRequest, _HTTP_ARAC, 0, _HTTP_POST, sizeof(_HTTP_POST)) ||
    !HttpQueryInfo(hHttpRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL)
    || dwRetCode >= 400) {
    InternetCloseHandle(hHttpRequest);
    InternetCloseHandle(hHttpSession);
    InternetCloseHandle(hInternet);
    return -4;
}

经过实验,发现在基本的请求头的基础上必须要加上content-type,服务器才能正确的返回json数据,参数方面只需要roomid,其他参数都留空就可以正常使用。HttpSendRequest()就是发送http请求的函数了,应该不难看懂吧。
最后就是接收服务器返回的数据了

#define READ_BUFFER_SIZE 1024
std::string strRet = "";
BOOL bRet = FALSE;
char szBuffer[READ_BUFFER_SIZE + 1] = { 0 };
DWORD dwReadSize = READ_BUFFER_SIZE;
while (true){
    bRet = InternetReadFile(hHttpRequest, szBuffer, READ_BUFFER_SIZE, &dwReadSize);
    if (!bRet || (0 == dwReadSize)){
        break;
    }
    szBuffer[dwReadSize] = '\0';
    strRet.append(szBuffer);
}

代码应该不难看懂吧,不过最初写这段代码的时候我被坑过。虽然InternetReadFile()可以指定自己要接收的字节数,但实际每次接收的数据不一定是你写的字节数,最后总会有几个字节是乱码。所以必须用到函数给出的接收到的字节数,在后面手动加一个'\0'才行。
这样,我们就得到了想要的数据并储存到string里了。

step3.项目的c++实现

这里json数据的解析我用的是CJsonObject,用法参考我上篇转载的博文。
这里有个需要注意的地方:我们每次的请求实际上返回的是最近的10条弹幕的数据,而我们要的是持续的弹幕姬,所以我的做法是循环进行请求,每次请求后与上次的请求进行比较,打印出不同的数据,来达到想要的效果。

typedef struct {
    int uid;
    std::string name;
    std::string time;
    std::string text;
} DM_DATA;

DM_DATA old_list[10] = { 0 };
DM_DATA new_list[10] = { 0 };

这里简单定义一个结构体,并认为:假如弹幕的发送者uid,发送时间,发送内容都相同的话,就认为这是同一条弹幕,就不打印。如果有人在一秒内发送多条相同的弹幕那我也没办法啦╮( ̄▽ ̄)╭不过这种情况并不常见而且也没多大影响。

neb::CJsonObject oJson(strRet);

for (int i = 0; i < 10; i++) {
    oJson["data"]["room"][i].Get("uid", new_list[i].uid);
    if (new_list[i].uid == 0)break;
    oJson["data"]["room"][i].Get("nickname", new_list[i].name);
    oJson["data"]["room"][i].Get("timeline", new_list[i].time);
    oJson["data"]["room"][i].Get("text", new_list[i].text);
}

for (int j = 0; j < 10; j++) {
    int k = 1;
    for (int i = 0; i < 10; i++) {
        if (old_list[i].uid == new_list[j].uid &&
            old_list[i].name == new_list[j].name &&
            old_list[i].time == new_list[j].time &&
            old_list[i].text == new_list[j].text)k = 0;
    }
    if (k)std::cout << "[" << new_list[j].name << "]" << new_list[j].text << std::endl;
}
for (int i = 0; i < 10; i++) {
    old_list[i].uid = new_list[i].uid;
    old_list[i].name = new_list[i].name;
    old_list[i].time = new_list[i].time;
    old_list[i].text = new_list[i].text;
}
Sleep(3000);

neb::CJsonObject oJson(strRet);这条语句用来处理刚刚接收的json数据,然后下面的三个循环应该很好理解,第一个循环用来把接收到的数据存入新数组,第二个循环进行新数组与旧数组的比较,如不相同则打印,第三个循环把新数组的内容存入旧数组中。最后的Sleep(3000);用来防止请求过快被服务器ban。
最后我们需要的就是循环了,由于有点偷懒的原因,所以最后就加一句goto label;,把label:放在HttpSendRequest 的前面就可以了。
这样程序的设计就基本完成了。不过还需要注意的一点是,服务器返回的数据是utf8编码的,而大多数中文的windows默认是GBK编码,所以直接转换数据会乱码。最开始的时候我从网上copy了个utf8转gbk的函数,不过现在有更简单的方法,直接在代码的最前面加上一句system("chcp 65001");把控制台的编码换成utf8就ok了。

最后放个我在github上的这个项目的旧版本吧:https://github.com/panedioic/CPPDanmaku


参考资料:
[[1]【python】b站直播弹幕获取][2]

by 猫先生的早茶
[[3]WinInet编程详解]4获取bilibili直播弹幕的WebSocket协议][3]

by 炒鸡嗨客协管徐

by skilledprogrammer
[[4]C++实现Http Post请求][5]

by DoubleLi
[[5]WinInet使用详解][6]

by analogous_love

0

评论 (0)

取消