3.4 运行规则分析

本节会给大家提供一个参考实例,用于告诉大家如何根据具体的业务实现自己的爬虫框架。

我们以公共规则中“阿里巴巴产品搜索”为例(这些公共的规则都在github.com/pholcus下面包含,大家可以参考下)。

package spider_lib

// 基础包
import (
    "github.com/PuerkitoBio/goquery"                        //DOM解析
    "github.com/henrylee2cn/pholcus/app/downloader/context" //必需
    . "github.com/henrylee2cn/pholcus/app/spider"           //必需
    . "github.com/henrylee2cn/pholcus/app/spider/common"    //选用
    "github.com/henrylee2cn/pholcus/logs"                   //信息输出

    // net包
    "net/http" //设置http.Header
    // "net/url"

    // 编码包
    // "encoding/xml"
    // "encoding/json"

    // 字符串处理包
    // "regexp"
    "strconv"
    "strings"

    // 其他包
    // "fmt"
    // "math"
    // "time"
)

func init() {
    AlibabaProduct.Register()
}

var AlibabaProduct = &Spider{
    Name:        "阿里巴巴产品搜索",
    Description: "阿里巴巴产品搜索 [s.1688.com/selloffer/offer_search.htm]",
    // Pausetime: 300,
    Keyword:      KEYWORD,
    MaxPage:      MAXPAGE,
    EnableCookie: false,
    RuleTree: &RuleTree{
        Root: func(ctx *Context) {
            ctx.Aid(map[string]interface{}{"loop": [2]int{0, 1}, "Rule": "生成请求"}, "生成请求")
        },

        Trunk: map[string]*Rule{

            "生成请求": {
                AidFunc: func(ctx *Context, aid map[string]interface{}) interface{} {
                    keyword := EncodeString(ctx.GetKeyword(), "GBK")
                    for loop := aid["loop"].([2]int); loop[0] < loop[1]; loop[0]++ {
                        ctx.AddQueue(&context.Request{
                            Url:    "http://s.1688.com/selloffer/offer_search.htm?enableAsync=false&earseDirect=false&button_click=top&pageSize=60&n=y&offset=3&uniqfield=pic_tag_id&keywords=" + keyword + "&beginPage=" + strconv.Itoa(loop[0]+1),
                            Rule:   aid["Rule"].(string),
                            Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
                        })
                    }
                    return nil
                },
                ParseFunc: func(ctx *Context) {
                    query := ctx.GetDom()
                    pageTag := query.Find("#sm-pagination div[data-total-page]")
                    // 跳转
                    if len(pageTag.Nodes) == 0 {
                        logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 由于跳转AJAX问题,目前只能每个子类抓取 1 页……\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
                        query.Find(".sm-floorhead-typemore a").Each(func(i int, s *goquery.Selection) {
                            if href, ok := s.Attr("href"); ok {
                                ctx.AddQueue(&context.Request{
                                    Url:    href,
                                    Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
                                    Rule:   "搜索结果",
                                })
                            }
                        })
                        return
                    }
                    total1, _ := pageTag.First().Attr("data-total-page")
                    total1 = strings.Trim(total1, " \t\n")
                    total, _ := strconv.Atoi(total1)
                    if total > ctx.GetMaxPage() {
                        total = ctx.GetMaxPage()
                    } else if total == 0 {
                        logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 没有抓取到任何数据!!!\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
                        return
                    }

                    // 调用指定规则下辅助函数
                    ctx.Aid(map[string]interface{}{"loop": [2]int{1, total}, "Rule": "搜索结果"})
                    // 用指定规则解析响应流
                    ctx.Parse("搜索结果")
                },
            },

            "搜索结果": {
                //注意:有无字段语义和是否输出数据必须保持一致
                ItemFields: []string{
                    "公司",
                    "标题",
                    "价格",
                    "销量",
                    "星级",
                    "地址",
                    "链接",
                },
                ParseFunc: func(ctx *Context) {
                    query := ctx.GetDom()

                    query.Find("#sm-offer-list > li").Each(func(i int, s *goquery.Selection) {

                        // 获取公司
                        company, _ := s.Find("a.sm-offer-companyName").First().Attr("title")

                        // 获取标题
                        t := s.Find(".sm-offer-title > a:nth-child(1)")
                        title, _ := t.Attr("title")

                        // 获取URL
                        url, _ := t.Attr("href")

                        // 获取价格
                        price := s.Find(".sm-offer-priceNum").First().Text()

                        // 获取成交量
                        sales := s.Find("span.sm-offer-trade > em").First().Text()

                        // 获取地址
                        address, _ := s.Find(".sm-offer-location").First().Attr("title")

                        // 获取信用年限
                        level := s.Find("span.sm-offer-companyTag > a.sw-ui-flaticon-cxt16x16").First().Text()

                        // 结果存入Response中转
                        ctx.Output(map[int]interface{}{
                            0: company,
                            1: title,
                            2: price,
                            3: sales,
                            4: level,
                            5: address,
                            6: url,
                        })
                    })
                },
            },
        },
    },
}

从代码中可以看到,总体上,我们实例化一个Spider类型的对象,如之前所讲,对象的Name是必须要有的:

    Name:        "阿里巴巴产品搜索",
    Description: "阿里巴巴产品搜索 [s.1688.com/selloffer/offer_search.htm]",
    // Pausetime: 300,
    Keyword:      KEYWORD,
    MaxPage:      MAXPAGE,
    EnableCookie: false,

这个结构的各种配置之前已经讲过,这里不赘述,如果需要调整,可以在这里赋值。

RuleTree正是我们的规则端的入口。Root: func可以认为程序在装载好任务后,首先执行的程序入口

    RuleTree: &RuleTree{
        Root: func(ctx *Context) {
            ctx.Aid(map[string]interface{}{"loop": [2]int{0, 1}, "Rule": "生成请求"}, "生成请求")
        },

这里我们看到,我们通过ctx.Aid函数给规则添加了第一个任务,就是执行key为"生成请求"的AidFunc: func函数,顺着代码,我们可以找到这个函数的代码:

            "生成请求": {
                AidFunc: func(ctx *Context, aid map[string]interface{}) interface{} {
                    keyword := EncodeString(ctx.GetKeyword(), "GBK")
                    for loop := aid["loop"].([2]int); loop[0] < loop[1]; loop[0]++ {
                        ctx.AddQueue(&context.Request{
                            Url:    "http://s.1688.com/selloffer/offer_search.htm?enableAsync=false&earseDirect=false&button_click=top&pageSize=60&n=y&offset=3&uniqfield=pic_tag_id&keywords=" + keyword + "&beginPage=" + strconv.Itoa(loop[0]+1),
                            Rule:   aid["Rule"].(string),
                            Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
                        })
                    }
                    return nil
                },
                ParseFunc: func(ctx *Context) {
                    query := ctx.GetDom()
                    pageTag := query.Find("#sm-pagination div[data-total-page]")
                    // 跳转
                    if len(pageTag.Nodes) == 0 {
                        logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 由于跳转AJAX问题,目前只能每个子类抓取 1 页……\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
                        query.Find(".sm-floorhead-typemore a").Each(func(i int, s *goquery.Selection) {
                            if href, ok := s.Attr("href"); ok {
                                ctx.AddQueue(&context.Request{
                                    Url:    href,
                                    Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
                                    Rule:   "搜索结果",
                                })
                            }
                        })
                        return
                    }
                    total1, _ := pageTag.First().Attr("data-total-page")
                    total1 = strings.Trim(total1, " \t\n")
                    total, _ := strconv.Atoi(total1)
                    if total > ctx.GetMaxPage() {
                        total = ctx.GetMaxPage()
                    } else if total == 0 {
                        logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 没有抓取到任何数据!!!\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
                        return
                    }

                    // 调用指定规则下辅助函数
                    ctx.Aid(map[string]interface{}{"loop": [2]int{1, total}, "Rule": "搜索结果"})
                    // 用指定规则解析响应流
                    ctx.Parse("搜索结果")
                },
            },

可以看到,"生成请求"这个成员变量内部包含了两个函数AidFunc和ParseFunc,如之前的文章介绍的,通过Aid添加的任务由AidFunc解析,通过Addqueue函数添加的任务由ParseFunc负责解析,不同的是程序在执行ParseFunc之前会默认抓取Addqueue添加的的request请求,并把response添加到ctx中去。 这就是为什么我们没有明显的看到URL请求,却能直接通过ctx.GetDom来获取Dom树了。此外,这里调用了Parse函数,执行过程是不解析url直接用当先的ctx执行ParseFunc函数。此外,代码中还有一些goquery的语法(建议参考W3c jquery相关语法)及golang本身的语法,这些大家可以自己研究下。

另外,我们可以看到一些关于输出的语句:

                    ctx.Output(map[int]interface{}{
                        0: title,
                        1: ctx.GetTemp("description", ""),
                        2: infoStr,
                        3: ctx.GetTemp("releaseTime", ""),
                        4: ctx.GetTemp("src", ""),
                        5: ctx.GetTemp("author", ""),
                    })

pholcus目前支持csv,mongo,mysql和excel四种数据存储方式,这些都可以在见面上手动设置。存储的方式统一都是已Key-Value数据的方式存储。