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 官方介绍:chromedp 是一种更快,更简单的方法,可以在 Go 中驱动支持Chrome DevTools 协议的浏览器 而无需外部依赖(例如 Selenium 或 PhantomJS)。
-
chromedp 能做什么
- 解决反爬虫问题
- 网站自动化测试
- 网页截图
- 解决类似 VueJS 和 SPA 之类的渲染
- 模拟点击事件 (刷点击量)
-
chromedp 的使用
chromedp 安装
go get -u github.com/chromedp/chromedp
chromedp 如何使用官方已经给了很详细的文档了,而且还给了很多示例代码。
这里先给出一个 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)
}
}
运行得到的截图如下:
使用的基本步骤介绍:
// 参数配置
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()
// 创建chrome实例 ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() // 设置超时时间 ctx, cancel = context.WithTimeout(ctx, 15*time.Second) defer cancel()
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
}
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;">• <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;"> ・</span></small><span style="font-size: 16px;"> </span><q style="box-sizing: border-box;"><span style="font-size: 16px;">(.+?)</span>` )
执行结果:
完整代码 github 地址:
总结
我们可以使用无头浏览器来绕过一些 js 和 cookie 的反爬。这里不建议太频繁的去抓取文章,因为这样就很有可能触发搜狗的反爬机制,你可能会遇到需要输入验证码的情况,不过你也可以训练搜狗的验证码做个验证码的识别,毕竟我们也是可以使用无头浏览器进行模拟输入点击的。