用c++写bilibili番剧抢楼程序
说明:转载该文章只为学习而用,如果侵权请留言,我会尽快删除
作为一个已经退役的抢楼玩家,我不想对三好和手速王评论什么。他们用脚本抢楼,那是他们自己的事。不过我在此实名反对所有“脚本抢楼不公平”的回答。没什么不公平的,他们能写脚本,没说你不能用脚本,完全公平,前提是你要会写。然而你自己不会,别人会,别人抢到楼,然后你说这不公平,这不是跟“我高中没别人努力高考成绩没别人好我去的大学没他好,这不公平”是一个概念么。。。
当然我不是鼓励抢楼,我也玩过这个,只不过真没什么意思,蛮浪费时间的,就不玩了,现在已经退役。。。
那个以前活跃过的retrospect2019就是我
但是我还是抢不过他们两个,因为家里网实在渣。有人说过,现在的黑客已经没有过去的那种分享精神了
好,那我今天就分享一回虽然我并算不上一个黑客,只是一个菜鸟。
接下来的篇章里,我会试着把这种抢楼机器人的实现原理尽可能地阐述清楚。不知道市民和手速王是不是用的这种方法,不过应该大致原理也差不多。
本篇科普会分为两个部分,第一个部分阐述大致原理,不会编程的也能大概明白;第二个部分上实际代码,需要大致的c/c++基础,TCP/IP协议,http协议,以及操作系统编程知识(如多线程互斥)才能明白。
注:C/C++并不是脚本,个人原因不喜欢用脚本,顺便也告诉大家不要把脚本和机器人挂上等号
PS:本人是菜鸟,编程习惯有些地方可能不规范,大神请轻喷,指出交流即可。
首先在实现这个机器人之前,我们要先想一个问题,我们要让它干什么?或者说,我们抢楼的时候,我们干的本质上是什么?
我们抢楼时,做的无非是以下:
1.不断地刷新“番剧”页面,看最新一集更新没有
2.如果更新,对这一集进行评论
用算法流程图表示,就是这样:
当然我不是鼓励抢楼,我也玩过这个,只不过真没什么意思,蛮浪费时间的,就不玩了,现在已经退役。。。
那个以前活跃过的retrospect2019就是我
但是我还是抢不过他们两个,因为家里网实在渣。有人说过,现在的黑客已经没有过去的那种分享精神了
好,那我今天就分享一回虽然我并算不上一个黑客,只是一个菜鸟。
接下来的篇章里,我会试着把这种抢楼机器人的实现原理尽可能地阐述清楚。不知道市民和手速王是不是用的这种方法,不过应该大致原理也差不多。
本篇科普会分为两个部分,第一个部分阐述大致原理,不会编程的也能大概明白;第二个部分上实际代码,需要大致的c/c++基础,TCP/IP协议,http协议,以及操作系统编程知识(如多线程互斥)才能明白。
注:C/C++并不是脚本,个人原因不喜欢用脚本,顺便也告诉大家不要把脚本和机器人挂上等号
PS:本人是菜鸟,编程习惯有些地方可能不规范,大神请轻喷,指出交流即可。
首先在实现这个机器人之前,我们要先想一个问题,我们要让它干什么?或者说,我们抢楼的时候,我们干的本质上是什么?
我们抢楼时,做的无非是以下:
1.不断地刷新“番剧”页面,看最新一集更新没有
2.如果更新,对这一集进行评论
用算法流程图表示,就是这样:
好了,大致就是这样。接下来就要上代码了,对编程没有了解的可以大概地浏览一下看我装个13,或者觉得我sb的可以直接alt+F4。。。
首先,我们要对“发送评论”这个动作进行实现,了解HTTP的都知道,评论的发送,实际上就是一个POST请求。我们这边直接抓一下b站的包:
嗯,b站就是这么耿直,赤裸裸地明文发送数据。那么接下来就很简单了,C/C++的话,socket,connect,send,搞定!
下面直接上代码:
首先,我们要对“发送评论”这个动作进行实现,了解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函数的调用,即发送评论。
我们来直接看代码:
这个“刷新番剧页面”,本质上就是一个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)。
先来大概讲下我这个多线程的思路吧,首先我不停地创建线程,然后每个线程做以下几件事情:
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协议啊我就不讲了,百度一下吧,不难