3.4 运行规则分析

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

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

  1. package spider_lib
  2. // 基础包
  3. import (
  4. "github.com/PuerkitoBio/goquery" //DOM解析
  5. "github.com/henrylee2cn/pholcus/app/downloader/context" //必需
  6. . "github.com/henrylee2cn/pholcus/app/spider" //必需
  7. . "github.com/henrylee2cn/pholcus/app/spider/common" //选用
  8. "github.com/henrylee2cn/pholcus/logs" //信息输出
  9. // net包
  10. "net/http" //设置http.Header
  11. // "net/url"
  12. // 编码包
  13. // "encoding/xml"
  14. // "encoding/json"
  15. // 字符串处理包
  16. // "regexp"
  17. "strconv"
  18. "strings"
  19. // 其他包
  20. // "fmt"
  21. // "math"
  22. // "time"
  23. )
  24. func init() {
  25. AlibabaProduct.Register()
  26. }
  27. var AlibabaProduct = &Spider{
  28. Name: "阿里巴巴产品搜索",
  29. Description: "阿里巴巴产品搜索 [s.1688.com/selloffer/offer_search.htm]",
  30. // Pausetime: 300,
  31. Keyword: KEYWORD,
  32. MaxPage: MAXPAGE,
  33. EnableCookie: false,
  34. RuleTree: &RuleTree{
  35. Root: func(ctx *Context) {
  36. ctx.Aid(map[string]interface{}{"loop": [2]int{0, 1}, "Rule": "生成请求"}, "生成请求")
  37. },
  38. Trunk: map[string]*Rule{
  39. "生成请求": {
  40. AidFunc: func(ctx *Context, aid map[string]interface{}) interface{} {
  41. keyword := EncodeString(ctx.GetKeyword(), "GBK")
  42. for loop := aid["loop"].([2]int); loop[0] < loop[1]; loop[0]++ {
  43. ctx.AddQueue(&context.Request{
  44. 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),
  45. Rule: aid["Rule"].(string),
  46. Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
  47. })
  48. }
  49. return nil
  50. },
  51. ParseFunc: func(ctx *Context) {
  52. query := ctx.GetDom()
  53. pageTag := query.Find("#sm-pagination div[data-total-page]")
  54. // 跳转
  55. if len(pageTag.Nodes) == 0 {
  56. logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 由于跳转AJAX问题,目前只能每个子类抓取 1 页……\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
  57. query.Find(".sm-floorhead-typemore a").Each(func(i int, s *goquery.Selection) {
  58. if href, ok := s.Attr("href"); ok {
  59. ctx.AddQueue(&context.Request{
  60. Url: href,
  61. Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
  62. Rule: "搜索结果",
  63. })
  64. }
  65. })
  66. return
  67. }
  68. total1, _ := pageTag.First().Attr("data-total-page")
  69. total1 = strings.Trim(total1, " \t\n")
  70. total, _ := strconv.Atoi(total1)
  71. if total > ctx.GetMaxPage() {
  72. total = ctx.GetMaxPage()
  73. } else if total == 0 {
  74. logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 没有抓取到任何数据!!!\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
  75. return
  76. }
  77. // 调用指定规则下辅助函数
  78. ctx.Aid(map[string]interface{}{"loop": [2]int{1, total}, "Rule": "搜索结果"})
  79. // 用指定规则解析响应流
  80. ctx.Parse("搜索结果")
  81. },
  82. },
  83. "搜索结果": {
  84. //注意:有无字段语义和是否输出数据必须保持一致
  85. ItemFields: []string{
  86. "公司",
  87. "标题",
  88. "价格",
  89. "销量",
  90. "星级",
  91. "地址",
  92. "链接",
  93. },
  94. ParseFunc: func(ctx *Context) {
  95. query := ctx.GetDom()
  96. query.Find("#sm-offer-list > li").Each(func(i int, s *goquery.Selection) {
  97. // 获取公司
  98. company, _ := s.Find("a.sm-offer-companyName").First().Attr("title")
  99. // 获取标题
  100. t := s.Find(".sm-offer-title > a:nth-child(1)")
  101. title, _ := t.Attr("title")
  102. // 获取URL
  103. url, _ := t.Attr("href")
  104. // 获取价格
  105. price := s.Find(".sm-offer-priceNum").First().Text()
  106. // 获取成交量
  107. sales := s.Find("span.sm-offer-trade > em").First().Text()
  108. // 获取地址
  109. address, _ := s.Find(".sm-offer-location").First().Attr("title")
  110. // 获取信用年限
  111. level := s.Find("span.sm-offer-companyTag > a.sw-ui-flaticon-cxt16x16").First().Text()
  112. // 结果存入Response中转
  113. ctx.Output(map[int]interface{}{
  114. 0: company,
  115. 1: title,
  116. 2: price,
  117. 3: sales,
  118. 4: level,
  119. 5: address,
  120. 6: url,
  121. })
  122. })
  123. },
  124. },
  125. },
  126. },
  127. }

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

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

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

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

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

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

  1. "生成请求": {
  2. AidFunc: func(ctx *Context, aid map[string]interface{}) interface{} {
  3. keyword := EncodeString(ctx.GetKeyword(), "GBK")
  4. for loop := aid["loop"].([2]int); loop[0] < loop[1]; loop[0]++ {
  5. ctx.AddQueue(&context.Request{
  6. 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),
  7. Rule: aid["Rule"].(string),
  8. Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
  9. })
  10. }
  11. return nil
  12. },
  13. ParseFunc: func(ctx *Context) {
  14. query := ctx.GetDom()
  15. pageTag := query.Find("#sm-pagination div[data-total-page]")
  16. // 跳转
  17. if len(pageTag.Nodes) == 0 {
  18. logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 由于跳转AJAX问题,目前只能每个子类抓取 1 页……\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
  19. query.Find(".sm-floorhead-typemore a").Each(func(i int, s *goquery.Selection) {
  20. if href, ok := s.Attr("href"); ok {
  21. ctx.AddQueue(&context.Request{
  22. Url: href,
  23. Header: http.Header{"Content-Type": []string{"text/html", "charset=GBK"}},
  24. Rule: "搜索结果",
  25. })
  26. }
  27. })
  28. return
  29. }
  30. total1, _ := pageTag.First().Attr("data-total-page")
  31. total1 = strings.Trim(total1, " \t\n")
  32. total, _ := strconv.Atoi(total1)
  33. if total > ctx.GetMaxPage() {
  34. total = ctx.GetMaxPage()
  35. } else if total == 0 {
  36. logs.Log.Critical("[消息提示:| 任务:%v | 关键词:%v | 规则:%v] 没有抓取到任何数据!!!\n", ctx.GetName(), ctx.GetKeyword(), ctx.GetRuleName())
  37. return
  38. }
  39. // 调用指定规则下辅助函数
  40. ctx.Aid(map[string]interface{}{"loop": [2]int{1, total}, "Rule": "搜索结果"})
  41. // 用指定规则解析响应流
  42. ctx.Parse("搜索结果")
  43. },
  44. },

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

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

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

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