博客園是本人每日必逛的一個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數據包了。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"
;
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文件,而從服務器返回的數據內容以下: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的演示到這裏結束了。
htmlcxx在解析中文的時候,可能會出現問題,須要進行調整。網上的代碼不少。聽說是htmlcxx的一個Bug。
libcurl使用POST的方式。CURLOPT_POSTFIELDS字段。
htmlcxx的編譯方式,須要保證編譯方式和目標工程方式一直,不然沒法和其餘庫一塊兒配合使用。解決方案:項目屬性-->C/C++-->代碼生成-->運行庫,與目標工程保持一致
登陸及頁面解析工做基本告一段落,下一階段就是界面整合。