用Qt寫軟件系列六:博客園客戶端的設計與實現(1)


引言

        博客園是本人每日必逛的一個IT社區。盡管博文以.net技術居多,但是相對於CSDN這種業務雜亂、體系龐大的平台,博客園的純粹更得我青睞。之前在園子里也見過不少講解為博客園編寫客戶端的博文。不過似乎都是移動端的技術為主。這篇博文開始講講如何在PC端編寫一個博客園客戶端程序。一方面是因為本人對於博客園的感情;另一方面也想用Qt寫點什么東西出來。畢竟在實踐中學習收效更快。

登錄過程分析

        登錄功能是一個客戶端程序比不可少的功能。在組裝Http數據包發送請求之前,我們得看看整個登錄是怎樣一個過程。Fiddler Web Debugger是一個非常不錯的捕捉http數據包的工具。我們就用它來抓取登錄時的幾個數據包,看看都發送些什么內容:

       觀察看看,POST請求的地址為http://passport.cnblogs.com/login.aspx?ReturnUrl=http%3a%2f%2fwww.cnblogs.com%2f,所有的請求數據都將發往login.aspx這個頁面。Referer字段是指從哪個頁面跳向這個頁面的,一般用於反盜鏈。我們模擬Http請求的時候,把它原樣復制進去就是。User-Agent則表明使用的瀏覽器內核版本信息,這里我用的是IE9。在模擬的時候也招辦不誤。剩余字段中最重要的是Host和Accept-Encoding兩個字段。其中Accept-Encoding表明客戶瀏覽器能接受什么格式的數據,gzip表示瀏覽器可接受壓縮格式的數據。這在編寫客戶端的時候需要注意了,因為瀏覽器可以對gzip格式數據解碼,除非自己實現解碼功能,否則我們的客戶端還是用deflate格式。這里的Cookie不知道是干什么用的,不過在登錄之前我想對用戶作用不大。

       這里用的是POST請求方式,報文數據部分才是登錄時最需要的數據。Fiddler的功能真是強大,看看下圖就知道了:

       可以看到,POST發送的數據總共有8對。其中__EVENTTARGET和__EVENTARGUMENT字段目前是空的,__VIEWSTATE和__EVENTVALIDATION則是兩個很長的字符串,具體作用不知道,但是這不影響我們。在驗證的時候我們手動組裝即可,自動登錄的時候從頁面中過濾出來即可。后面將利用htmlcxx這個工具完成。剩下四個字段中只有用戶名和密碼是變化的,其他兩個字段固定不變,拼接到末尾即可。也就是說,我們需要自己組裝http報文頭部和數據部分。這個工作利用Libcurl這個庫來完成。

模擬HTTP請求

       那么接下來的工作就是組裝Http數據包了。libcurl是完成這項工作的有力工具,關於這個工具的使用網上的頁面挺多,但是正式用在模擬登陸中的少見。這篇博文倒是講解了利用libcurl登陸csdn的原理。然而區別的是,該博文中並未講解如何使用POST方式請求數據。因此在摸索過程遇到不少困難,接下來以代碼的形式講解組包發送的過程:

void createSession(CURL* curl, int postoff, const char* post_params, const char* post_url, const char* hosts, const char* refer, struct curl_slist *headers)
{
    if(curl){
        headers = curl_slist_append(headers,"User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)"); 
        headers = curl_slist_append(headers, hosts);
        headers = curl_slist_append(headers,"Accept: text/html, application/xhtml+xml, */*");
        headers = curl_slist_append(headers,"Accept-Language:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3");
        headers = curl_slist_append(headers,"Accept-Encoding:deflate");
        headers = curl_slist_append(headers, refer);
        headers = curl_slist_append(headers,"Connection:keep-alive");
        
        curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookie.txt");        //把服務器發過來的cookie保存到cookie.txt
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
	curl_easy_setopt(curl, CURLOPT_URL, post_url);  
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_params);        // 使用POST方式發送請求數據

	curl_easy_setopt(curl, CURLOPT_POST, postoff);  
	curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);  
        curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookie.txt");       // cookies文件
    }

}

  在調用該函數先需要先初始化libcurl的上下文環境,並將初始化得到的CURL*指針傳遞進來。注意headers是一個struct curl_slist*類型的指針,在使用之前需要先清空。這里需要注意的是:每一次發送請求數據之前,我們都要清空這個headers所指向的結構體,否則會服務器會返回400錯誤!在上面的函數中,我們初始化了headers結構體。這個結構體存儲的都是數據包頭部相關的字段,前面抓取到的字段全部往這里面塞就行了。curl_easy_setopt()函數是libcurl中非常重要的函數,其功能類似於fnctl和ioctl這樣的系統調用,主要用於控制libcurl的行為。這里需要需要注意的是CURLOPT_POSTFIELDS這個屬性,它用於控制當前的請求方式是否使用POST。

int loginServer()
{
	CURL* curl = NULL;
	CURLcode res = CURLE_FAILED_INIT;
	const char* filename = "out.txt";
	struct curl_slist *headers = NULL;
	FILE* outfile;
	static const char* post_params = "__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=(前面的內容)&__EVENTVALIDATION=(前面的內容)&tbUserName=name&tbPassword=name&btnLogin=%E7%99%BB++%E5%BD%95&txtReturnUrl=http%3A%2F%2Fwww.cnblogs.com%2F";
	static const char* post_url = "http://passport.cnblogs.com/login.aspx?ReturnUrl=http%3a%2f%2fwww.cnblogs.com%2f";
	static const char* refer = "Referer: http://passport.cnblogs.com/login.aspx?ReturnUrl=http%3A%2F%2Fwww.cnblogs.com%2F";

	curl_global_init(CURL_GLOBAL_ALL);
       curl = curl_easy_init();

	createSession(curl, 1, post_params, post_url, "Host:passport.cnblogs.com", refer, headers);
	outfile = fopen(filename, "w");
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);   // 注冊回調函數,當數據到來的時候自動調用這個函數存儲數據
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, outfile);              // 和回調函數一起設置,表示數據存儲的地方
      //執行http請求
      res = curl_easy_perform(curl);     // 發送數據、接受數據等工作,我們不需插手

      //釋放資源
      curl_easy_cleanup(curl);
      curl_slist_free_all(headers);
     curl_global_cleanup();
	fclose(outfile);

	return res == CURLE_OK;		
}

  接着便是登錄了。我們首先手動組裝了需要發送的數據部分,這個地方也需要注意:如果是直接從網頁中提取出來的話,需要進行編碼將' ', '/', '+'等字符編碼替換。這里是手動的直接粘貼即可。然后就初始化libcurl的使用環境,設置回調函數保存數據。curl_easy_perform()在后台完成了所有的工作,數據的首發、cookies文件的發送保存工作都不要程序員插手。所以整個代碼看起來非常簡單。

      調用完成后將在工程目錄下可以看到下載到的頁面源代碼。如果登錄成功,還可以在工程目錄下可到生成的cookies文件,而從服務器返回的數據內容如下:

      接下來我們就可以開始訪問我們賬戶的數據了,如我評論過的博文、我推薦過的博文、我關注的人!那么,我們還得先把頁面代碼下載下來:

void downloadPage()
{
	CURLcode res = CURLE_FAILED_INIT;
	CURL* curl = NULL;
	FILE* homepage;
	struct curl_slist *headers = NULL;
	static const char* post_url = "http://www.cnblogs.com/aggsite/mydigged";    // 我推薦過的博文
	static const char* refer = "Referer: http://www.cnblogs.com/login.aspx?ReturnUrl=http%3A%2F%2Fwww.cnblogs.com%2F";

	if (loginServer())
	{
		curl_global_init(CURL_GLOBAL_ALL);
		curl = curl_easy_init();
		createSession(curl, 0, "", post_url, "Host:www.cnblogs.com", refer, headers);
		homepage = fopen("homepage.txt", "w");
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);  
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, homepage); 
		//執行http請求
		res = curl_easy_perform(curl);

		//釋放資源
		curl_easy_cleanup(curl);
		curl_slist_free_all(headers);
		curl_global_cleanup();
		fclose(homepage);
	}
}

  請求URL設置為http://www.cnblogs.com/aggsite/mydigged,表示我推薦過的博文頁面。而Referer和host字段則根據fiddler抓取結果進行填充。注意這里的headers又進行了一次初始化哦。其他的仍然保持不變。要是沒有什么大問題,這個頁面的源代碼已經下載完成了。那么接下來的工作就是解析頁面內容了。

解析頁面內容

      解析HTML這種結構性文本用字符串查找的方式或正則表達式看似都行,但是工作量實在太大,准確性還很難說。在網上找到一個專用於解析html代碼的C++庫:htmlcxx。這個庫是C++編寫的,目前似乎已經停止更新了,最新的版本下載到的是0.84。這個庫下載下來的是源代碼,需要進行編譯生成lib使用。在windows環境下我使用vs2010直接編譯的,沒有錯誤產生。這個庫的文檔基本沒有,網上只有少數的幾個例子。下面以實例講解下該庫的使用方式:

using namespace htmlcxx;
fstream out; out.open("out.txt", ios::out); // 所有的解析結果全部保存在out.txt文件中 fstream htmlFileStream; htmlFileStream.open( "test.txt", ios::in ); // text.txt中保存的是上文中下載的頁面源代碼 istreambuf_iterator<char> fileBeg(htmlFileStream), fileEnd; string html( fileBeg, fileEnd ); htmlFileStream.close(); HTML::ParserDom parser; tree<HTML::Node> dom = parser.parseTree(html); tree<HTML::Node>::iterator domBeg = dom.begin(); tree<HTML::Node>::iterator domEnd = dom.end();

    先引入命名空間初始化解析器,並從中獲取到兩個迭代器。該庫允許我們以迭代器的方式來遍歷其構造的DOM樹:

int count;
string temp;
for (; domBeg != domEnd; ++domBeg)   // 遍歷文檔中所有的元素
{
	if (!domBeg->tagName().compare("div"))    // 查找所有div標簽
	{
		domBeg->parseAttributes();    // 這個函數很重要。如果不調用,我們無法獲取標簽的屬性。而下面我們正需要獲取div的class屬性,所以必須調用。
		if (!domBeg->attribute("class").second.compare("post_item"))   // 如果是class屬性值為post_item,表明是一個博文結構,開始解析
		{
			count = 0;  // count計數,每條博文只解析7個字段,主要是為了跳出循環。沒有找到更好的跳出循環的方法
			out << "-----------------------------------------------" << endl;
			for (; domBeg != domEnd; ++domBeg)
			{
				if (!domBeg->tagName().compare("a"))  // 如果是a標簽,則將a標簽的href屬性值提取出來保存到文件
				{
					domBeg->parseAttributes();
					out << domBeg->attribute("href").second << endl;
				}
				if (!domBeg->isTag())   // 如果不是html標簽而是普通文本,那么就要進行空格處理
				{
					temp = domBeg->text();  // 先將該文本提出取出來
					temp.erase(0,temp.find_first_not_of(" \t\v\r\n"));  // 去掉' ', '\t', '\v', '\n', '\r'
					temp.erase(temp.find_last_not_of(" \t\v\r\n") + 1);
					if (!temp.empty())  // 如果剔除了空格字符之后還剩下其他字符,則保存到文件
					{
						out << temp << endl;
						++count;
					}
				}
				if (count == 7)   // 已經找到7個字段,跳出循環,繼續下一條博文的解析
				{
					break;
				}
			}
		}
	}

}

    上面的注釋已經非常清楚了,htmlcxx這個庫的使用也非常簡單,提供的API只有七八個。看看都輸出了些什么:

       結果還不錯,代碼量卻很少。還真的是挺強大的,算法的力量!要是光靠字符串匹配還正不知道有沒有勇氣去做。另外,前面還提到了在登錄時需要組裝POST數據的問題。如果是手動寫死在代碼中,在推廣使用的時候顯然是不行的。還得從頁面中自動提取才行:

int count = 0;
for (; domBeg != domEnd; ++domBeg)
{
	if (!domBeg->tagName().compare("input"))   // 只檢查input標簽,因為那幾個字段都是在input里面
	{
		domBeg->parseAttributes();
		out << "name: " << domBeg->attribute("name").second ;  // 提取鍵名,即input的name屬性
		out << " value:" << domBeg->attribute("value").second << endl;  // 提取鍵值,即input的value屬性
		if (++count == 4)  // 只要四個字段,提前結束解析工作。
		{
			break;
		}
	}
}

  再看看提取結果:

      規規矩矩、整整齊齊。好了,htmlcxx的演示到這里結束了。

遇到的問題

  1. htmlcxx在解析中文的時候,可能會出現問題,需要進行調整。網上的代碼很多。據說是htmlcxx的一個Bug。
  2. libcurl使用POST的方式。CURLOPT_POSTFIELDS字段。
  3. htmlcxx的編譯方式,需要保證編譯方式和目標工程方式一直,否則無法和其他庫一起配合使用。解決方案:項目屬性-->C/C++-->代碼生成-->運行庫,與目標工程保持一致

小結

      登錄及頁面解析工作基本告一段落,下一階段就是界面整合。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM