使用chromedp解决反爬虫问题

9,643 阅读4分钟
原文链接: lailin.xyz

前言

最近We川大上的教务处公告新闻已经很久没有更新了,想到可能是ip被封了,查了一下log,发现并不是,而是获取到的页面全变成了混淆过的js,下面放两个格式化的函数

function _$Es(_$Cu) {
    _$Cu[14] = _$v9();
    _$Cu[_$yf(_$ox(), 16)] = _$Dn();
    var _$cR = _$CR();
    _$cR = _$iT();
    return _$DA();
}

function _$Dk(_$Cu) {
    var _$x5 = _$Dv();
    var _$x5 = _$EB();
    if (_$Ex()) {
        _$w9 = _$Dw();
    }
    _$Cu[_$yf(_$EJ(), 16)] = _$ED();
    _$Cu[_$yf(_$Ep(), 16)] = _$EP();
    _$w9 = _$EB();
    return _$Cu[_$yf(_$v9(), 16)];
}

function _$rK() {
    var _$aJ = _$c0(_$DN());
    _$aJ = _$BC(_$aJ, 2);
    var _$Ce = _$yr(_$qt());
    for (var _$Cu = 0; _$Cu < _$aJ[_$gX()]; _$Cu++) {
        _$aJ[_$Cu] = _$Ce + _$aJ[_$Cu];
    }
    return _$aJ;
}

看着这一堆就头大,但是本着只要是浏览器能够渲染出来的页面爬虫就可以爬到的原则,一步一步的解决

分析

  1. 先使用postman发送了一下请求,发现返回了上面一堆乱码
  2. 复制了正常渲染页面request header重新发送请求,可以得到正常的页面。考虑两个可能一个是header有什么特殊的处理,一个是cookie上的问题。
  3. header其他内容不变,去掉cookie重新发送请求,再一次得到一堆乱码。问题定位成功,应该就是cookie的问题了
  4. 清空chrome的缓存,重新加载页面,查看请求记录,可以看到这个页面一共加载了两次 第一次加载 第一次加载没有返回cookie 第二次加载 第二次加载返回了一个JSESSIONID,这个应该就是最终需要的cookie了
  5. 观察两次请求的中间,我们可以发现还有两个请求,这两个请求应该就是第二次返回cookie的原因了,第一个请求是页面内的外链js文件,第二个请求应该就是混淆过的js发出的请求了。
  6. 因为实力有限,分析了几个小时都没有分析出来这个逻辑是怎么加载的。但是想到了直接从浏览器把cookie复制下来给爬虫使用不就可以了?但是这样也还有一个问题,就是不可能每一次都手动的去获取cookie这样达不到想要的效果。然后看到Python有使用Selenium来完全模拟浏览器渲染然后解析页面的爬虫案例,找了一下golang有没有类似的浏览器渲染方案,在万能的gayhub上找到了chromedp。下面使用chromedp来解决这个问题。

chromedp

Package chromedp is a faster, simpler way to drive browsers (Chrome, Edge, Safari, Android, etc) without external dependencies (ie, Selenium, PhantomJS, etc) using the Chrome Debugging Protocol.

1.install(建议使用梯子)
go get -u github.com/chromedp/chromedp
2.code

运行下面这一段代码可以看到chrome会弹出一个窗口并且运行网页,最后在console输出期望的html,但是我们其实只需要得到正确的cookie,用来之后爬取网页使用。如果所有的页面都需要等待chrome渲染结束之后爬取,那么效率实在是太低了

package main

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

	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/chromedp"
)

func main() {
	var err error

	// create context
	ctxt, cancel := context.WithCancel(context.Background())
	defer cancel()

	// create chrome instance
	c, err := chromedp.New(ctxt, chromedp.WithLog(log.Printf))
	if err != nil {
		log.Fatal(err)
	}

	// run task list
	var res string
	err = c.Run(ctxt, chromedp.Tasks{
        // 访问教务处页面
		chromedp.Navigate(`http://jwc.scu.edu.cn/jwc/moreNotice.action`),
        // 等待table渲染成功,成功则说明已经获取到了正确的页面
        chromedp.WaitVisible(`table`, chromedp.ByQuery),
        // 获取body标签的html字符
		chromedp.OuterHTML("body", &res),
	})
	if err != nil {
		log.Fatal(err)
	}

	// 关闭chrome实例
	err = c.Shutdown(ctxt)
	if err != nil {
		log.Fatal(err)
	}

	// 等待chrome实例关闭
	err = c.Wait()
	if err != nil {
		log.Fatal(err)
	}

    // 输出html字符串
	log.Printf(res)
}
3.获取cookie

修改第2步当中task list 的代码获取cookie,修改之后可以看到console当中输出了一段cookie字符串,使用这个cookie在postman当中测试可以发现,可以获取到正确的页面。到了这一步其实就应该算基本完成了,但是还是有一个缺点:每次运行的时候都会弹出一个chrome窗口,爬虫在服务器上运行是没有gui页面的,并且每次打开一个chrome实例的时间开销也比较大。

// 将chromedp.OuterHTML("body", &res) 替换为下面的代码
chromedp.ActionFunc(func(ctx context.Context, h cdp.Executor) error {
    // 获取cookie
    cookies, err := network.GetAllCookies().Do(ctx, h)

    // 将cookie拼接成header请求中cookie字段的模式
    var c string
    for _, v := range cookies {
        c = c + v.Name + "=" + v.Value + ";"
    }
    log.Println(c)

    if err != nil {
        return err
    }
    return nil
}),
5.使用chrome的headless模式

a.使用docker运行一个headless模式的chrome

docker run -d -p 9222:9222 --rm --name chrome-headless knqz/chrome-headless

b.修改代码

可以看到主要的区别就在创建chrome实例的时候没有去启动一个chrome,当然最后也不需要去关闭它

package main

import (
	"context"
	"log"

	"github.com/chromedp/chromedp/client"

	"github.com/chromedp/cdproto/network"

	"github.com/chromedp/cdproto/cdp"

	"github.com/chromedp/chromedp"
)

func main() {
	var err error

	// create context
	ctxt, cancel := context.WithCancel(context.Background())
	defer cancel()

	// create chrome instance
	c, err := chromedp.New(ctxt, chromedp.WithTargets(client.New().WatchPageTargets(ctxt)), chromedp.WithLog(log.Printf))
	if err != nil {
		log.Fatal(err)
	}

	// run task list
	err = c.Run(ctxt, chromedp.Tasks{
        // 访问教务处页面
		chromedp.Navigate(`http://jwc.scu.edu.cn/jwc/moreNotice.action`),
        // 等待table渲染成功,成功则说明已经获取到了正确的页面
        chromedp.WaitVisible(`table`, chromedp.ByQuery),
        // 获取body标签的html字符
		chromedp.ActionFunc(func(ctx context.Context, h cdp.Executor) error {
            // 获取cookie
            cookies, err := network.GetAllCookies().Do(ctx, h)

            // 将cookie拼接成header请求中cookie字段的模式
            var c string
            for _, v := range cookies {
                c = c + v.Name + "=" + v.Value + ";"
            }
            log.Println(c)

            if err != nil {
                return err
            }
            return nil
        }),
	})
	if err != nil {
		log.Fatal(err)
	}
}

到这里基本就可以使用了,获取到cookie之后可以使用喜欢的方式去获取页面