数据解析子模块

该子模块的逻辑比较简单,就是接收爬虫子模块传来的html代码,然后调用python代码后,返回所需要的对象,进一步包装后写入到数据访问层。

代码实现的话,先构建一个python帮助类,这里使用了boost::python

class PythonHelper
{
public:
    static void Init()
    {
        Py_Initialize();
    }
    //根据python文件名,python函数名获取相应的函数对象
    static boost::python::object GetFunc(const std::string &module, const std::string &func_name)
    {
        static auto main_namespace_ = []()->boost::python::object&
        {
            static boost::python::object main_module = boost::python::import("__main__");
            static boost::python::object obj = main_module.attr("__dict__");
            return obj;
        };
        static boost::python::object& main_namespace = main_namespace_();

        std::string filename("../py/");
        filename += module;
        filename += ".py";
        boost::python::exec_file(filename.c_str(), main_namespace, main_namespace);
        return main_namespace[func_name];
    }
    //运行python脚本的过程中发生异常时,输出日志
    static void WriteErrorLog(Common::Logger& log)
    {
        using namespace boost::python;
        using namespace boost;

        PyObject *exc, *val, *tb;
        object formatted_list, formatted;
        PyErr_Fetch(&exc, &val, &tb);
        handle<> hexc(exc), hval(allow_null(val)), htb(allow_null(tb));
        object traceback(import("traceback"));
        object format_exception(traceback.attr("format_exception"));
        formatted_list = format_exception(hexc, hval, htb);
        formatted = boost::python::str("\t\n").join(formatted_list);
        log.fatal(extract<std::string>(formatted));
    }

private:
    PythonHelper(void){}
    ~PythonHelper(void){}
};

其中Init()进行初始化操作,GetFunc()传入python脚本的文件(模块)名和函数名,返回相应的函数对象,而WriteErrorLog()允许在捕获到python异常时写入异常信息。

然后就着手编写具体的业务了,整个解析类的结构如下:

enum class TencentJobInfoParseResult
{
    Continue,//告诉clawer继续工作
    Stop,//告诉clawer已访问到末尾,该停下了
    Pass,//属于个例的错误,跳过
    Retry,//属于可以重试的错误
    Error,//遇到普通错误
    Fatal//遇到致命错误
};

//负责解析腾讯招聘的网页信息
class TencentJobInfoParser
{
public:
    //解析列表页面里的职位清单
    static TencentJobInfoParseResult ParseJobList(const std::string& html,
        std::vector<Model::Job*>& job_list,
        const Base::Date& end_date);
    //解析列表页面里的职位清单的hash值
    static TencentJobInfoParseResult ParseJobListHash(const std::string& html, uint64_t& hash);
    //解析列表页面里的职位清单,获取所有的职位日期
    static TencentJobInfoParseResult ParseJobDates(const std::string& html, std::vector<Base::Date>& job_dates);
    //只获取第一和最后一个item的日期
    static TencentJobInfoParseResult ParseJobDates(const std::string& html, Base::Date& first_item_date, Base::Date& last_item_date);
    //解析详情页面里的职位详细信息
    static TencentJobInfoParseResult ParseJobDetail(Model::Job& job,const std::string& html);
    static void Init();
    static Common::Logger& log;
private:
    TencentJobInfoParser(void){}
    ~TencentJobInfoParser(void){}
};

ParseJobList()解析列表页的方法展示一下解析的过程吧:

首先是C++代码:

TencentJobInfoParseResult TencentJobInfoParser::ParseJobList(const std::string& html, 
    std::vector<Model::Job*>& job_list,
    const Base::Date& end_date)
{
    using namespace boost::python;
    using Common::PythonHelper;
    using Common::DateHelper;
    using Base::Date;            
    try
    {
        static const std::string kTruncatedStr("truncated");
        if (kTruncatedStr.compare(html)==0)
        {
            return TencentJobInfoParseResult::Stop;
        }

        //由于解析html的时间相对于抓取html的时间可以忽略不计
        //再加上两次http请求间必须有延迟(频率过高可能被防火墙拉黑)
        //所以此处的重点不在于执行效率,为了灵活起见嵌入python脚本

        //调用parser.py中的parseTencentJobList函数,将来若页面结构发生变化只需要调整py文件
        //此处将函数对象设为static,这样第一次调用GetFunc方法时这个对象会被保存起来供下次使用,避免重复的文件访问
        static object func = PythonHelper::GetFunc("parser", "parseTencentJobList");
        object ret = func(html);
        object list = ret[0];
        bool has_next = extract<bool>(ret[1]);

        if (!list.is_none())
        {
            int length = len(list);
            if (length == 0)
            {
                //有列表,但没数据,说明已经抓到底了
                return TencentJobInfoParseResult::Stop;
            }

            //正常返回
            for (int i = 0; i < length; i++)
            {
                using std::string;
                object obj = list[i];
                string url, title, type, location, date_str;
                int id, hiring_number;
                id = extract<int>(obj[0]);
                title = extract<string>(obj[1]);
                type = extract<string>(obj[2]);
                hiring_number = extract<int>(obj[3]);
                location = extract<string>(obj[4]);
                date_str = extract<string>(obj[5]);
                try
                {
                    Date date = DateHelper::ToDate(date_str);
                    if (date<end_date)
                    {
                        //日期超越抓取范围,停止抓取任务
                        return TencentJobInfoParseResult::Stop;
                    }
                    //将数据写入数据访问层,同时将实体类对象指针写入列表结果
                    job_list.emplace_back(&Model::Job::Add(id, title, type, hiring_number, location, date));
                }
                catch (std::exception& e)
                {
                    log.error(fmt("解析日期时发生异常[%1%,%2%,%3%]:%4%")%id %title % date_str % e.what());
                    continue;//当做是个别现象,跳过这一行
                }
            }
            return has_next?TencentJobInfoParseResult::Continue : TencentJobInfoParseResult::Stop;
        }
        else
        {
            return TencentJobInfoParseResult::Error;
        }

    }
    catch (boost::python::error_already_set&)
    {
        PythonHelper::WriteErrorLog(log);
        return TencentJobInfoParseResult::Fatal;
    }
}

假设是所解析的如图所示列表页,程序调用解析html的python脚本后,会得到一个python的list对象。list对象保存了每行的数据,我们分别将其还原成id,title(标题),type(种类),hiring_number(招聘人数),location(地点),date_str(发布日期)。

id = extract<int>(obj[0]);
title = extract<string>(obj[1]);
type = extract<string>(obj[2]);
hiring_number = extract<int>(obj[3]);
location = extract<string>(obj[4]);
date_str = extract<string>(obj[5]);

然后写入职位信息到数据访问层,如果发现职位信息超过抓取范围,则会返回一个标识,标识应停止任务:

Date date = DateHelper::ToDate(date_str);
if (date<end_date)
{
    //日期超越抓取范围,停止抓取任务
    return TencentJobInfoParseResult::Stop;
}

job_list.emplace_back(&Model::Job::Add(id, title, type, hiring_number, location, date));

注意,经过这一步抓取后职位对象的数据还不完整,因为没有访问到详情页,并不知道里面的内容(工作职责和工作要求)是什么,需要后面再做进一步的数据填充。

C++的代码就是这些,接下来是对应的python脚本的代码:

def parseTencentJobList(html):
    tree=etree.HTML(html)
    table=tree.xpath("//div[@id='position']//table[@class='tablelist']")
    #若table长度为0,说明未能成功解析到期待的页面,返回None
    if len(table)==0:
        return None
    list=[]
    has_next=len(table[0].xpath(".//a[@id='next' and @class='noactive']"))==0
    nodes=table[0].xpath("tr[@class='even' or @class='odd']")
    for node in nodes:
        tds=node.findall("./td")
        a=tds[0].find("./a")
        url=a.get("href")
        m=re.search(r"id=(\d+)",url) #利用正则表达式从href的url中解析出职位的id
        id=int(m.group(1))
        title=myEncode(a.text)
        type=myEncode(tds[1].text)
        hiring_number=int(tds[2].text)
        location=myEncode(tds[3].text)
        date=myEncode(tds[4].text)
        list.append((id,title,type,hiring_number,location,date))
    return list,has_next

其中职位id没有直接展现出来,我是利用正则表达式从href里的url里提取出来的。

整个代码完全是利用xpath对网页元素进行解析,比较死板,任何页面结构上的小改动都有可能导致该代码失效。

好在解析部分的代码用python实现,更新代码所花费的代价相对比较小。

详情页的解析等等原理差不多,这里就不重复了。

results matching ""

    No results matching ""