差不多是为了后续某个功能插件的开发,于是开了这么个坑。之后还可以学习下相关知识,~~同时由于考试腾不出太多时间学习新知识所以拿旧项目顶一下,~~于是就有了这篇文章。。
Step1.查找b站弹幕的http请求
随便点开一个b站的直播间,打开f12,点击网络,刷新下,找有没有弹幕的相关请求包。

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

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


从浏览器中可以看到请求的网址,消息头和相关参数,下一步我们就要用c++去模拟请求了。
step2.WinInet库的简单使用
虽然用c++模拟请求时可以直接用底层的socket去发送请求,但为了方便,所以还是去直接使用相关库了。wininet库有点类似python的request库,这里就简单介绍使用wininet库去请求了。
首先是使用wininet库必须包含的头文件:
1 2 3 4
| #include <Windows.h> #include <wininet.h>
#pragma comment(lib,"wininet.lib")
|
这里本来想贴一个之前学习时对我帮助挺大的一个网站的,结果找不到了,只能凭着自己的记忆写了。。
首先,我们看一眼刚才的请求包,得知请求的网址是http://api.live.bilibili.com/ajax/msg
,接下来我们用wininet的函数将网址分解。
这里简单贴一段示例代码,看完应该就知道这个函数怎么用了:
展开查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| 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); } }
|
然后是代码:
1 2 3 4 5 6 7 8 9 10 11
| #define URL_STRING L"http://api.live.bilibili.com/ajax/msg"
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);
|
之后是和服务器建立连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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。。
然后就是向服务器发送请求了。
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define _HTTP_ARAC L"Content-Type: application/x-www-form-urlencoded\r\n" char _HTTP_POST[] = "roomid=5322&csrf_token=&csrf=&visit_id=";
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请求的函数了,应该不难看懂吧。
最后就是接收服务器返回的数据了
1 2 3 4 5 6 7 8 9 10 11 12 13
| #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条弹幕的数据,而我们要的是持续的弹幕姬,所以我的做法是循环进行请求,每次请求后与上次的请求进行比较,打印出不同的数据,来达到想要的效果。
1 2 3 4 5 6 7 8 9
| 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,发送时间,发送内容都相同的话,就认为这是同一条弹幕,就不打印。如果有人在一秒内发送多条相同的弹幕那我也没办法啦╮( ̄▽ ̄)╭不过这种情况并不常见而且也没多大影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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]
[2]: https://blog.csdn.net/qq_43017750/article/details/88041247 “【python】b站直播弹幕获取”
by 猫先生的早茶
[3]WinInet编程详解[[2]获取bilibili直播弹幕的WebSocket协议][3]
[3]: https://blog.csdn.net/xfgryujk/article/details/80306776 “获取bilibili直播弹幕的WebSocket协议”
by 炒鸡嗨客协管徐
by skilledprogrammer
[4C++实现Http Post请求][5]
[5]: https://www.cnblogs.com/lidabo/p/3346620.html “C++实现Http Post请求”
by DoubleLi
[[5]WinInet使用详解][6]
[6]: https://blog.csdn.net/analogous_love/article/details/72515002 “WinInet使用详解” (想起来这对代码大致就是以这篇文章的代码为模板写的,所以推荐这篇文章!)
by analogous_love