手把手教你用C++ 寫ACM自動刷題神器(衝入HDU首頁)
少年,作為苦練ACM,通宵刷題的你 是不是想著有一天能夠榮登各大OJ榜首,俯瞰芸芸眾生,唔....要做到這件事情可是需要一定天賦的哦!
博主本身也搞過一段時間的acm,對刷題深有感觸,不信可以去看我部落格的acm題解(哈哈)。
不過,先給各位辛苦刷題的ACMer賠個不是,畢竟這是很投機的一種方式,僅供娛樂,還請各位見諒!
受學長的啟蒙,打算自己做一個使用C++語言完成的自動刷題神器,也可以叫自動AC機(什麼?ac自動機...嚇尿),先來看一下成果:
(注:這是第一次刷完後的排名,後來對程式碼進行了很大的優化,但是由於時間關係,沒有再刷一遍,否則肯定進入前十!)
第17名是我,AC率還算不錯吧,但是我優化之後的肯定比這個高!
好了,扯淡完畢,下面進入正文,先來說一下整體思路:
1)使用socket程式設計模擬HTTP協議GET請求向伺服器傳送頁面請求
2)藉助搜尋引擎找到相關題目的程式碼(一般csdn的居多)
3)使用正則表示式解析HTML程式碼獲取部落格連線,緊接著從部落格中解析出 題目的程式碼
4)對程式碼進行編碼轉換的處理,模仿HTTP協議的POST請求向伺服器提交程式碼
5)解析提交後返回的State頁面,提取最終的結果(是否Accepted)、耗時和空間佔用。
6)將刷題過程儲存至SQL Server資料庫,供以後的資料分析。
是不是感覺很簡單的樣子,讓我們一步一步來!
(一)使用socket程式設計模擬HTTP協議GET請求向伺服器傳送頁面請求
我們在baidu中搜索關鍵字,點選按鈕,伺服器會返回我們一個頁面,這件事情使用程式該如何實現呢?
答案就是我們使用Socket程式設計通過bind(),connect(),send(),recv()這些函式建立與伺服器的連線。這些知識就不再這裡展開了,讀者可以自行baidu或者參考我的Linux網路程式設計專欄。 接下來我們想:點選按鈕的過程發生了什麼,我們使用send()需要將什麼資訊傳送至伺服器,這裡就要涉及到HTTP協議的GET請求。
我們只需要實現GET的請求頭即可(可以通過chrome按F12來檢視),注意和正文之間有一個空行,即/r/n
(二)藉助搜尋引擎獲取csdn部落格連結//向伺服器傳送GET請求 string reqInfo = "GET " + (string)othPath + " HTTP/1.1\r\nHost: " + (string)host + "\r\nConnection:Close\r\n\r\n"; if (SOCKET_ERROR == send(sock, reqInfo.c_str(), reqInfo.size(), 0)) { cout << "send error! 錯誤碼: " << WSAGetLastError() << endl; closesocket(sock); }
一開始的時候我想利用百度搜索引擎,但是發現返回到HTML頁面中根本無法找出csdn部落格的連結特徵,後來發現,原來是baidu為了避免爬蟲爬取進行了加密處理,如下圖:
注意左下角是第一個csdn部落格的連線....坑了我一段時間。
後來發現360搜尋沒有加密,所以那就用360吧。。提取出來放入vector儲存,然後使用C++11的正則表示式解析出csdn部落格的地址。
void regexGetcom(string &allHtml) //提取網頁中的csdn部落格的url
{
blogUrl.clear();
smatch mat;
regex pattern("href=\"(http://blog.csdn[^\\s\"]+)\"");
string::const_iterator start = allHtml.begin();
string::const_iterator end = allHtml.end();
while (regex_search(start, end, mat, pattern))
{
string msg(mat[1].first, mat[1].second);
blogUrl.push_back(msg);
start = mat[0].second;
}
}
(三)從HTML中解析出程式碼
注意程式碼一般由#include開始,結束的位置是</textarea>或者</pre>,我們利用這個特徵進行提取。
void GetCode(string &allHtml)
{
CodeHtml = "";
int pos = allHtml.find("#include");
if (pos != string::npos)
{
for (int i = pos; i < allHtml.length(); i++)
{
if ((allHtml[i] == '<'&&allHtml[i + 1] == '/'&&allHtml[i + 2] == 't'&&allHtml[i + 3] == 'e'&&allHtml[i + 4] == 'x'&&allHtml[i + 5] == 't') || (allHtml[i] == '<'&&allHtml[i + 1] == '/'&&allHtml[i + 2] == 'p'&&allHtml[i + 3] == 'r'&&allHtml[i + 4] == 'e'&&allHtml[i + 5] == '>'))
{
return ;
}
CodeHtml += allHtml[i];
}
}
else
{
cout << "未找到合適的程式碼!" << endl;
return;
}
}
(四)對程式碼進行編碼轉換的處理,模仿HTTP協議的POST請求向伺服器提交程式碼
我們可以看到上面的程式碼並不是解析出來就能用的,還包含有<,>等HTML的元素,並且還有漢字轉碼的問題需要我們需要處理。POST的時候,還需要考慮HTTP編碼,
將空格回車等轉換為十六進位制傳送提交,不說了,直接看程式碼:
string ReplaceDiv(string &CodeHtml)
{
string ans;
for (int i = 0; i < CodeHtml.length(); i++)
{
if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == 'l'&&CodeHtml[i + 2] == 't'&&CodeHtml[i + 3] == ';')
{
ans += '<';
i += 3;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == 'g'&&CodeHtml[i + 2] == 't'&&CodeHtml[i + 3] == ';')
{
ans += '>';
i += 3;
}
else if (CodeHtml[i] == '/'&&CodeHtml[i + 1] == 'n')
{
ans += "\\n";
i +=1;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == 'a'&&CodeHtml[i + 2] == 'm'&&CodeHtml[i + 3] == 'p'&&CodeHtml[i + 4] == ';')
{
ans += '&';
i += 4;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == 'q'&&CodeHtml[i + 2] == 'u'&&CodeHtml[i + 3] == 'o'&&CodeHtml[i + 4] == 't'&&CodeHtml[i + 5] == ';')
{
ans += '\"';
i += 5;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == 'n'&&CodeHtml[i + 2] == 'b'&&CodeHtml[i + 3] == 's'&&CodeHtml[i + 4] == 'p'&&CodeHtml[i + 5] == ';')
{
ans += ' ';
i += 5;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == '#'&&CodeHtml[i + 2] == '4'&&CodeHtml[i + 3] == '3'&&CodeHtml[i + 4] == ';')
{
ans += '+';
i += 4;
}
else if (CodeHtml[i] == '&'&&CodeHtml[i + 1] == '#'&&CodeHtml[i + 2] == '3'&&CodeHtml[i + 3] == '9'&&CodeHtml[i + 4] == ';')
{
ans += '\'';
i += 4;
}
else
ans += CodeHtml[i];
}
return ans;
}
string ASCtoHex(int num) //十進位制轉換成十六進位制
{
char str[] = "0123456789ABCDEF";
int temp=num;
string ans;
while (temp)
{
ans += str[temp % 16];
temp /= 16;
}
ans += '%';
reverse(ans.begin(), ans.end());
return ans;
}
string GetRescode(string &CodeHtml)
{
ResCode = "";
for (int i = 0; i < CodeHtml.length(); i++)
{
//if (!isdigit(unsigned(CodeHtml[i])) && !isalpha(unsigned(CodeHtml[i])))
if ((CodeHtml[i] >= 0 && CodeHtml[i] < 48) || (CodeHtml[i]>57 && CodeHtml[i]<65) || (CodeHtml[i]>90 && CodeHtml[i]<97) || (CodeHtml[i]>122 & CodeHtml[i] <= 127))
{
//if (CodeHtml[i] == '\r' && (i + 1) < CodeHtml.length() && CodeHtml[i + 1] == '\n')
//if (CodeHtml[i] == '\r')
//{
// ResCode += "++%0D";
//
//}
//else if (CodeHtml[i] == '\n')
if (CodeHtml[i] == '\n')
{
ResCode += "%0D%0A";
}
else if (CodeHtml[i] == '.' || CodeHtml[i] == '-' || CodeHtml[i] == '*')
ResCode += CodeHtml[i];
/*if (CodeHtml[i] == 10)
ResCode += "%0D%0A";
*/
/*else if (CodeHtml[i] >= 0xB0 && (i + 1)<CodeHtml.length()&&CodeHtml[i + 1] >= 0xA1)//判斷漢字
{
i++;
ResCode += CodeHtml[i];
ResCode += CodeHtml[i + 1];
}*/
else
{
string cur = ASCtoHex(CodeHtml[i]);
if (cur == "%9")
ResCode += "++++";
else if (cur == "%20")
ResCode += '+';
else if (cur == "%D")
ResCode += "++";
else
ResCode += cur;
}
}
else
ResCode += CodeHtml[i];
}
return ResCode;
//UTF-8到GB2312的轉換
char* U2G(const char* utf8)
{
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
wchar_t* wstr = new wchar_t[len + 1];
memset(wstr, 0, len + 1);
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len);
len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
char* str = new char[len + 1];
memset(str, 0, len + 1);
WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL);
if (wstr) delete[] wstr;
return str;
}
POST:注意頭資訊要全,並且Cookie要寫你自己的,一旦瀏覽器關閉就會失效,要重置:
string Typee = "\r\nContent-Type: application/x-www-form-urlencoded";
string ConLen = "\r\nContent-Length: ";
_itoa(ProblemID, s, 10);
//string ElseInfo = "\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8O\r\nOrigin: http://acm.hdu.edu.cn\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36\r\nReferer: http://acm.hdu.edu.cn/submit.php?pid=1003\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.8";
string ElseInfo = "\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8O\r\nOrigin: http://acm.hdu.edu.cn\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36\r\nReferer: http://acm.hdu.edu.cn/submit.php?pid=";
ElseInfo = ElseInfo+ (string)s + "\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.8";
//向伺服器傳送POST請求
string HeaderP="check=0&problemid="+(string)s;
HeaderP += "&language=2&usercode=";
ResCode = HeaderP + ResCode;
char s[300];
_itoa( ResCode.length(), s, 10); /////??????
string Cookie = "exesubmitlang=2; PHPSESSID=8qdqoujc8ptncdb518cksqr687; CNZZDATA1254072405=385429082-1445151305-http%253A%252F%252Facm.hdu.edu.cn%252F%7C1446089001";
string reqInfo = "POST " + (string)othPath + " HTTP/1.1\r\nHost: " + (string)host + ElseInfo+ Typee + ConLen + (string)s + "\r\nCookie: " + Cookie + "\r\nConnection:Close\r\n\r\n" + ResCode;
if (SOCKET_ERROR == send(sock, reqInfo.c_str(), reqInfo.size(), 0))
{
cout << "send error! 錯誤碼: " << WSAGetLastError() << endl;
closesocket(sock);
}
(五)解析提交後返回的State頁面,提取最終的結果(是否Accepted)、耗時和空間佔用這個就比較簡單了,資料分析,可能實現都不一樣,我是先定位的題號:
void GetResult(string &allHtml,int Prob) //解析出state.php中的結果,空間,時間
{
StateAns="";
StateSapce="";
StateTime="";
char d[200];
_itoa(ProblemID, d, 10);
strcat(d, "</a>");
int pos = allHtml.find((string)d);
int Mpos = pos;
int Tpos;
if (Mpos == string::npos)
return;
else
{
Mpos += 17;
while (1)
{
if (allHtml[Mpos] == '<')
{
Tpos = Mpos;
break;
}
StateSapce += allHtml[Mpos];
Mpos++;
}
cout << "使用的空間大小為:" << StateSapce << endl;
}
Tpos += 9;
while (1)
{
if (allHtml[Tpos] == '<')
break;
StateTime += allHtml[Tpos];
Tpos++;
}
cout << "使用的時間為:" << StateTime << endl;
if (pos == string::npos)
return;
else
{
pos = pos - 52;
int begin;
while (1)
{
if (allHtml[pos] == '>')
{
begin = pos;
break;
}
pos--;
}
for (int i = begin + 1;allHtml[i]!='<';i++)
{
StateAns += allHtml[i];
}
}
cout << "最終的state介面的答案是:" << "---------------::::::" << StateAns << endl;
}
(六)將刷題過程儲存至SQL Server資料庫,供以後的資料分析
要點就是C++使用ado連線SQL Server資料庫
CoInitialize(NULL);//初始化Com庫
_ConnectionPtr pMyConnect = NULL;//這是個物件指標,關於物件指標的內容可以百度一下,不過不理解也就算了
HRESULT hr = pMyConnect.CreateInstance(__uuidof(Connection));
//將物件指標例項化
if (FAILED(hr))
{
cout << "_ConnectionPtr物件指標例項化失敗!" << endl;
return 0;
}
_bstr_t strConnect="Driver={sql server};server=Tach-PC\\SQLEXPRESS;uid=tach1;pwd=123456;database=ProblemSolved"; //SQLSERVER
//這是連線到SQL SERVER資料庫的連線字串,其中的引數要自己改
try{
pMyConnect->Open(strConnect, "", "", NULL);
}//連線到資料庫,要捕捉異常
catch (_com_error &e){
cout << "連線資料庫異常!" << endl;
cout << e.ErrorMessage() << endl;
}
注意將上面的server名字,uid和pwd改為你自己的。
下圖的Queuing請無視,因為我為了速度並沒有Sleep(),頁面還沒有顯示出結果,對,我比較懶==
好啦,到這裡就大功告成啦!別刷太快哦,貌似被hdu封了一次IP。。。。。(囧)
最後專案的完整原始碼在我的Github,歡迎大家fork!
有疑問或者優化請留言或者 通過郵箱聯絡我,聯絡方式在部落格的左上角,希望給大家學習網路程式設計帶來幫助!