前言
最近遇到了一个比较好玩的反爬--携程eleven参数的生成。
说好玩的原因是请求一个接口后,会返回js代码,只要稍微调试下,便可以在浏览器上得到eleven参数了。
但如果想要在node或者无头浏览器之类的东西完成的话,只会报错。
(需要代码的大佬可以跳到最后(node环境+油猴+py, 通过websocket给油猴和py代码通信))
爬取目标
说一下我们要爬的数据吧。(如下内容) https://hotels.ctrip.com/hotel/beijing1#ctm_ref=hod_hp_sb_lst
这些数据来自于此接口
是一个post请求,在请求体中便是著名的eleven参数了。
如果eleven参数错误的话,是不会返回上面的数据的。
Eleven参数
前面说过了,Eleven参数来自于一段js代码(这段js代码是请求一个url后直接返回的)
下面便是那个所要请求的url
请求此url后返回的js代码
1. 测试返回的js代码
我们新开一个标签页,然后将返回的js代码复制到控制台执行。会发现报了一个错
应该是缺少了其它js代码造成的
// 这样可以产生一个与上图相似的错误 Function.prototype.toString.call(1);
我们还可以将返回的js代码复制到携程页面的控制台运行下,发现并没有报错,但我们的页面被重定向到了登陆页面
因此猜测返回的js代码的只能被执行一次(毕竟就是在携程的页面执行的。)
2. 下断点(看看返回的js代码是怎么运行的)
怎么下断点?搜索url中的关键字? 下xhr断点。
当时我用的方式是搜索url中的关键字, oceanball。那天是直接可以搜索到的。但今天没有了。
下xhr断点是不可能断下来的。因为他用到是jsonp来请求的。
那用啥方法呢?
看这个请求的发起者。
鼠标移到下面红色箭头指的位置便可以看到这个请求的发起者
点击第二个发起者(js @cQuery_110421.js:formatted:823那行)
为什么是第二个,因为第一个不行,可以自行尝试
如图 下一个断点(823行,也是第二个发起者代码执行行数)
我们其实也可以在 "欢迎度排序" 和 "好评优先" 之间来回切换,不然总是刷新的话,效率不高
如果页面刷新了,或者如上切换了选项的话。页面会在我们之前下的断点停住。
注意下右边的 call stack(调用栈)
我们如下图点击一下 ,来到上一层的执行环境。
往上翻一下,就会发现这部分便是那个url从发起请求到处理返回结果的所有细节
如下图所示
其实只要在返回的js代码里加上如下内容
// 这里的o是请求url中callback参数。
window[o] = function (e){
console.log(e()); // 这样便可以输出结果了。
}
// 下面是请求url后返回的js代码
这样代码便可以在浏览器中输出结果了
好了,重点部分来了。
3. 如何批量生成eleven参数
我不能说我手动复制到浏览器中运行,然后复制下结果吧。
在node环境中运行难度比较大,他会严格检测执行环境。
也别想断点调试。见过一个函数被调用18多万次吗?
是不是想问我是怎么知道这些的?
我是通过Object.defineProperty 劫持了 navigator.userAgent。
当这个js代码想要获取 navigator.userAgent 时,代码便会在此就会停住
Object.defineProperty(navigator, "userAgent", { get(){ debugger; return navigator.userAgent; } })
然后慢慢堆栈时发现某一个函数貌似便是用于检测环境的函数啥的。然后那个函数被调用了18万多次。
检测的内容非常多,不光是node环境,还有无头浏览器啥的,你听过的没听过的都有。
处理方法
已经有大佬在node环境中模拟了这个浏览器环境了,像我这种菜鸡,估计是难做到了。
我的想法很简单,还是通过浏览器执行那些js代码,但是需要自动执行。
vscode的自动保存便刷新页面的插件给了我启发,他是通过websocket进行通信,服务器会将最新的html传给客户端,客户端可以做一定的处理
1. 首先需要使用nodejs搭建一个websocket的环境,可以使用 nodejs-websocket 模块搭建
代码如下
需要安装下node环境(node官网下载,像装软件一样安装即可。)
var ws = require("nodejs-websocket"); console.log("开始建立连接...") var cached = { } var server = ws.createServer(function(conn){ conn.on("text", function (msg) { if (!msg) return; // conn.sendText(str) // console.log(str); if (msg.length > 1000){ console.log("msg 这是js代码") }else{ console.log("msg", msg); } var key = conn.key; if ((msg === "Browser") || (msg === "Python")){ // browser或者python第一次连接 cached[msg] = key; console.log("cached",cached); return; } console.log(cached, key); if (Object.values(cached).includes(key)){ console.log(server.connections.forEach(conn=>conn.key)); var targetConn = server.connections.filter(function(conn){ return conn.key !== key; }) console.log(targetConn.key); console.log("将要发送的js代码"); targetConn.forEach(conn=>{ conn.send(msg); }) } // broadcast(server, str); }) conn.on("close", function (code, reason) { console.log("关闭连接") }); conn.on("error", function (code, reason) { console.log("异常关闭") }); }).listen(8014) console.log("WebSocket建立完毕") // var server = http.createServer(function(request, response){ // response.end("ok"); // }).listen(8000);