用c++寫bilibili番劇搶樓程序

用c++寫bilibili番劇搶樓程序

轉自知乎,原作者:2019

說明:轉載該文章只爲學習而用,如果侵權請留言,我會盡快刪除

作爲一個已經退役的搶樓玩家,我不想對三好和手速王評論什麼。他們用腳本搶樓,那是他們自己的事。不過我在此實名反對所有“腳本搶樓不公平”的回答。沒什麼不公平的,他們能寫腳本,沒說你不能用腳本,完全公平,前提是你要會寫。然而你自己不會,別人會,別人搶到樓,然後你說這不公平,這不是跟“我高中沒別人努力高考成績沒別人好我去的大學沒他好,這不公平”是一個概念麼。。。
當然我不是鼓勵搶樓,我也玩過這個,只不過真沒什麼意思,蠻浪費時間的,就不玩了,現在已經退役。。。

那個以前活躍過的retrospect2019就是我


但是我還是搶不過他們兩個,因爲家裏網實在渣。有人說過,現在的黑客已經沒有過去的那種分享精神了
好,那我今天就分享一回雖然我並算不上一個黑客,只是一個菜鳥。

接下來的篇章裏,我會試着把這種搶樓機器人的實現原理儘可能地闡述清楚。不知道市民和手速王是不是用的這種方法,不過應該大致原理也差不多。

本篇科普會分爲兩個部分,第一個部分闡述大致原理,不會編程的也能大概明白;第二個部分上實際代碼,需要大致的c/c++基礎,TCP/IP協議,http協議,以及操作系統編程知識(如多線程互斥)才能明白。
注:C/C++並不是腳本,個人原因不喜歡用腳本,順便也告訴大家不要把腳本和機器人掛上等號

PS:本人是菜鳥,編程習慣有些地方可能不規範,大神請輕噴,指出交流即可。

首先在實現這個機器人之前,我們要先想一個問題,我們要讓它幹什麼?或者說,我們搶樓的時候,我們乾的本質上是什麼?

我們搶樓時,做的無非是以下:
1.不斷地刷新“番劇”頁面,看最新一集更新沒有
2.如果更新,對這一集進行評論

用算法流程圖表示,就是這樣:

好了,大致就是這樣。接下來就要上代碼了,對編程沒有了解的可以大概地瀏覽一下看我裝個13,或者覺得我sb的可以直接alt+F4。。。
首先,我們要對“發送評論”這個動作進行實現,瞭解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>

最後這一切都被封裝到sendcomment這個函數裏面了,我只要調用就發送OK了。接下來,我們就要對“刷新番劇頁面”進行代碼實現了。
這個“刷新番劇頁面”,本質上就是一個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"
;
這個類其實已經可以直接用了,先LoadPage,然後FindLastAv,最後判斷,如果Av不一樣了,那就發送評論。但是呢,天下搶樓,唯快不破,這邊我要用多線程,不然這個太慢了。以下就是多線程的實現:

先來大概講下我這個多線程的思路吧,首先我不停地創建線程,然後每個線程做以下幾件事情:
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協議啊我就不講了,百度一下吧,不難

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章