我准备在一个图片站上抓点图,但发现它启用了 DDos 保护。站点会首先显示一段文本:

This process is automatic. Your browser will redirect to your requested content shortly.

要求你等待几秒钟检测浏览器,然后通过 302 重定向跳转到正确的页面(当然,这个正确的页面地址依然没变)。

等待的过程表现在浏览器上是这样的:

im_under_attack_page

这个保护的详细说明在这里: CloudFlare advanced DDoS protection

让我们看看怎么来解决这个问题。

源码

打开页面看源码,可以看到这样的 javascript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function(){
var a = function() {try{return !!window.addEventListener} catch(e) {return !1} },
b = function(b, c) {a() ? document.addEventListener("DOMContentLoaded", b, c) : document.attachEvent("onreadystatechange", b)};
b(function(){
var a = document.getElementById('cf-content');a.style.display = 'block';
setTimeout(function(){
var t,r,a,f, cgksoUW={"bRM":+((+!![]+[])+(!+[]+!![]))};
t = document.createElement('div');
t.innerHTML="<a href='/'>x</a>";
t = t.firstChild.href;r = t.match(/https?:\/\//)[0];
t = t.substr(r.length); t = t.substr(0,t.length-1);
a = document.getElementById('jschl-answer');
f = document.getElementById('challenge-form');
;cgksoUW.bRM-=+((!+[]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]));cgksoUW.bRM*=+((!+[]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![]));cgksoUW.bRM*=+((!+[]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]));cgksoUW.bRM+=+((!+[]+!![]+[])+(!+[]+!![]));cgksoUW.bRM*=+((!+[]+!![]+!![]+[])+(+[]));a.value = parseInt(cgksoUW.bRM, 10) + t.length;
f.submit();
}, 5000);
}, false);
})();

这是 JS 中出现的几个相关 Element:

1
2
3
4
5
6
7
8
<p data-translate="process_is_automatic">This process is automatic. Your browser will redirect to your requested content shortly.</p>
<p data-translate="allow_5_secs">Please allow up to 5 seconds&hellip;</p>
</div>
<form id="challenge-form" action="/cdn-cgi/l/chk_jschl" method="get">
<input type="hidden" name="jschl_vc" value="c841963d655c6bb040c4ef02fab8d5a3"/>
<input type="hidden" name="pass" value="1441270501.274-WM96e3sQx+"/>
<input type="hidden" id="jschl-answer" name="jschl_answer"/>
</form>

分析

上面代码的作用,主要是原来判断访问站点的客户端是否是一个真实的浏览器。

访问网站主页的时候,首先得到的是一个 503 错误,显示上面的页面内容。然后等待 5 秒,让用户看清楚提示信息,然后再根据得到的随机值东拼西凑计算出另一个值,写入 jschl-answer 中,最后通过 challenge-form 这个表单提交。

提交成功之后, /cdn-cgi/l/chk_jschl 会使用 302 重定向返回真实的网页内容。

cgksoUW 是一个随机的名称,pass 那个 input 中包含的值也是一个变化的值。每次进入这个页面,这两个值都会变化。

那么,pass 的值在提交的时候到底是如何计算出来的?

f = document.getElementById('challenge-form'); 下面的那段代码可以看出,这个计算无非是对 cgksoUW.bRM 进行一些加乘运算。然后再转成一个 Int 进行提交。关键在于那些看起来莫名其妙的加号、括号和方括号的集合的含义为何?

这是一个简单的障眼法。

让我们看看 cgksoUW.bRM 的初始值,这个表达式的结果等于数字 12 :

1
+((+!![]+[])+(!+[]+!![]))

我们把上面的代码拆成几个子表达式,就好理解了。

首先是第一个嵌套括号中的: (+!![]+[]),打开浏览器的 console ,在其中输入 +!![] 回车,可以得到数字 1 。

先不管哪个加号,单独看 !![] 的值,它是布尔值 true 。这是因为 [] 返回的是一个有效的 Array,代表 true ,两次取反,依然是 true

而左边的那个加号由于没有提供左操作数,因此它的含义是正数,这里做了一次数字转换,自动把 true 转换成了 1 。

接着往后算,后面是个加号,然后是个 [] 。空的数组,默认是作为字符串处理的。[].toString() 的值为空字符串。前面的 1 加上后面的空字符串,得到一个字符串 "1"

知道了原理,可以使用同样的方式算出第二个嵌套括号中的值为数字 2 。

接下来是字符串 "1" 加上数字 2,得到字符串 "12"

最左边的加号又做了一次转换,将字符串 "12" 转换成了数字 12 。

所以,这一段看似乱码的代码,只是为了获取一些随机的数字,便于和服务器验证罢了。我判断这些数值都是有意义的,而且和服务器时间关联,用来判断访问网站的是不是标准的浏览器。

选择

分析完之后,就要进行技术的选择了。

写爬虫自然是首选 Python 。开始,我准备使用 requests 来模拟请求,使用 BeautifulSoup4 来解析得到的 HTML,然后进行资源的下载。

但 requests 并不擅于模拟浏览器的行为,也不能执行 Javascript,还需要用 PyV8 等库来执行上面的数据计算,得到最终的 pass 。

PyExecJS 可以实现在 Python 环境中调用 Javascript 引擎,支持的引擎有 PyV8Node.js 等等。

成功跳过 DDos 防护之后,还需要保持 cookies,构建 heads 。

既然计算部分要依赖别的引擎解析 Javascript ,那还不如直接使用 Javascript 来写好了。

这就是 Headless Browser 大显身手的时候了。

Headless Browser

Headless Browser ,通俗地来说就是个无界面且完整功能的浏览器。目前最流行的 Headless Browser 框架非 PhantomJS 莫属。它是基于 WebKit 开发的,想要整个网页截图神马的(再长也不怕!)用它就最好了。

不过,用 PhantomJS 保存网页中的图片 还真是有点麻烦。由于上面的 DDOS 防护的存在,每个单独的图像文件依然存在防护,所以不能拿到地址后直接下载,必须使用浏览器来浏览这个图片。

于是我选择了另一个框架 SlimerJS ,它和 PhantomJS 功能基本一致,API 也基本一致,所不同的就是它使用 Mozilla Firefox 的 Gecko 引擎。

SlimerJS 的方便之处就在于它的 OnResourceReceived 回调支持 body 这个属性,其中包含正在访问的页面资源(页面中的 CSS、JS、Image 或者页面本身都算资源)的内容。我只需要简单地把它写入到文件中就能得到我需要的图片了。就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function onResourceReceived(response)
{
if(state != 'image' || response.stage!="end" || response.stage == "fail" || !response.bodySize) return;
console.log('[onResourceReceived] id: '+response.id + ' starge:' + response.stage + ' contentType:' + response.contentType+' url:'+response.url);
var curGalleryObj = galleryList[curGallery];
var curImageObj = curGalleryImages[curImage];

if(!fs.exists(curGalleryObj.dir))
{
fs.makeTree(curGalleryObj.dir);
}
var fname = curGalleryObj.dir+"/"+curImage+'.'+curImageObj.ext;
curImageObj.file = fname;
curImageObj.done = true;

fs.write(fname, response.body, 'b');
}

下载流程

下面我挑选下载流程中的一些比较重要的实现贴出来,其实很简单的啦。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
function getGalleryList(htmlFile, callback)
{
//htmlFile 是个本地的 HTML 文件,这是为了方便
var file = htmlDir + '/' + htmlFile + '.html';
var p = WebPage.create();
//不解析这个文件中的javascript 因为有些 js 保存在 google 的服务器上,下载很慢
p.settings.javascriptEnabled = false;
//不再如这个文件中的图像,原因同上
p.settings.loadImages = false;
p.open(file, function(status)
{
//在页面中调用 DOM 函数或者 DOM 列表
var list = p.evaluate(function()
{
return document.querySelectorAll('.content .gallery-list div a');

var newList = [];
//根据列表构建需要下载的 Gallery 列表
for(var i=0;i<list.length;i++)
{
var href = list[i].href.slice(8);
var arr = href.split('/');
var obj = {};
obj.url = href;
obj.id = arr[1];
obj.name = arr[2].split('.')[0];
obj.dir = imageDir+'/['+obj.id+']'+obj.name;
obj.index = site + '/images/' + obj.id + '/' + obj.name + '.html';
obj.progress = obj.dir + '/progress.json';
newList[i] = obj;
}
callback(newList);
});
}

function onIndexOpen(status)
{
console.log('[onIndexOpen]');
state = 'index';
var intervalId = null;
var t = 0;
var _checkTimeout = function()
{
if(t > 60)
{
console.log('!!!!Timeout!!!!');
clearInterval(intervalId);
slimer.exit();
}
}

var _checkTitle = function()
{
console.log('[_checkTitle]');
console.log('['+t+'] Check Title...'+page.title);
t++;
_checkTimeout();
//若出现正确的标题则代表载入成功
if(page.title.indexOf('Welcome') == 0)
{
clearInterval(intervalId);
t = 0;
page.close();
openGallery();
}
}

intervalId = setInterval(_checkTitle, 1000);
}

function openGallery()
{
console.log('[openGallery '+curGallery+']');
if(curGallery >= galleryList.length)
{
console.log('==== Download all Gallery Done! ====');
slimer.exit();
}
curImage = 0;
var curGalleryObj = galleryList[curGallery];
//本地已有列表,不必再获取
if(fs.exists(curGalleryObj.progress))
{
curGalleryImages = JSON.parse(fs.read(curGalleryObj.progress, 'r')).images;
//本地已经有这个文件了(上次下载成功),下一个
while(curGalleryImages[curImage] && curGalleryImages[curImage].done)
{
console.log('The file '+curGalleryImages[curImage].file + ' is downloaded.');
curImage++;
}
//开始下载文件
downloadImage();
}
else
{
state = 'gallery';
page.open(curGalleryObj.index, onGalleryOpen);
}
}

function onGalleryOpen(status)
{
console.log('[onGalleryOpen '+curGallery+']');
//script 标签中保存了 JSON 格式的图像列表,直接作为字符串获取它们
//这样就不必翻页分析 DOM 了
var script = page.evaluate(function()
{
return document.querySelector('body > div.outer-wrapper.image-page > div.page-wrapper.page-wrapper-full > script');
});
if(script)
{
var re = /\"images\":(.+\])\}\);/i;
var images = script.innerHTML.match(re)[1];
curGalleryImages = JSON.parse(images);
curGalleryImages.forEach(function(e,i,a)
{
var name = e.f.split('.');
a[i].ext = name[1];
a[i].name = name[0];
});
curImage = 0;
downloadImage();
}
else
{
console.log('NO SCRIPT');
slimer.exit();
}
}

function downloadImage()
{
if(curImage >= curGalleryImages.length)
{
var curGalleryObj = galleryList[curGallery];
console.log('==== download '+curGalleryObj.dir +' has done. ====');
//下载下一个 gallery
curGallery ++;
openGallery();
return;
}
state = 'image';
var image = getCurImage();
console.log('[downloadImage]'+image);
//关闭当前的 page
page.close();
//重用资源
page.open(image);
}

// 检测文件是否是标准的 JPEG
function checkJPEG(path)
{
console.log('[checkJPEG '+path+ ' '+fs.exists(path)+']');
if(fs.exists(path))
{
var content = fs.read(path, 'rb');
if( content &&
//content.length > 10240 &&
content.charCodeAt(0) == 0xff &&
content.charCodeAt(1) == 0xd8 &&
content.charCodeAt(2) == 0xff &&
content.charCodeAt(3) == 0xe0)
{
return true;
}
}
return false;
}

function onLoadFinished(status, url, isFrame)
{
loading = false;
console.log('==== ['+state+']onLoadFinished Loading page('+url+') '+ status + " loading:"+loading);
if(state == 'image')
{
var curGalleryObj = galleryList[curGallery];
var curImageObj = curGalleryImages[curImage];
var progressObj = {'gallery':curGalleryObj, 'images':curGalleryImages};

// 判断 JPEG 文件头,看看是否需要重新下载
if(checkJPEG(curImageObj.file))
{
curImageObj.done = true;
curImage++;
}
else
{
curImageObj.done = false;
}
// 将当前的下载进度写入到配置文件中,以便一次下载不完,
// 或异常退出后。下次下载时可以知道进度
fs.write(curGalleryObj.progress, JSON.stringify(progressObj), 'w');
//下载下一个文件,或者重新下载
downloadImage();
}
}

function onLoadStarted()
{
loading = true;
var currentUrl = page.evaluate(function() {
return window.location.href;
});
console.log('==== ['+state+']onLoadStarted Current page(' + currentUrl + ') will gone. loading:'+loading);
};

function onUrlChanged(targetUrl) {
console.log('=== ['+state+']onUrlChanged New URL: ' + targetUrl+' loading:'+loading);
}

var WebPage = require('webpage');
var page = WebPage.create();
var loading = false;
var imageDir = 'images';
var htmlDir = 'html';
var site = 'http://examples.com';
var fs = require('fs');

var page = WebPage.create();
var galleryList = null;
var curGallery = null;
var curGalleryImages = null;
var curImage = 0;
var state = null;

page.onLoadStarted = onLoadStarted;
page.onLoadFinished = onLoadFinished;
page.onUrlChanged = onUrlChanged
page.onResourceReceived = onResourceReceived;
getGalleryList('favorites_0', function(list)
{
galleryList = list;
curGallery = 1;
page.open(site, onIndexOpen);
});

(全文完)

留言