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

     

    运行成功的截图

     

  • 相关阅读:
    js给redio设置哪一个被选中
    问候struts2升级的新版本2.5
    图片上传与显示时候的路径问题
    Json,String,Map之间的转换
    springboot之web
    关于struts2中出现的漏洞,如何测试解决
    Table-valued functions and Scalar-valued functions in SQL Server
    Using system view: sys.sysprocesses to check SqlServer's block and deadlock
    利用sys.sysprocesses检查SqlServer的阻塞和死锁
    SQLSERVER加密解密函数(非对称密钥 证书加密 对称密钥)
  • 原文地址:https://www.cnblogs.com/re-is-good/p/xiecheng-eleven.html
Copyright © 2011-2022 走看看