zoukankan      html  css  js  c++  java
  • 用Qt写软件系列六:博客园客户端的设计与实现(用Fiddler抓包,用CURL提交数据,用htmlcxx解析HTML)

    引言

            博客园是本人每日必逛的一个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方式请求数据。因此在摸索过程遇到不少困难,接下来以代码的形式讲解组包发送的过程:

    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";
        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文件,而从服务器返回的数据内容如下:

          接下来我们就可以开始访问我们账户的数据了,如我评论过的博文、我推荐过的博文、我关注的人!那么,我们还得先把页面代码下载下来:

    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;
        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直接编译的,没有错误产生。这个库的文档基本没有,网上只有少数的几个例子。下面以实例讲解下该库的使用方式:

    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();    // 这个函数很重要。如果不调用,我们无法获取标签的属性。而下面我们正需要获取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(" v "));  // 去掉' ', ' ', 'v', ' ', ' '
                        temp.erase(temp.find_last_not_of(" v ") + 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++-->代码生成-->运行库,与目标工程保持一致

    小结

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

    http://www.cnblogs.com/csuftzzk/p/libcurl_htmlcxx.html

  • 相关阅读:
    同余 扩展欧几里得
    185. [USACO Oct08] 挖水井
    Dijkstra算法
    Floyed算法
    codves——1079 回家
    codves——1021 玛丽卡
    codves——5960 信使
    计算几何基础
    【正睿oi省选十连测】第一场
    [APIO2012]守卫
  • 原文地址:https://www.cnblogs.com/findumars/p/6025119.html
Copyright © 2011-2022 走看看