使用chromedp解决反爬虫问题

前言

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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(建议使用梯子)
1
go get -u github.com/chromedp/chromedp
2.code

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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实例的时间开销也比较大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 将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

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

b.修改代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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之后可以使用喜欢的方式去获取页面