Go语言网络爬虫分析器接口

分析器的接口包含两个额外的方法 RespParsers 和 Analyze,其中前者会返回当前分析器使用的 HTTP 响应解析函数(以下简称解析函数)的列表因此,分析器的实现类型有用于存储此列表的字段。另外,与下载器的实现类型相同,它也有一个 stub.ModuleInternal 类型的匿名字段。相关代码如下:
//分析器的实现类型
type myAnalyzer struct {
    //组件基础实例
    stub.ModuleInternal
    //响应解析器列表
    respParsers []module.ParseResponse
}
该类型及其方法存放在 gopcp.v2/chapter6/webcrawler/module/local/analyzer 代码包 中。大家可以从我的网盘(链接:https://pan.baidu.com/s/1yzWHnK1t2jLDIcTPFMLPCA 提取码:slm5)中下载相关的代码包。当然,还有 New 函数:
//用于创建一个分析器实例
func New(
    mid module.MID,
    respParsers []module.ParseResponse,
    scoreCalculator module.CalculateScore) (module.Analyzer, error) {
    moduleBase, err := stub.NewModuleInternal(mid, scoreCalculator)
    if err != nil {
        return nil, err
    }
    if respParsers == nil {
        return nil, genParameterError("nil response parsers")
    }
    if len(respParsers) == 0 {
        return nil, genParameterError("empty response parser list")
    }
    var innerParsers []module.ParseResponse
    for i, parser := range respParsers {
        if parser == nil {
            return nil, genParameterError(fmt.Sprintf("nil response parser[%d]", i))
        }
        innerParsers = append(innerParsers, parser)
    }
    return &myAnalyzer{
        ModuleInternal: moduleBase,
        respParsers:    innerParsers,
    }, nil
}
该函数中的大部分代码都用于参数检查。对参数 respParsers 的检査要尤为仔细,因为它们一定是网络爬虫框架的使用方提供的,属于外来代码。

分析器的 Analyze 方法的功能是,先接收响应并检查,再把 HTTP 响应依次交给它持有的若干解析函数处理,最后汇总并返回从解析函数那里获得的数据列表和错误列表。

由于 Analyze 方法的实现比较长,这里分段讲解。先来看看检查响应的代码:
func (analyzer *myAnalyzer) Analyze(
    resp *module.Response) (dataList []module.Data, errorList []error) {
    analyzer.ModuleInternal.IncrHandlingNumber()
    defer analyzer.ModuleInternal.DecrHandlingNumber()
    analyzer.ModuleInternal.IncrCalledCount()
    if resp == nil {
        errorList = append(errorList,
            genParameterError("nil response"))
        return
    }
    httpResp := resp.HTTPResp()
    if httpResp == nil {
        errorList = append(errorList,
            genParameterError("nil HTTP response"))
        return
    }
    httpReq := httpResp.Request
    if httpReq == nil {
        errorList = append(errorList,
            genParameterError("nil HTTP request"))
        return
    }
    var reqURL = httpReq.URL
    if reqURL == nil {
        errorList = append(errorList,
            genParameterError("nil HTTP request URL"))
        return
    }
    analyzer.ModuleInternal.IncrAcceptedCount()
    respDepth := resp.Depth()
    logger.Infof("Parse the response (URL: %s, depth: %d)... \n",
        reqURL, respDepth)
    //省略部分代码
}
这里的检查非常细,要像庖丁解牛一样检查参数值的内里。因为任何异常都有可能造成解析函数执行失败。我们一定不要给它们造成额外的困扰。一旦检查通过,就可以递增接受计数了。然后打印出一行日志,代表分析器已经开始解析某个响应了。

还记得前面讲的多重读取器吗?现在该用到它了:
func (analyzer *myAnalyzer) Analyze(
    resp *module.Response) (dataList []module.Data, errorList []error) {
    //省略部分代码
    //解析HTTP响应
    if httpResp.Body != nil {
        defer httpResp.Body.Close()
    }
    multipleReader, err := reader.NewMultipleReader(httpResp.Body)
    if err != nil {
        errorList = append(errorList, genError(err.Error()))
        return
    }
    dataList = []module.Data{}
    for respParser := range analyzer.respParsers {
        httpResp.Body = multipleReader.Reader()
        pDataList, pErrorList := respParser(httpResp, respDepth)
        if pDataList != nil {
            for _, pData := range pDataList {
                if pData == nil {
                    continue
                }
                dataList = appendDataList(dataList, pData, respDepth)
            }
        }
        if pErrorList I- nil {
            for _, pError := range pErrorList {
                if pError == nil {
                    continue
                }
                errorList = append(errorList, pError)
            }
        }
    }
    if len(errorList) == 0 {
        analyzer.ModuleInternal.IncrCompletedCount()
    }
    return dataList, errorList
}
这里先依据 HTTP 响应的 Body 字段初始化一个多重读取器,然后在每次调用解析函数之前先从多重读取器那里获取一个新的读取器并对 HTTP 响应的 Body 字段重新赋值,这样就解决了 Body 字段值的底层数据只能读取一遍的问题。

每个解析函数都可以顺利读出 HTTP 响应体。在所有解析都完成之后,如果错误列表为空,就递增成功完成计数。最后,我会返回收集到的数据列表和错误列表。

由于我们把解析 HTTP 响应的任务都交给了解析函数,所以 Analyze 方法的实现还是比较简单的,代码逻辑也很清晰。