引言
博客園是本人每日必逛的一個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的演示到這里結束了。
遇到的問題
- htmlcxx在解析中文的時候,可能會出現問題,需要進行調整。網上的代碼很多。據說是htmlcxx的一個Bug。
- libcurl使用POST的方式。CURLOPT_POSTFIELDS字段。
- htmlcxx的編譯方式,需要保證編譯方式和目標工程方式一直,否則無法和其他庫一起配合使用。解決方案:項目屬性-->C/C++-->代碼生成-->運行庫,與目標工程保持一致
小結
登錄及頁面解析工作基本告一段落,下一階段就是界面整合。