Skip to content

Latest commit

 

History

History
242 lines (210 loc) · 8.9 KB

03.4.md

File metadata and controls

242 lines (210 loc) · 8.9 KB

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数据的方式存储。