用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++-->代码生成-->运行库,与目标工程保持一致

小结

登陆及页面解析工做基本告一段落,下一阶段就是界面整合。

相关文章
相关标签/搜索