zoukankan      html  css  js  c++  java
  • 携程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

     

    运行成功的截图

     

  • 相关阅读:
    UVALive 5983 MAGRID DP
    2015暑假训练(UVALive 5983
    poj 1426 Find The Multiple (BFS)
    poj 3126 Prime Path (BFS)
    poj 2251 Dungeon Master 3维bfs(水水)
    poj 3278 catch that cow BFS(基础水)
    poj3083 Children of the Candy Corn BFS&&DFS
    BZOJ1878: [SDOI2009]HH的项链 (离线查询+树状数组)
    洛谷P3178 [HAOI2015]树上操作(dfs序+线段树)
    洛谷P3065 [USACO12DEC]第一!First!(Trie树+拓扑排序)
  • 原文地址:https://www.cnblogs.com/re-is-good/p/xiecheng-eleven.html
Copyright © 2011-2022 走看看