用c++寫bilibili番劇搶樓程序
說明:轉載該文章只爲學習而用,如果侵權請留言,我會盡快刪除
當然我不是鼓勵搶樓,我也玩過這個,只不過真沒什麼意思,蠻浪費時間的,就不玩了,現在已經退役。。。
那個以前活躍過的retrospect2019就是我
但是我還是搶不過他們兩個,因爲家裏網實在渣。有人說過,現在的黑客已經沒有過去的那種分享精神了
好,那我今天就分享一回雖然我並算不上一個黑客,只是一個菜鳥。
接下來的篇章裏,我會試着把這種搶樓機器人的實現原理儘可能地闡述清楚。不知道市民和手速王是不是用的這種方法,不過應該大致原理也差不多。
本篇科普會分爲兩個部分,第一個部分闡述大致原理,不會編程的也能大概明白;第二個部分上實際代碼,需要大致的c/c++基礎,TCP/IP協議,http協議,以及操作系統編程知識(如多線程互斥)才能明白。
注:C/C++並不是腳本,個人原因不喜歡用腳本,順便也告訴大家不要把腳本和機器人掛上等號
PS:本人是菜鳥,編程習慣有些地方可能不規範,大神請輕噴,指出交流即可。
首先在實現這個機器人之前,我們要先想一個問題,我們要讓它幹什麼?或者說,我們搶樓的時候,我們乾的本質上是什麼?
我們搶樓時,做的無非是以下:
1.不斷地刷新“番劇”頁面,看最新一集更新沒有
2.如果更新,對這一集進行評論
用算法流程圖表示,就是這樣:
首先,我們要對“發送評論”這個動作進行實現,瞭解HTTP的都知道,評論的發送,實際上就是一個POST請求。我們這邊直接抓一下b站的包:
嗯,b站就是這麼耿直,赤裸裸地明文發送數據。那麼接下來就很簡單了,C/C++的話,socket,connect,send,搞定!
下面直接上代碼:
<span style="font-size:14px;"><em>作者:2019
鏈接:https://www.zhihu.com/question/50257440/answer/123993369
來源:知乎</em>
#define BILIBILI_IP "114.80.223.172"
class CommentSender//顧名思義,用於發送評論的類
{
public:
CommentSender();
~CommentSender();
int SetInfo(char * szMsg, char * szCookie, char * szAv);//設置數據包的評論內容,cookie(用於告訴服務器,這是哪個用戶發的評論),和視頻AV號。。。
char * szHeader;//堆,用於存儲將要發送的POST數據包
private:
static char szFormatHeader[];//數據包頭模板,到時候就用它來sprintf...
//它的定義往下翻就好。。。
};
//SetInfo函數。。。
CommentSender::CommentSender()
{
szHeader = new char[2048];
memset(szHeader, 0, 2048);
}
CommentSender::~CommentSender()
{
delete[] szHeader;
}
int CommentSender::SetInfo(char * szMsg, char * szCookie, char * szAv)
//功能:根據已知msg cookie av號,sprintf動態生成請求頭
{
char * szMsgEnc = new char[4096];
CnvrtToUrlEnc(szMsg, szMsgEnc);//自己寫的函數,用於把字符串進行URL編碼,網上大把,就不講了。。。
int iRet = sprintf_s(szHeader, 2048, szFormatHeader, 39 + strlen(szAv) + strlen(szMsgEnc), szAv, szCookie, szMsgEnc, szAv);//生成POST請求數據包的字符串
delete[] szMsgEnc;
return iRet;
}
__declspec(selectany) char CommentSender::szFormatHeader[]="POST /x/reply/add HTTP/1.1\r\n"
"Host: api.bilibili.com\r\n"
"Connection: close\r\n"
"Content-Length: %d\r\n"
"Accept: application/json, text/javascript, */*; q=0.01\r\n"
"Origin: http://www.bilibili.com\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0\r\n"
"Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"
"Referer: http://www.bilibili.com/video/av%s/\r\n"
"Accept-Language: zh-CN,zh;q=0.8\r\n"
"Cookie: %s\r\n\r\n"
"jsonp=jsonp&message=%s&type=1&plat=1&oid=%s";
//用了蠻多格式化符號用來sprintf
int SendComment(char * szMsg, char * szCookie, char * szAv)
{
CommentSender * cs = new CommentSender;
cs->SetInfo(szMsg, szCookie, szAv);//根據參數生成POST請求數據包
char * szRecvBuf = new char[1024]; memset(szRecvBuf, 0, 1024);
EncapBy2019::TCP tcp(BILIBILI_IP ,80,0);//IP,端口,最後一個參數請無視
//然後這個類是我自己封裝的一個TCP類,把socket封裝進去了,原理是一樣的,這裏就不說了,TCP範例網上大把。。。
if (tcp.Connect() == 0)//connect方法中,我封裝了socket()和connect()。。。
{
delete cs;
delete[] szRecvBuf;
return 0;
}
tcp.send(cs->szHeader, strlen(cs->szHeader));//這步就是關鍵了,發送
memset(szRecvBuf, 0, 1024);
int iRet = tcp.recv(szRecvBuf, 1024,5);//接收返回報文
tcp.DisConnect();
delete cs;
delete[] szRecvBuf;
return iRet;//返回接收到的長度
//當然還要判斷發回來的JSON,比方說有沒有驗證碼,發送是否成功,這裏爲了簡化,就不判斷了
}</span>
這個“刷新番劇頁面”,本質上就是一個GET的請求,然後收到這個GET請求的返回數據包之後,對其進行分析,如果最新一集的AV號改變了,那麼就對這個AV好進行sendcomment函數的調用,即發送評論。
我們來直接看代碼:
#define PAGECOMPRSIZE (1024*16)
#define PAGESIZE (1024*100)
#define BILIBILI_IP "114.80.223.172"
class AnimeFromList//這個類就是用來刷新番劇頁面的。。。
{
public:
AnimeFromList();
~AnimeFromList();
bool LoadPage(char * szAnimeID,unsigned short sVpnPort);
//load page 顧名思義,利用TCP,刷新頁面
//這邊我還考慮到了VPN使用的可能性,但是這不是我要講的重點,所以vpn port這個參數,就設爲0好了
//AnimeID指的是番劇號,大家可以在b站留心一下就能發現每個番劇都有一個ID...
char* FindLastAv(char* szAvDes);
//顧名思義,找到最新的AV號,塞到szAvDes中
private:
char * szPageRaw;//原始數據包
char * szPage;//真正的html腳本
char * szPageCompr;//把chunked拼接後的數據包
static char szHeaderFormat[];//POST請求頭的模板
static char szHeaderFormatVpn[];//VPN用的,這裏無視掉吧。。。
char * szHeader ;//用來放GET的請求。。。
};
AnimeFromList::AnimeFromList()
{
szPageRaw = new char[PAGECOMPRSIZE];
memset(szPageRaw, 0, PAGECOMPRSIZE);
szPage = new char[PAGESIZE];
memset(szPage, 0, PAGESIZE);
szPageCompr = new char[PAGECOMPRSIZE];
memset(szPageCompr, 0, PAGECOMPRSIZE);
szHeader = new char[1024];
memset(szHeader, 0, 1024);
//各種初始化,不用說了吧
}
AnimeFromList::~AnimeFromList()
{
delete[] szPage;
delete[] szPageCompr;
delete[] szPageRaw;
delete[] szHeader;
}
bool AnimeFromList::LoadPage(char * szAnimeID,unsigned short sVpnPort)
{
int iRet = 0; int iRec = 0;
if (!sVpnPort)//我們這邊只看這裏面就OK了,else裏面的是VPN的代碼,無視掉即可
{
sprintf_s(szHeader, 1024, this->szHeaderFormat, szAnimeID);
//用szHeaderFormat,設置GET請求
EncapBy2019::TCP tcp(BILIBILI_IP, 80, nullptr);
if (!tcp.Connect())
{
printf("connect() failed");
return false;
}
tcp.send(szHeader, strlen(szHeader));
memset(szPageRaw, 0, PAGECOMPRSIZE);
while (1)
{
iRet = tcp.recv(szPageRaw + iRec, PAGECOMPRSIZE - iRec, 5);
if (iRet == 0)break;
iRec += iRet;
}
tcp.DisConnect();
//然後就發送,while然後recv是爲了接收分段的chunk數據包
//recv方法最後一個參數是超時時間,用的是select模型,這裏就不詳細講了
if (iRec == 0)
{
printf("獲取AnimeID時Recv失敗\n");
return false;
}
}
else//這個代碼塊無視掉吧
{
sprintf_s(szHeader, 1024, this->szHeaderFormatVpn, szAnimeID);
EncapBy2019::TCP tcp("127.0.0.1", sVpnPort, nullptr);
if (!tcp.Connect())
{
printf("connect() failed");
return false;
}
tcp.send(szHeader, strlen(szHeader));
memset(szPageRaw, 0, PAGECOMPRSIZE);
while (1)
{
iRet = tcp.recv(szPageRaw + iRec, PAGECOMPRSIZE - iRec, 5);
if (iRet == 0)break;
iRec += iRet;
}
tcp.DisConnect();
if (iRec == 0)
{
printf("獲取AnimeID時Recv失敗\n");
return false;
}
}
memset(szPage, 0, PAGESIZE);
memset(szPageCompr, 0, PAGECOMPRSIZE);
int iLen = ConvertChunkedToNormal(strstr(szPageRaw, "\r\n\r\n") + 4, szPageCompr, PAGECOMPRSIZE);
ungzip(szPageCompr, iLen, szPage);
//這兩個函數,一個用來拼接chunk,一個用來解壓,怎麼實現的就不多說了,百度一堆。。。
//如果大家嫌麻煩可以在請求頭裏把壓縮的選項(Accept-Encoding: gzip\r\n)刪掉,不過這樣大大降低傳輸速度。。。
return true;
}
char* AnimeFromList::FindLastAv(char* szAv)
{
char* i = strstr(szPage, "playnow: \'");
//這個strstr是找最新一集的AV號,我看過b站番劇頁面的html腳本,裏面有個JavaScript,然後裏面很幸運地有最新一集的AV號。。。我這邊直接拿strstr匹配了。。。就沒用正則
/*
以下是javascript
<script type="text/javascript">
BangumiModules.init({
seasonId: 5017,
shareData: {
url: location.href,
title: '#bilibili#分享 番劇 “食戟之靈 貳之皿”',
desc: '#bilibili#分享 番劇 “食戟之靈 貳之皿”'
},
playnow: '6398082',
finished: 1,
copyright: false,
pubTime: '2016-07-02 22:00:00',
newestEp: '13'
});
</script>
*/
if (!i)
{
return 0;
}//沒找到,就返回0
char* p = i + 10;//10便是"playnow: \'"的長度、、、
for (i+=10; *i >= '0'&&*i <= '9'; i++){}//執行完這個之後,i便指向數字後面的一個字符,請大家自行領會
*i = 0;//把它設爲0
printf("線程%u找到Av:%s\n", GetCurrentThreadId(), p );
//輸出一下,之所以要輸出線程ID是因爲待會要用多線程。。。
strcpy_s(szAv, 32, p);//這個32其實填的有點不好。。。應該傳參進來的。。。不過無所謂了。。。
return p;
//執行完這個函數之後呢,傳進來的指針所指向的buf就被填充成了最新一集的AV號
}
__declspec(selectany) char AnimeFromList::szHeaderFormat[] = "GET /anime/%s HTTP/1.1\r\n"
"Host: bangumi.bilibili.com\r\n"
"Connection: close\r\n"
"Cache-Control: max-age=0\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0\r\n"
"Accept-Encoding: gzip\r\n"
"Accept-Language: zh-CN,zh;q=0.8\r\n\r\n"
;
__declspec(selectany) char AnimeFromList::szHeaderFormatVpn[]="GET http://bangumi.bilibili.com/anime/%s HTTP/1.1\r\n"
"Host: bangumi.bilibili.com\r\n"
"Proxy-Connection: close\r\n"
"Cache-Control: max-age=0\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0\r\n"
"Accept-Encoding: gzip\r\n"
"Accept-Language: zh-CN,zh;q=0.8\r\n\r\n"
;
先來大概講下我這個多線程的思路吧,首先我不停地創建線程,然後每個線程做以下幾件事情:
1.調用LoadPage加載頁面
2.調用FindLastAv找到最後一集的Av號
3.把這個Av號作爲一個字符串(string類),push到STL的一個隊列裏面
PS:其實這裏不停地創建線程可以優化成每個線程死循環做同樣的事情,這裏我懶得優化了,反正也不難
然後呢,main函數通過調用某個方法,實現從隊列中取出一個字符串,然後在將這個字符串與上一個AV號進行比較,如果AV號改變了(即更新了),就發送一個評論(調用SendComment)。
class MultThrdForAnimeList
{
public:
MultThrdForAnimeList();
~MultThrdForAnimeList();
int StartThread(string * AnimeID);//這個函數用來做初始化。。。
bool GetString(char*);//從對列中pop出一個字符串,主函數到時候就調用這個
bool IfEmpty();//判斷隊列是否爲空,用於當隊列爲空時進行阻塞
private:
CRITICAL_SECTION cs;//用於做queue的互斥
string strID;
queue<string> lQueue;//這邊用了STL的queue
int iNumOfThr;
CRITICAL_SECTION cs4Num;//用於做線程數的互斥
static unsigned int _stdcall SendThread(void*);
static unsigned int _stdcall CreateSendThread(void*);
};
MultThrdForAnimeList::MultThrdForAnimeList()
{
InitializeCriticalSection(&cs);
InitializeCriticalSection(&cs4Num);
iNumOfThr = 0;
}
MultThrdForAnimeList::~MultThrdForAnimeList()
{
DeleteCriticalSection(&cs);
DeleteCriticalSection(&cs4Num);
}
int MultThrdForAnimeList::StartThread(string * AnimeID)
{
strID = *AnimeID;
return _beginthreadex(0, 0, MultThrdForAnimeList::CreateSendThread, this, 0, 0);
}
bool MultThrdForAnimeList::GetString(char* szEpsID)
{
while (IfEmpty()){}//阻塞,直到隊列不爲空
EnterCriticalSection(&cs);
if (!lQueue.empty())
{
string str;
str = lQueue.front();
lQueue.pop();
strcpy_s(szEpsID, 32, str.c_str());
LeaveCriticalSection(&cs);
return true;
}
else
{
LeaveCriticalSection(&cs);
return false;
}
}
bool MultThrdForAnimeList::IfEmpty()
{
EnterCriticalSection(&cs);
bool ret = lQueue.empty();
LeaveCriticalSection(&cs);
return ret;
}
unsigned int _stdcall MultThrdForAnimeList::SendThread(void* pThis)
//這個線程函數調用LoadPage和FindLastAv,然後把AV號塞到隊列裏去
{
char szAv[32]; string str;
MultThrdForAnimeList* This = (MultThrdForAnimeList*)pThis;
AnimeFromList a;
if (!a.LoadPage((char*)This->strID.c_str(),0))
{
EnterCriticalSection(&This->cs4Num);
This->iNumOfThr--;
LeaveCriticalSection(&This->cs4Num);
return 0;
}
if (!a.FindLastAv(szAv))
{
EnterCriticalSection(&This->cs4Num);
This->iNumOfThr--;
LeaveCriticalSection(&This->cs4Num);
return 0;
}
str = szAv;
EnterCriticalSection(&This->cs);
This->lQueue.push(str);
LeaveCriticalSection(&This->cs);
EnterCriticalSection(&This->cs4Num);
This->iNumOfThr--;
LeaveCriticalSection(&This->cs4Num);
return str.length();
}
unsigned int _stdcall MultThrdForAnimeList::CreateSendThread(void* pThis)
{
while (1)
{
if (((MultThrdForAnimeList*)(pThis))->iNumOfThr <= 10)//這個10可以根據你自己的網速改,網速越快,可以調得越高,我家50ping,大概就10
//這個小於等於10就是用來抑制線程數的
{
_beginthreadex(0, 0, MultThrdForAnimeList::SendThread, pThis, 0, 0);
EnterCriticalSection(&((MultThrdForAnimeList*)(pThis))->cs4Num);
((MultThrdForAnimeList*)(pThis))->iNumOfThr++;
LeaveCriticalSection(&((MultThrdForAnimeList*)(pThis))->cs4Num);
}
Sleep(50);//網速越快,這個可以調得越小
}
}
//最後就是主函數了
int main()
{
WSADATA wsa;
string str;
if (WSAStartup(MAKEWORD(2, 2), &wsa))
{
printf("網絡環境初始化失敗\n");
}
char* szCookie = new char[2048]; memset(szCookie, 0, 2048);
char* szLastAv = new char[32]; memset(szLastAv, 0, 32);
char* szMsg = new char[4096]; memset(szMsg, 0, 4096);
char * szID = new char[32]; memset(szID, 0, 32);
GetCookieFromFile(szCookie, 2048);
GetMsgFromFile(szMsg, 4096);
GetIDFromFile(szID, 32);
str = szID;
mtl.StartThread(&str);
GetLastAvFromFile(szLastAv, 32);
//這幾個GetXXXFromFile基本上就是從文件中獲取信息,就是FILE的操作之類的,這裏就不詳細講了
str = szLastAv;
lOld.push_back(str);
while (1)
{
*szLastAv = 0;
mtl.GetString(szLastAv);//從隊列中取一個字符串出來,這裏看不到隊列,因爲我已經把它封裝起來了
printf("最新視頻AV爲:%s\n", szLastAv);
str = szLastAv;
if (find(lOld.begin(), lOld.end(), str) == lOld.end())
{
while (!SendComment(szCookie, szLastAv, szMsg));
lOld.push_back(str);
}
}
delete[] szID;
delete[] szCookie;
delete[] szLastAv;
delete[] szMsg;
WSACleanup();
return 0;
}
//基本上就這麼多,個人認爲講的蠻清楚了,基礎知識比方說HTTP協議啊我就不講了,百度一下吧,不難