携程eleven参数

前言

最近遇到了一个比较好玩的反爬--携程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);
View Code

 

2. 其次, 需要安装浏览器插件 油猴(英文名 tampermonkey),需要FQ。

 

 

 

 

点击应用后就会有 谷歌应用商店(需要FQ),然后搜索 油猴便可以了。

关于油猴的代码

// ==UserScript==
// @name         携程websocket
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://hotels.ctrip.com/hotel/beijing1
// @grant        none
// ==/UserScript==

(function() {
    var mess = document.getElementById("mess");
    if(window.WebSocket){
        ws = new WebSocket('ws://127.0.0.1:8014/');
        ws.onopen = function(e){
            // console.log("连接服务器成功");
            ws.send("Browser");
        }
        ws.onclose = function(e){
            console.log("服务器关闭");
        }
        ws.onerror = function(){
            console.log("连接出错");
        }

        ws.onmessage = function(e){
            var data = e.data;
            var execJS = document.getElementById("execJS");
            if (execJS){
                document.body.removeChild(execJS);
            }
            execJS = document.createElement("script");
            execJS.id = "execJS";
            execJS.innerHTML = data;
            document.body.appendChild(execJS);
        }

        }
    // Your code here...
})();
View Code

 

说明一下,为什么需要油猴?

使用油猴,使得js代码的运行环境直接就是携程的网页,而不是单独打开的页面。

(注意,携程的服务器每天验证的严格程度都不太一样。)

那天测试的时候,我是直接写了一个html文件的,然后本地打开。就可以直接用了。

如果没有装油猴,可以先试试我下面提供的html文件。如果验证没有通过,就需要使用油猴环境

 

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        #mess{text-align: center}
    </style>
</head>
<body>
    <script id="execJS"></script>
    <script>
        var mess = document.getElementById("mess");
        var execJS = document.getElementById("execJS");
        if(window.WebSocket){
            var ws = new WebSocket('ws://127.0.0.1:8010/');
            ws.onopen = function(e){
                // console.log("连接服务器成功");
                ws.send("Browser");
            }
            ws.onclose = function(e){
                console.log("服务器关闭");
            }
            ws.onerror = function(){
                console.log("连接出错");
            }

            ws.onmessage = function(e){
                var data = e.data;
                var execJS = document.getElementById("execJS");
                if (execJS){
                    document.body.removeChild(execJS);
                }
                execJS = document.createElement("script");
                execJS.id = "execJS";
                execJS.innerHTML = data;
                document.body.appendChild(execJS);
            }
            
        }
    </script>
</body>
</html>
View Code

 

返回的eleven参数是正确的,请求也成功了。但是今天测试时失败了,然后我对比了一下在携程的控制台下和我本地路径下的html的控制台的结果

# 21e3255d0f89cdf5c3a347d61e7dafbcf15db34f7afe97cda2b5a7ec578652ee_1965113742
# 21e3255d0f89cdf5c3a347d61e7cafbcf15db34f7afe97cda2b5a7ec578652ee_1965113417

如果不仔细看的话,还看不出来。最后的三位是不一样的,应该是对location的检测。

油猴的作用是在携程的网站打开时注入我们的js代码,然后接下来要运行的代码环境便是携程的了。这样产生的eleven参数便是正确的。

 

3. python代码的编写

python的作用其实是连接websocket服务,发送我们需要运行的js代码,node会帮我们将js代码传给前端页面(油猴插件)。

当js代码在携程的环境里运行完毕后,它会将eleven参数通过websocket传给node,node会把结果返回给我们。这样我们的py代码就能获取到eleven参数了。

import requests
import time
import datetime
import execjs
import os

from ws4py.client.threadedclient import WebSocketClient


class CG_Client(WebSocketClient):
    def opened(self):
        print("连接成功")
        # req = open("../a.js").read()
        self.send("Python")

    def closed(self, code, reason=None):
        print("Closed down:", code, reason)

    def received_message(self, resp):
        print("resp", resp)
        currentDate = time.strftime("%Y-%m-%d")
        today = datetime.datetime.now()  # 今天,如 "2020-05-11"
        last_time = today + datetime.timedelta(hours=-24)
        tomorrow = last_time.strftime("%Y-%m-%d")  # 明天,如 '2020-05-10'
        data = {
            "__VIEWSTATEGENERATOR": "DB1FBB6D",
            "cityName": "%E5%8C%97%E4%BA%AC",
            "StartTime": today,
            "DepTime": tomorrow,
            "RoomGuestCount": "1,1,0",
            "txtkeyword": "",
            "Resource": "",
            "Room": "",
            "Paymentterm": "",
            "BRev": "",
            "Minstate": "",
            "PromoteType": "",
            "PromoteDate": "",
            "operationtype": "NEWHOTELORDER",
            "PromoteStartDate": "",
            "PromoteEndDate": "",
            "OrderID": "",
            "RoomNum": "",
            "IsOnlyAirHotel": "F",
            "cityId": "1",
            "cityPY": "beijing",
            "cityCode": "010",
            "cityLat": "39.9105329229",
            "cityLng": "116.413784021",
            "positionArea": "",
            "positionId": "",
            "hotelposition": "",
            "keyword": "",
            "hotelId": "",
            "htlPageView": "0",
            "hotelType": "F",
            "hasPKGHotel": "F",
            "requestTravelMoney": "F",
            "isusergiftcard": "F",
            "useFG": "F",
            "HotelEquipment": "",
            "priceRange": "-2",
            "hotelBrandId": "",
            "promotion": "F",
            "prepay": "F",
            "IsCanReserve": "F",
            "k1": "",
            "k2": "",
            "CorpPayType": "",
            "viewType": "",
            "checkIn": today,
            "checkOut": tomorrow,
            "DealSale": "",
            "ulogin": "",
            "hidTestLat": "0%7C0",
            "AllHotelIds": "",
            "psid": "",
            "isfromlist": "T",
            "ubt_price_key": "htl_search_noresult_promotion",
            "showwindow": "",
            "defaultcoupon": "",
            "isHuaZhu": "False",
            "hotelPriceLow": "",
            "unBookHotelTraceCode": "",
            "showTipFlg": "",
            "traceAdContextId": "",
            "allianceid": "0",
            "sid": "0",
            "pyramidHotels": "",
            "hotelIds": "",
            "markType": "0",
            "zone": "",
            "location": "",
            "type": "",
            "brand": "",
            "group": "",
            "feature": "",
            "equip": "",
            "bed": "",
            "breakfast": "",
            "other": "",
            "star": "",
            "sl": "",
            "s": "",
            "l": "",
            "price": "",
            "a": "0",
            "keywordLat": "",
            "keywordLon": "",
            "contrast": "0",
            "PaymentType": "",
            "CtripService": "",
            "promotionf": "",
            "allpoint": "",
            "page_id_forlog": "102002",
            "contyped": "0",
            "productcode": "",
            "eleven": resp.data,
            "orderby": "3",
            "ordertype": "0",
            "page": "1",
        }
        headers = {
            "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
            "referer": "https://hotels.ctrip.com/hotel/shanghai2",

            "cookie": 请在此处写入你的cookie,因为携程会检测cookie的ip字段(经过混淆加密)
        }
        url = "https://hotels.ctrip.com/Domestic/Tool/AjaxHotelList.aspx"
        a = requests.post(url, data=data, headers=headers)
        print(a.text)

        # resp = json.loads(str(resp))
        # data = resp['data']
        # if type(data) is dict:
        #     ask = data['asks'][0]
        #     print('Ask:', ask)
        #     bid = data['bids'][0]
        #     print('Bid:', bid)


def getTime():
    return str(time.time()).replace(".", "")[0:13]


def getCallbackParam():
    f = open("./callback.js")
    context = execjs.compile(f.read())
    return context.call("getCallback")


def getContent():
    t = getTime()
    callback = getCallbackParam()
    print(callback)
    url = "https://hotels.ctrip.com/domestic/cas/oceanball?callback=%s&_=%s" % (
        callback,
        t,
    )
    headers = {
        "user-agent": "Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/16.2.2",
        "referer": "https://hotels.ctrip.com/hotel/shanghai2",
    }
    r = requests.get(url, headers=headers)

    code = (
        """
        window["%s"] = function (e) {
        var f = e();
        console.log(f);
        ws.send(f);
    };;
    """
        % callback
        + r.text
    )
    print(code)
    ws.send(code)


# getContent()

ws = None
try:
    ws = CG_Client("ws://127.0.0.1:8014/")
    ws.connect()
    getContent() # 如果想要多次请求,可在此处再写一个
    ws.run_forever()

except KeyboardInterrupt:
    ws.close()
View Code

 

 

python代码需要依赖一个callback.js文件,内容如下

// callback.js function e(e) { var t = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"], a = "CAS", o = 0 for (; o < e; o++) { var i = Math.ceil(51 * Math.random()); a += t[i] } return a } function getCallback() { return e(15); }

 

 

4. 代码的启动顺序

1. 启动node websocket服务 (node app.js)

2. 刷新携程网页,F12后查看是否连接上了node websocket服务

3. 启动python代码

 

5. 注意事项

如果有端口占用错误(如果是mac,这个现象很正常,可以npm i nodemon, 然后nodemon app.js 启动。这样我们只要保存app.js,就会重启)

如果python代码运行后一直收不到结果,可以先看看node有没有报错,然后刷新下携程的页面

 

6. 关于运行速度

基本就是浏览器运行js脚本的速度,(浏览器引擎的解释速度可能比node快很多,毕竟浏览器专门做这个的)。

只有中间websocket通信的时耗,并且websocket是复用的,不是用一次就连接一次。

 

7. 关于canvas指纹

如果大量采集的话,会是一样的canvas指纹。可以选择hook canvas相关的api。

 

8. 关于爬取评论的py代码

import requests
import time
import datetime
import execjs
import os

from ws4py.client.threadedclient import WebSocketClient

callback = ""


class CG_Client(WebSocketClient):
    def opened(self):
        print("连接成功")
        # req = open("../a.js").read()
        self.send("Python")

    def closed(self, code, reason=None):
        print("Closed down:", code, reason)

    def received_message(self, resp):
        global callback
        print("resp", resp.data)
        headers = {
            "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.35",
            "referer": "https://hotels.ctrip.com/hotel/shanghai2",
            "cookie": 请在此处输入你的cookie,
        }
        eleven = resp.data
        params = {
            "MasterHotelID": "608516",
            "hotel": "608516",
            "NewOpenCount": "0",
            "AutoExpiredCount": "0",
            "RecordCount": "1659",
            "OpenDate": "",
            "card": "-1",
            "property": "-1",
            "userType": "-1",
            "productcode": "",
            "keyword": "",
            "roomName": "",
            "orderBy": "2",
            "currentPage": "2",
            "viewVersion": "c",
            "contyped": "0",
            "eleven": "",
            "callback": callback,
            "_": str(time.time()).replace(".", "")[0:13],
        }
        
        comment_url = (
            "https://hotels.ctrip.com/Domestic/tool/AjaxHotelCommentList.aspx?"
        )
        r = requests.get(comment_url, params=params, headers=headers)
        print(r.url)
        print(r.text)
        # a = requests.post(url, data=data, headers=headers)
        # print(a.text)

        # resp = json.loads(str(resp))
        # data = resp['data']
        # if type(data) is dict:
        #     ask = data['asks'][0]
        #     print('Ask:', ask)
        #     bid = data['bids'][0]
        #     print('Bid:', bid)


def getTime():
    return str(time.time()).replace(".", "")[0:13]


def getCallbackParam():
    # f = open("./callback.js")
    callbackCode = """
        function e(e) {
            var t = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"], a = "CAS", o = 0
            for (; o < e; o++) {
                var i = Math.ceil(51 * Math.random()); a += t[i]
            }
            return a
        }
        function getCallback() {
            return e(15);
        }
    """
    context = execjs.compile(callbackCode)
    return context.call("getCallback")


def getContent():
    global callback
    t = getTime()
    callback = getCallbackParam()
    print(callback)
    url = "https://hotels.ctrip.com/domestic/cas/oceanball?callback=%s&_=%s" % (
        callback,
        t,
    )
    headers = {
        "user-agent": "Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/16.2.2",
        "referer": "https://hotels.ctrip.com/hotel/shanghai2",
        "cookie": 请在此处输入你的cookie,
    }
    r = requests.get(url, headers=headers)

    code = (
        """
        window["%s"] = function (e) {
        var f = e();
        console.log(f);
        ws.send(f);
    };;
    """
        % callback
        + r.text
    )
    # print(code)
    ws.send(code)

    # open("a.js", "w").write(code)
    #
    # os.system("node a.js")


# getContent()

ws = None
try:
    ws = CG_Client("ws://127.0.0.1:8014/")
    ws.connect()
    getContent()
    ws.run_forever()

except KeyboardInterrupt:
    ws.close()

View Code

 

运行成功的截图

 

 

 

posted @ 2020-05-10 22:41  re大法好  阅读(3307)  评论(5编辑  收藏  举报