【总结】libcurl+jsoncpp实现简单的数据爬取

Posted on Posted in 计算机2,646 views

    背景:在做一个小的爬取数据工具,由于其他原因得采用C++开发。C++在做爬虫这类应用时不是很方便,主要有两个问题需要解决:一是模拟http请求,二是http响应数据的解析。其他的就只是设计业务逻辑的组织了,倒是问题不大。最终选择采用libcurl来实现http请求,由于选择的目标站点接口返回的是json数据,所以数据解析采用了jsoncpp来实现。

零、环境介绍

  • 服务器:腾讯云服务器

            CPU:单核,最大 2000 MHz 

            内存: 1 GB

  • 系统:Linux VM-122-135-ubuntu 3.13.0-36-generic 

            #63-Ubuntu SMP Wed Sep 3 21:30:07 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

  • G++:g++ (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

  • libcurl:7.50.1

  • jsoncpp:0.5.0

一、libcurl库

1.1、介绍

    libcurl为一个免费开源的、跨平台的网络协议库,支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议。libcurl同样支持HTTPS证书授权,HTTP POST, HTTP PUT, FTP 上传, HTTP基本表单上传,代理,cookies,和用户认证。同时libcurl是线程安全的,也兼容IPV6,已经被很多知名的大企业以及应用程序所采用。

    curl命令行工具,可以通过shell或脚本来运行curl,而curl底层所使用的库是libcurl。不同语言对curl有着不同的封装,比如php的curl、python的pycurl等。他们底层调用的东西是一样的,但可能封装出来的接口参数可能不一样。

    官网:https://curl.haxx.se/

1.2、编译

    1、下载:https://curl.haxx.se/download/curl-7.50.1.tar.gz

    2、编译:

tar -zxvf curl-7.50.1.tar.gz 
cd curl-7.50.1
./configure --prefix=xxxx #自己定义编译完安装的目录
make 
make install

    3、编译安装结束后,在xxxx目录下会有变异结果,大致结构如下,需要的主要还是include和lib下的libcurl.a。

  xxxx/
        bin/
        include/
        lib/
        share/

1.3、使用

    1、编码

    按自己的习惯组织代码的include文件,然后就可以在程序中直接使用libcurl的接口了,具体接口可以去官网查看文档。为了使用方便可以自己按需求封装一个httpclient类,实现get/post等方法。以get请求为例,部分实现一下代码:

#include "../curl/include/curl.h"
static size_t OnWriteData(void* buffer, size_t size, size_t nmemb, void* lpVoid) {
    std::string* str = dynamic_cast<std::string*>((std::string *)lpVoid);
    if( NULL == str || NULL == buffer ) {
        return -1;
    }
    char* pData = (char*)buffer;
    str->append(pData, size * nmemb);
    return nmemb;
}

int HttpClient::Get(const std::string url, std::string& respone) {
    CURLcode res;
    CURL* curl = curl_easy_init();
    if(NULL == curl) {
        return CURLE_FAILED_INIT;
    }
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&respone);
    /**
     * 当多个线程都使用超时处理的时候,同时主线程中有sleep或是wait等操作。
     * 如果不设置这个选项,libcurl将会发信号打断这个wait从而导致程序退出。
     */
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5);
    res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);
    return res;
}

    2、编译

    当时在编译时遇到了点问题,按常规的g++ http_client.cc -o http_client -L../curl/lib -lcurl编译会有如下错误:

图片3.png

    查阅网上资料,原因是没有加-lz,所以完整的编译链接命令如下,在工程中:

g++ http_client.cc -o http_client -L../curl/lib -lcurl -lz

二、jsoncpp库

2.1、介绍

    JSON(JavaScript Object Notation)跟xml一样也是一种数据交换格式,现在在网络中普遍存在,其官网为:http://json.org

    JsonCpp是C++中对json解析比较好的第三方库。主要包含三种类型:Value Reader Writer。不过注意Value只能处理ANSI类型的字符串,如果C++程序使用Unicode编码的,最好加一个Adapt类来适配。

2.2、编译

    1、下载:https://sourceforge.net/projects/jsoncpp/

    2、编译

    jsoncpp采用scons来编译,所以要先安装scons。scons是一个Python写的自动化构建工具,从构建这个角度说,它跟GNU make是同一类的工具。它是一种改进,并跨平台的gnu make替代工具,其集成功能类似于autoconf/automake 。scons是一个更简便,更可靠,更高效的编译软件。

tar zxvf jsoncpp-src-0.5.0.tar.gz
cd jsoncpp-src-0.5.0/
apt-get install scons
scons platform=linux-gcc

    编译完成后在主目录下会有需要的头文件目录include,和库文件目录libs,我们需要使用头文件和库文件libjson_linux-gcc-4.8_libmt.a。

2.3、使用

    1、编码

    同样按自己的习惯组织代码的include文件,然后就可以在程序中直接使用jsoncpp的接口了,具体接口可以去官网查看文档。一般在使用中json的格式是给定了了,然后根据格式来解析。部分实现一下代码:

{
 "success":"1",
 "result":{
 "status":"ALREADY_ATT",
 "phone":"18813759486",
 "area":"020",
 "postno":"510000",
 "att":"中国,广东,广州",
 "ctype":"中国移动188卡",
 "par":"1881375",
 "prefix":"188",
 "operators":"中国移动",
 "style_simcall":"中国,广东,广州",
 "style_citynm":"中华人民共和国,广东省,广州市"
 }
} 
#include "../jsoncpp/include/json.h"
int PhoneSpiderK78::DecodeJsonK78(const std::string& json_str, DataResponeK78& result) {
    int code = OK;
    Json::Reader reader;
    Json::Value root;
    if (json_str=="") {
        code = ERROR_K78_JSON_NULL;
        return code;
    }
    if (reader.parse(json_str, root)) {
        //解析json中的对象
        if ((result.success = root["success"].asString() ) == "1") {
            if (! root["result"].isNull() ) {
                Json::Value value = root["result"]; // 解析
                if ((result.status = value["status"].asString() ) == "ALREADY_ATT") {
                    result.phone = value["phone"].asString();
                    result.area = value["area"].asString();
                    result.postno = value["postno"].asString();
                    result.att = value["att"].asString();
                    result.ctype = value["ctype"].asString();
                    result.par = value["par"].asString();
                    result.prefix = value["prefix"].asString();
                    result.operators = value["operators"].asString();
                    result.style_simcall = value["style_simcall"].asString();
                    result.sstyle_citynmtatus = value["sstyle_citynmtatus"].asString();
                    code = result.SplitBelongs();
                } else {
                    result.msg = value["msg"].asString();
                    code = ERROR_K78_PHONE_NOEXIST;
                }
            }
        } else {
            result.msg = root["msg"].asString();
            code = ERROR_K78_SERVER_ERROR;  
        }
    } else {
        code = ERROR_K78_JSON_FORMAT;
    }
    return code;
}

    2、编译:按正常的库引用格式编译即可,即在连接时加上libjson_linux-gcc-4.8_libmt.a

g++ http_client.cc -o http_client ../jsoncpp/lib/libjson_linux-gcc-4.8_libmt.a

三、逻辑封装

3.1、整体思路

    因为该工具主要是用于爬取特定的数据,数据最终处理的格式是统一的。所以整个爬取过程可以分为四部分:生成访问连接、访问、解析结果、格式化存储。考虑到通用性,不同的数据源的访问请求格式和返回结果不一样,所以生成连接和访问结果处理对于不同的数据源需要特殊处理。

    于是考虑使用继承来实现整个工具结构,父类主要用于控制整个爬取的流程和最终的数据处理,子类负责具体的爬取和数据解析过程,对于不同的数据源只需要书写相应的子类即可。下面将粗略介绍整个结构,具体实现和细节处理忽略。

3.2、数据结构

    1、枚举:SpiderCode    整个爬取工具的异常代码,统一管理

    2、结构:PhoneData    统一的数据交互的结构

    3、父类:PhoneSpider    父类

    4、子类:PhoneSpiderK78    k78对应的子类

    5、类:HttpClient    对libcurl的封装,用于网络结构访问

    6、类:MysqlDb    对mysql操作的封装,用于和数据库交互

3.3、类和流程

    根据目前的构思实现了下面的类结构,父类为PhoneSpider实现整体流程控制,对外开放Run和RunSegment,子类PhoneSpiderK78实现具体的获取和解析数据。

QQ截图20160805144315.png

    整个调用和数据流程如下,蓝色部分是对外接口:

QQ截图20160805151306.png

3.4、扩展和使用

    目前这个结构是第一版本,之后可以按照需求修改和调整,比如实现参数配置。下面是调用的接口示例:

int main(int argc, char const *argv[]) {
    if (argc == 2) {
        spider::PhoneSpider* phone_spider = new spider::PhoneSpiderK78("10003", "b59bc3ef6191eb9f747dd4e83c99f2a4");
        phone_spider->Run(argv[1]);
        delete phone_spider;
    }
    else if (argc == 4) {
        int sleep_time = atoi(argv[3]);
        spider::PhoneSpider* phone_spider = new spider::PhoneSpiderK78("10003", "b59bc3ef6191eb9f747dd4e83c99f2a4");
        phone_spider->RunSegment(sleep_time, argv[1], argv[2]);
        delete phone_spider;
    }
    else {
        std::cout<<"error input!"<<std::endl;;
    }
    return 0;
}

四、参考

1、https://zh.wikipedia.org/wiki/CURL

2、http://www.cnblogs.com/moodlxs/archive/2012/10/15/2724318.html

3、http://www.justwinit.cn/post/4698/

4、http://www.cnblogs.com/johnice/archive/2013/01/17/2864319.html

5、http://blog.csdn.net/huyiyang2010/article/details/7664201

6、http://blog.sina.com.cn/s/blog_6c4a60110101342m.html

7、http://developer.51cto.com/art/201201/311073.htm


转载标明出处:https://blog.evanxia.com/2016/08/935