用Qt写软件系列六:博客园客户端的设计与实现(1)
引言
? ? ? ? 博客園是本人每日必逛的一個IT社區(qū)。盡管博文以.net技術(shù)居多,但是相對于CSDN這種業(yè)務(wù)雜亂、體系龐大的平臺,博客園的純粹更得我青睞。之前在園子里也見過不少講解為博客園編寫客戶端的博文。不過似乎都是移動端的技術(shù)為主。這篇博文開始講講如何在PC端編寫一個博客園客戶端程序。一方面是因為本人對于博客園的感情;另一方面也想用Qt寫點什么東西出來。畢竟在實踐中學習收效更快。
登錄過程分析
? ? ? ? 登錄功能是一個客戶端程序比不可少的功能。在組裝Http數(shù)據(jù)包發(fā)送請求之前,我們得看看整個登錄是怎樣一個過程。Fiddler Web Debugger是一個非常不錯的捕捉http數(shù)據(jù)包的工具。我們就用它來抓取登錄時的幾個數(shù)據(jù)包,看看都發(fā)送些什么內(nèi)容:
? ? ? ?觀察看看,POST請求的地址為http://passport.cnblogs.com/login.aspx?ReturnUrl=http%3a%2f%2fwww.cnblogs.com%2f,所有的請求數(shù)據(jù)都將發(fā)往login.aspx這個頁面。Referer字段是指從哪個頁面跳向這個頁面的,一般用于反盜鏈。我們模擬Http請求的時候,把它原樣復(fù)制進去就是。User-Agent則表明使用的瀏覽器內(nèi)核版本信息,這里我用的是IE9。在模擬的時候也招辦不誤。剩余字段中最重要的是Host和Accept-Encoding兩個字段。其中Accept-Encoding表明客戶瀏覽器能接受什么格式的數(shù)據(jù),gzip表示瀏覽器可接受壓縮格式的數(shù)據(jù)。這在編寫客戶端的時候需要注意了,因為瀏覽器可以對gzip格式數(shù)據(jù)解碼,除非自己實現(xiàn)解碼功能,否則我們的客戶端還是用deflate格式。這里的Cookie不知道是干什么用的,不過在登錄之前我想對用戶作用不大。
? ? ? ?這里用的是POST請求方式,報文數(shù)據(jù)部分才是登錄時最需要的數(shù)據(jù)。Fiddler的功能真是強大,看看下圖就知道了:
? ? ? ?可以看到,POST發(fā)送的數(shù)據(jù)總共有8對。其中__EVENTTARGET和__EVENTARGUMENT字段目前是空的,__VIEWSTATE和__EVENTVALIDATION則是兩個很長的字符串,具體作用不知道,但是這不影響我們。在驗證的時候我們手動組裝即可,自動登錄的時候從頁面中過濾出來即可。后面將利用htmlcxx這個工具完成。剩下四個字段中只有用戶名和密碼是變化的,其他兩個字段固定不變,拼接到末尾即可。也就是說,我們需要自己組裝http報文頭部和數(shù)據(jù)部分。這個工作利用Libcurl這個庫來完成。
模擬HTTP請求
? ? ? ?那么接下來的工作就是組裝Http數(shù)據(jù)包了。libcurl是完成這項工作的有力工具,關(guān)于這個工具的使用網(wǎng)上的頁面挺多,但是正式用在模擬登陸中的少見。這篇博文倒是講解了利用libcurl登陸csdn的原理。然而區(qū)別的是,該博文中并未講解如何使用POST方式請求數(shù)據(jù)。因此在摸索過程遇到不少困難,接下來以代碼的形式講解組包發(fā)送的過程:
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"); //把服務(wù)器發(fā)過來的cookie保存到cookie.txtcurl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);curl_easy_setopt(curl, CURLOPT_URL, post_url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_params); // 使用POST方式發(fā)送請求數(shù)據(jù)curl_easy_setopt(curl, CURLOPT_POST, postoff); curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookie.txt"); // cookies文件}}在調(diào)用該函數(shù)先需要先初始化libcurl的上下文環(huán)境,并將初始化得到的CURL*指針傳遞進來。注意headers是一個struct curl_slist*類型的指針,在使用之前需要先清空。這里需要注意的是:每一次發(fā)送請求數(shù)據(jù)之前,我們都要清空這個headers所指向的結(jié)構(gòu)體,否則會服務(wù)器會返回400錯誤!在上面的函數(shù)中,我們初始化了headers結(jié)構(gòu)體。這個結(jié)構(gòu)體存儲的都是數(shù)據(jù)包頭部相關(guān)的字段,前面抓取到的字段全部往這里面塞就行了。curl_easy_setopt()函數(shù)是libcurl中非常重要的函數(shù),其功能類似于fnctl和ioctl這樣的系統(tǒng)調(diào)用,主要用于控制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=(前面的內(nèi)容)&__EVENTVALIDATION=(前面的內(nèi)容)&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); // 注冊回調(diào)函數(shù),當數(shù)據(jù)到來的時候自動調(diào)用這個函數(shù)存儲數(shù)據(jù)curl_easy_setopt(curl, CURLOPT_WRITEDATA, outfile); // 和回調(diào)函數(shù)一起設(shè)置,表示數(shù)據(jù)存儲的地方//執(zhí)行http請求res = curl_easy_perform(curl); // 發(fā)送數(shù)據(jù)、接受數(shù)據(jù)等工作,我們不需插手//釋放資源curl_easy_cleanup(curl);curl_slist_free_all(headers);curl_global_cleanup();fclose(outfile);return res == CURLE_OK; }接著便是登錄了。我們首先手動組裝了需要發(fā)送的數(shù)據(jù)部分,這個地方也需要注意:如果是直接從網(wǎng)頁中提取出來的話,需要進行編碼將' ', '/', '+'等字符編碼替換。這里是手動的直接粘貼即可。然后就初始化libcurl的使用環(huán)境,設(shè)置回調(diào)函數(shù)保存數(shù)據(jù)。curl_easy_perform()在后臺完成了所有的工作,數(shù)據(jù)的首發(fā)、cookies文件的發(fā)送保存工作都不要程序員插手。所以整個代碼看起來非常簡單。
? ? ? 調(diào)用完成后將在工程目錄下可以看到下載到的頁面源代碼。如果登錄成功,還可以在工程目錄下可到生成的cookies文件,而從服務(wù)器返回的數(shù)據(jù)內(nèi)容如下:
? ? ? 接下來我們就可以開始訪問我們賬戶的數(shù)據(jù)了,如我評論過的博文、我推薦過的博文、我關(guān)注的人!那么,我們還得先把頁面代碼下載下來:
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); //執(zhí)行http請求res = curl_easy_perform(curl);//釋放資源curl_easy_cleanup(curl);curl_slist_free_all(headers);curl_global_cleanup();fclose(homepage);} }請求URL設(shè)置為http://www.cnblogs.com/aggsite/mydigged,表示我推薦過的博文頁面。而Referer和host字段則根據(jù)fiddler抓取結(jié)果進行填充。注意這里的headers又進行了一次初始化哦。其他的仍然保持不變。要是沒有什么大問題,這個頁面的源代碼已經(jīng)下載完成了。那么接下來的工作就是解析頁面內(nèi)容了。
解析頁面內(nèi)容
? ? ? 解析HTML這種結(jié)構(gòu)性文本用字符串查找的方式或正則表達式看似都行,但是工作量實在太大,準確性還很難說。在網(wǎng)上找到一個專用于解析html代碼的C++庫:htmlcxx。這個庫是C++編寫的,目前似乎已經(jīng)停止更新了,最新的版本下載到的是0.84。這個庫下載下來的是源代碼,需要進行編譯生成lib使用。在windows環(huán)境下我使用vs2010直接編譯的,沒有錯誤產(chǎn)生。這個庫的文檔基本沒有,網(wǎng)上只有少數(shù)的幾個例子。下面以實例講解下該庫的使用方式:
using namespace htmlcxx;fstream out; out.open("out.txt", ios::out); // 所有的解析結(jié)果全部保存在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();
?先引入命名空間初始化解析器,并從中獲取到兩個迭代器。該庫允許我們以迭代器的方式來遍歷其構(gòu)造的DOM樹:
int count; string temp; for (; domBeg != domEnd; ++domBeg) // 遍歷文檔中所有的元素 {if (!domBeg->tagName().compare("div")) // 查找所有div標簽{domBeg->parseAttributes(); // 這個函數(shù)很重要。如果不調(diào)用,我們無法獲取標簽的屬性。而下面我們正需要獲取div的class屬性,所以必須調(diào)用。if (!domBeg->attribute("class").second.compare("post_item")) // 如果是class屬性值為post_item,表明是一個博文結(jié)構(gòu),開始解析{count = 0; // count計數(shù),每條博文只解析7個字段,主要是為了跳出循環(huán)。沒有找到更好的跳出循環(huán)的方法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) // 已經(jīng)找到7個字段,跳出循環(huán),繼續(xù)下一條博文的解析{break;}}}}}?上面的注釋已經(jīng)非常清楚了,htmlcxx這個庫的使用也非常簡單,提供的API只有七八個。看看都輸出了些什么:
? ? ? ?結(jié)果還不錯,代碼量卻很少。還真的是挺強大的,算法的力量!要是光靠字符串匹配還正不知道有沒有勇氣去做。另外,前面還提到了在登錄時需要組裝POST數(shù)據(jù)的問題。如果是手動寫死在代碼中,在推廣使用的時候顯然是不行的。還得從頁面中自動提取才行:
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) // 只要四個字段,提前結(jié)束解析工作。{break;}} }再看看提取結(jié)果:
? ? ? 規(guī)規(guī)矩矩、整整齊齊。好了,htmlcxx的演示到這里結(jié)束了。
遇到的問題
小結(jié)
? ? ? 登錄及頁面解析工作基本告一段落,下一階段就是界面整合。
轉(zhuǎn)載于:https://www.cnblogs.com/csuftzzk/p/libcurl_htmlcxx.html
總結(jié)
以上是生活随笔為你收集整理的用Qt写软件系列六:博客园客户端的设计与实现(1)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 10、并发容器,ConcurrentHa
- 下一篇: BZOJ.2780.[SPOJ8093]