1. 程式人生 > >手把手教你用C++ 寫ACM自動刷題神器(衝入HDU首頁)

手把手教你用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 

//向伺服器傳送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);
	}
(二)藉助搜尋引擎獲取csdn部落格連結

一開始的時候我想利用百度搜索引擎,但是發現返回到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請求向伺服器提交程式碼

我們可以看到上面的程式碼並不是解析出來就能用的,還包含有&lt,&gt等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!

有疑問或者優化請留言或者 通過郵箱聯絡我,聯絡方式在部落格的左上角,希望給大家學習網路程式設計帶來幫助!