原创分享 Golang 使用 Chromedp 绕过反爬抓取微信公众号文章

wxj95 · 2020年09月16日 · 2441 次阅读
本帖已被设为精华帖!

Golang 使用 Chromedp 绕过反爬抓取微信公众号文章

前言

最近准备爬取一些资讯类技术类文章来做一个信息的聚合。但是呢爬取某些网站的时候会遇到一些反爬策略,很是令人头疼。这里拿利用搜狗搜索爬取微信公众号来举个栗子。这里利用https://weixin.sogou.com/weixin?type=1&s_from=input&query=%E8%85%BE%E8%AE%AF%E7%8E%84%E6%AD%A6%E5%AE%9E%E9%AA%8C%E5%AE%A4

这个链接来爬取微信公众号腾讯玄武实验室。按照往常一样,我们会打开浏览器 f12 来审查元素。定位到最近文章的链接如下:

但是我们拿着这个链接用 postman 访问的时候,得到的并不是我们想要的文章详情页,而是一段 js 代码,如下图:

当然你也可以在代码中去执行这段 js 代码得到新的链接,不过你还要去获取前一个页面得到的 cookie,不然就会触发搜狗的验证码验证,这样操作就比较复杂了,所以我们可以采用无头浏览器来解决反爬问题。在 golang 中使用无头浏览器 headless chrome,就需要 chromedp 这个开源库。

chromedp 介绍

  • ##### chromedp 是什么

chromedp GitHub 开源地址:https://github.com/chromedp/chromedp

chromedp 官方介绍:chromedp 是一种更快,更简单的方法,可以在 Go 中驱动支持Chrome DevTools 协议的浏览器 而无需外部依赖(例如 Selenium 或 PhantomJS)。

  • chromedp 能做什么
    • 解决反爬虫问题
    • 网站自动化测试
    • 网页截图
    • 解决类似 VueJS 和 SPA 之类的渲染
    • 模拟点击事件 (刷点击量)
  • chromedp 的使用

chromedp 安装

go get -u github.com/chromedp/chromedp

chromedp 如何使用官方已经给了很详细的文档了,而且还给了很多示例代码。

GoDoc: https://godoc.org/github.com/chromedp/chromedp

Examples: https://github.com/chromedp/examples

这里先给出一个 google 搜索网页截图的 demo:

package main

import (
   "context"
   "fmt"
   "io/ioutil"
   "time"

   "github.com/chromedp/chromedp"
)

func main() {
   // 参数设置
   options := []chromedp.ExecAllocatorOption{
      chromedp.Flag("headless", false),
      chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
   }
   options = append(chromedp.DefaultExecAllocatorOptions[:], options...)
   allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), options...)
   defer cancel()

   // 创建chrome示例
   ctx, cancel := chromedp.NewContext(allocCtx)
   defer cancel()
   ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
   defer cancel()

   var (
      buf   []byte
      value string
   )
   err := chromedp.Run(ctx,
      chromedp.Tasks{
         // 打开导航
         chromedp.Navigate("https://google.com/"),
         // 等待元素加载完成
         chromedp.WaitVisible("body", chromedp.ByQuery),
         // 输入chromedp
         chromedp.SendKeys(`.gLFyf.gsfi`, "chromedp", chromedp.NodeVisible),
         // 打印输入框的值
         chromedp.Value(`.gLFyf.gsfi`, &value),
         // 提交
         chromedp.Submit(".gLFyf.gsfi", chromedp.ByQuery),
         chromedp.Sleep(3 * time.Second),
         // 截图
         chromedp.CaptureScreenshot(&buf),
      },
   )
   if err != nil {
      fmt.Println(err)
   }
 fmt.Println("value: ", value)
   if err := ioutil.WriteFile("fullScreenshot.png", buf, 0644); err != nil {
    fmt.Println(err)
   }
}

运行得到的截图如下:

使用的基本步骤介绍:

  1. 参数配置

    // 参数配置
    options := []chromedp.ExecAllocatorOption{
       chromedp.Flag("headless", false), // 是否打开浏览器调试
       chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),         // 设置UserAgent
     chromedp.ProxyServer("socks5://127.0.0.1:9050"), // 设置代理
    }
     options = append(chromedp.DefaultExecAllocatorOptions[:], options...)
    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), options...)
     defer cancel()
    

    默认的参数配置

  2. 创建 chrome 实例

    // 创建chrome实例
     ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()
     // 设置超时时间
    ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
    defer cancel()
    
  3. 执行

    var body string
    if err := chromedp.Run(ctx,
       chromedp.Tasks{
          // 打开导航
          chromedp.Navigate(_url),
          // 等待元素加载完成
          chromedp.WaitVisible("body"),
          // 延迟2秒
          chromedp.Sleep(2 * time.Second),
          // 点击事件
          chromedp.Click(`a[uigs="account_article_0"]`, chromedp.NodeVisible),
          chromedp.Sleep(3 * time.Second),
          // 获取html
          chromedp.OuterHTML("html", &body, chromedp.ByQuery),
     },
    ); err != nil {
       log.Printf("[scrapeNewArticle] chromedp Run fail,err: %s", err.Error())
       return
    }
    

    注:元素选择器不熟悉的可以参考https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector

    你也可以在 console 里验证是否选中元素,如下:

Run 函数第二个参数是 action 切片,一些常用的 action 还有如下几个:

  • Sendkeys 往输入框中输入值,比如我们在 google 搜索 chromedp 就可以这样写
// 输入chromedp
chromedp.SendKeys(`.gLFyf.gsfi`, "chromedp", chromedp.NodeVisible),
// 打印输入框的值
chromedp.Value(`.gLFyf.gsfi`, &value),
// 提交
chromedp.Submit(".gLFyf.gsfi", chromedp.ByQuery),
  • Screenshot 网页截图
chromedp.Screenshot("#main", &buf, chromedp.NodeVisible, chromedp.ByID),
  • 获取 cookie
// 获取cookie
   chromedp.ActionFunc(func(ctx context.Context) error {
      cookies, err := network.GetAllCookies().Do(ctx)
      if err != nil {
         return err
      }
      for i, v := range cookies {
         cookie += v.Name + "=" + v.Value
         if i != len(cookies)-1 {
            cookie += "; "
         }
      }
      return nil
   }),

其它一些 action 以及 action 的详细介绍可以查看官方文档。

微信公众号爬取

首先点击第一个页面的每日安全动态推送(09-15)进行跳转,并且打开第二个 tab 页,也就是文章详情页。

var body string
if err := chromedp.Run(ctx,
   chromedp.Tasks{
      // 打开导航
      chromedp.Navigate(_url),
      // 等待元素加载完成
      chromedp.WaitVisible("body"),
      // 延迟2秒
      chromedp.Sleep(2 * time.Second),
      // 点击事件
      chromedp.Click(`a[uigs="account_article_0"]`, chromedp.NodeVisible),
      chromedp.Sleep(3 * time.Second),
      // 获取html
      chromedp.OuterHTML("html", &body, chromedp.ByQuery),
   },
); err != nil {
   log.Printf("[scrapeNewArticle] chromedp Run fail,err: %s", err.Error())
   return
}

当使用搜狗搜索爬取腾讯玄武实验实的时候会遇到一个问题,问题就是我们会打开两个 tab 页,而且我们要的数据是在第二 tab 页中。所以我们需要创建第二个 tab 页的实例,具体解决办法如下,分为两步,第一步我们通过 ListenTarget 监听得到第二个 tab 页的 target ID,然后第二步拿着这个 target ID 创建新的 chrome 实例,在这个新的实例下执行任务,就可以获取到我们想要的 html 元素。

// 监听得到第二个tab页的target ID
ch := make(chan target.ID, 1)
chromedp.ListenTarget(ctx, func(ev interface{}) {
   if ev, ok := ev.(*target.EventTargetCreated); ok &&
      // if OpenerID == "", this is the first tab.
      ev.TargetInfo.OpenerID != "" {
      ch <- ev.TargetInfo.TargetID
   }
})
// 第二个tab页
newCtx, cancel := chromedp.NewContext(ctx, chromedp.WithTargetID(<-ch))
defer cancel()

获取文章详情页的 html。

if err := chromedp.Run(
   newCtx,
   chromedp.Sleep(1*time.Second),
   chromedp.OuterHTML("#js_content", &html, chromedp.ByID),
); err != nil {
   log.Printf("[scrapeNewArticle] chromedp Run fail,err: %s", err.Error())
   return
}

获取到文章列表的 html 之后,我们就可以使用正则去得到文章,正则如下,一个是链接的正则,一个是文章标题的正则。

_linkReg  = `<p style="text-align: left;"><span style="font-size: 16px;">•&nbsp;<span style=" box-sizing: border-box; width: 100%;padding-right: 5px;padding-left: 5px;flex-basis: 0px;flex-grow: 1;max-width: 100%; ">.+?<a href=".+?" rel="nofollow" style="box-sizing: border-box;color: rgb\(0, 123, 255\);" data-linktype="2"><br style="box-sizing: border-box;">(.+?)</a></span></span></p>`
_titleReg = `<p style="box-sizing: border-box;margin-top: 0\.25rem !important;margin-bottom: 0\.25rem !important;text-align: left;"><small style="box-sizing: border-box;font-size: 12\.8px;"><span style="font-size: 16px;">&nbsp;&nbsp;&nbsp;・</span></small><span style="font-size: 16px;">&nbsp;</span><q style="box-sizing: border-box;"><span style="font-size: 16px;">(.+?)</span>`
)

执行结果:

完整代码 github 地址:

https://github.com/wxj95/chromedpdemo

总结

我们可以使用无头浏览器来绕过一些 js 和 cookie 的反爬。这里不建议太频繁的去抓取文章,因为这样就很有可能触发搜狗的反爬机制,你可能会遇到需要输入验证码的情况,不过你也可以训练搜狗的验证码做个验证码的识别,毕竟我们也是可以使用无头浏览器进行模拟输入点击的。😅

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 09月17日 16:44
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册