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

引言

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

登陸過程分析

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

       觀察看看,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方式請求數據。所以在摸索過程遇到很多困難,接下來以代碼的形式講解組包發送的過程:windows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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。瀏覽器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int loginServer()
{
     CURL* curl = NULL;
     CURLcode res = CURLE_FAILED_INIT;
     const char * filename = "out.txt" ;
     <strong><span style= "color: #3366ff;" > struct curl_slist *headers = NULL;</span></strong>
     FILE * outfile;
     static const char * post_params = "__EVENTTARGET=& amp;__EVENTARGUMENT=&__VIEWSTATE=(前面的內容)&__EVENTVALIDATION=(前面的內 容)&tbUserName=name&tbPassword=name&btnLogin=%E7%99%BB++%E5%BD%95&txtReturnUrl=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文件,而從服務器返回的數據內容以下:cookie

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void downloadPage()
{
     CURLcode res = CURLE_FAILED_INIT;
     CURL* curl = NULL;
     FILE * homepage;
     <span style= "color: #3366ff;" > struct curl_slist *headers = NULL;</span>
     static const char * post_url = <strong><span style= "color: #ff0000;" > "http://www.cnblogs.com/aggsite/mydigged" </span></strong>;    // 我推薦過的博文
     static const char * refer = <strong><span style= "color: #ff0000;" > "Referer: http://www.cnblogs.com/login.aspx?ReturnUrl=http%3A%2F%2Fwww.cnblogs.com%2F" </span></strong>;
 
     if (loginServer())
     {
         curl_global_init(CURL_GLOBAL_ALL);
         curl = curl_easy_init();
         createSession(curl, 0, "" , post_url, <strong><span style= "color: #ff0000;" > "Host:www.cnblogs.com" </span></strong>, 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直接編譯的,沒有錯誤產生。這個庫的文檔基本沒有,網上只有少數的幾個例子。下面以實例講解下該庫的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
using namespace htmlcxx;<br>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樹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int count;
string temp;
for (; domBeg != domEnd; ++domBeg)   // 遍歷文檔中全部的元素
{
     if (!domBeg->tagName().compare( "div" ))    // 查找全部div標籤
     {
         domBeg->parseAttributes();   <span style= "color: #000000;" > // 這個函數很重要。若是不調用,咱們沒法獲取標籤的屬性。而下面咱們正須要獲取div的class屬性,因此必須調用。</span>
         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數據的問題。若是是手動寫死在代碼中,在推廣使用的時候顯然是不行的。還得從頁面中自動提取才行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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++-->代碼生成-->運行庫,與目標工程保持一致

小結

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

相關文章
相關標籤/搜索