用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协议啊我就不讲了,百度一下吧,不难

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值