zoukankan      html  css  js  c++  java
  • 爬虫笔记之电视猫节目单爬取

    难度: ★☆☆☆☆ 1星

    一、缘起

    目标站点: https://www.tvmao.com/program/CCTV-CCTV1-w3.html

    这个网站第一次接触是在17年刚毕业的时候在一家公司接手维护公司大佬写的项目,那个时候没做过爬虫,这是接触的第一个有JS反爬的网站,还是有些纪念意义的,一转眼几年过去了,网站的反爬策略貌似还是跟印象中差不多,而我似乎也没什么长进,我与君共蹉跎。

    二、分析

    打开一个节目单列表,比如这个页面:

    https://www.tvmao.com/program/CCTV-CCTV1-w3.html

    这个页面展示了CCTV1频道一天的节目单,上午的节目单它是随着页面doc返回的,这个没什么好搞的,而下午和晚上的节目单则是ajax懒加载,而这个ajax请求有一个加密参数p,本次就是要搞定这个参数加密。

    首先打开上面那个节目单的地址,然后打开开发者工具,切换到Network,把无关请求清除掉,然后单击页面上的“查看更多”加载更多节目单:

    0

    捕捉到了懒加载的ajax请求:

    1

    这个ajax请求的地址为:

    https://www.tvmao.com/api/pg?p=xxx

    切换到Sources,然后给这个url打一个xhr断点:

    2

    然后刷新页面,重新点“加载更多”,让它进入xhr断点,然后格式化代码向前追溯调用栈,在一个匿名函数的栈帧里找找到了传参数发请求的地方:

    3

    将鼠标悬停到86行的A.d上,然后单击弹出框里的地址跟进入:

    4

    注意到这个代码的标题框是VMxxx,这段代码可能是用了eval加密之类的,但我们已经拿到代码了,所以就不去管那些了。

    然后把这段代码拷贝出来,做个静态分析即可:

    var A = {
        _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
        _keyStr2: "KQMFS=DVGO",
    
        /**
         * 这个函数其实并没有看,扫了一眼看着像是base64,然后就在console上调用它加密一个字符串:
         * A.J("CC11001100")
         * 得到"Q0MxMTAwMTEwMA==",然后base64对它解码之后得到原字符串,证明这是一个标准的base64加密
         * 所以,折叠不看了...
         *
         * @param a
         * @returns {string|string}
         * @constructor
         */
        J: function (a) {
            var b = "";
            var c, chr2, chr3, enc1, enc2, enc3, enc4;
            var i = 0;
            a = A._C(a);
            while (i < a.length) {
                c = a.charCodeAt(i++);
                chr2 = a.charCodeAt(i++);
                chr3 = a.charCodeAt(i++);
                enc1 = c >> 2;
                enc2 = ((c & 3) << 4) | (chr2 >> 4);
                enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
                enc4 = chr3 & 63;
                if (isNaN(chr2)) {
                    enc3 = enc4 = 64
                } else if (isNaN(chr3)) {
                    enc4 = 64
                }
                b = b + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4)
            }
            return b
        },
    
        H: function (a) {
            a = a.toString();
            var b = '';
            for (var i = 0; i < a.length; i++) {
                b += this._keyStr2[a.charAt(i)]
            }
            for (var i = 0; i < a.length; i++) {
                b += this._keyStr[a.charAt(i)]
            }
            return b
        },
    
        _C: function (a) {
            a = a.replace(/
    /g, "
    ");
            var b = "";
            for (var n = 0; n < a.length; n++) {
                var c = a.charCodeAt(n);
                if (c < 128) {
                    b += String.fromCharCode(c)
                } else if ((c > 127) && (c < 2048)) {
                    b += String.fromCharCode((c >> 6) | 192);
                    b += String.fromCharCode((c & 63) | 128)
                } else {
                    b += String.fromCharCode((c >> 12) | 224);
                    b += String.fromCharCode(((c >> 6) & 63) | 128);
                    b += String.fromCharCode((c & 63) | 128)
                }
            }
            return b
        },
        E: function (a) {
            $(':input[name="ed"]', a).val(A.J('l' + $(".ed", a).val() + 'o'))
        },
        B: function (a) {
            var b = (new Date()).getTime();
            if (a != undefined)
                return A.J(a + '|' + b);
            else
                return A.J('' + b)
        },
    
        /**
         *
         * step 6:
         *
         * 返回页面上第一个form的a属性
         *
         * @param u
         * @returns {*}
         */
        e: function (u) {
            // u --> "a"
            // // document.querySelector("form").querySelector("input[class='baidu']")
            // 并没有选到东西...
            var x = 1;
            var f = $('form').first();
            var a = f.find("input[class='baidu']");
            if (a != undefined) {
                x = 2
            } else if (u != undefined) {
                x = u
            }
            if (f == undefined)
                return x;
            // 所以兜了半天最后返回的还是form的a属性
            // document.querySelector("form")
            // 30B972D97E1572D06EAA84CDA91A136DB0
            return f.attr('a')
        },
    
        /**
         *
         * step 5:
         * 这一步就是获取页面上第一个form的submit按钮的id属性
         *
         * @param e
         * @returns {*}
         */
        c: function (e) {
            var v;
            var f = $('form').first();
            if (f == undefined)
                return "";
            var s = f.find("*[type='submit']");
            if (s == undefined) {
                v = f.find("input[class='qq']");
                if (v == undefined)
                    return "";
                v = e
            }
            // 在console上模拟这个过程,选取这个元素:
            // document.getElementsByTagName("form")[0].querySelector("*[type='submit']");
            // 拿到其id属性为: A50CB26A1B14FFF05ECA58F9128FE059406FED4EFD
            v = s.attr('id');
            return v
        },
    
        /**
         *
         * step 2: 跟进来的是这个方法,但是实际上这里并不先被执行,先执行最下面的立即执行方法,然后执行这里
         *
         * @param p 本次调用是 "a"
         * @param h 本次调用是 "src"
         * @returns {string}
         */
        d: function (p, h) {
    
            // h --> "src"
            var v = A.w(h);
    
            // 混淆视听的,x在这两个地方的赋值根本没被用到
            var a = $("div.fix");
            var x = a || p;
            if (a != undefined) {
                x = h || $("s.fix1")
            }
            // 真正有用的赋值是这里
            // 获取到页面上第一个表单的submit按钮的id属性
            x = A.c();
    
            var b = new Date();
            var c = b.getUTCDate();
            var d = b.getDay();
            var i = d == 0 ? 7 : d;
            i = i * i;
            var F = this._keyStr.charAt(i);
    
            return F + A.J(x + "|" + A.e(p)) + v
        },
    
        /**
         * step 3:
         *
         * @param v
         */
        w: function (v) { // v --> "src"
            var t = $("head");
            var a = "|";
            if (t == undefined) {
                tl = "/"
            } else {
                tl = v
            }
    
            // tl --> "src"
            // A.J("|07BBCD432D5102A1B885F27E8988AAB4AC8BF81B26C74F565501E65C69")
            var r = A.J(a + k(tl));
            // r --> "fDA3QkJDRDQzMkQ1MTAyQTFCODg1RjI3RTg5ODhBQUI0QUM4QkY4MUIyNkM3NEY1NjU1MDFFNjVDNjk="
            return r
        },
    
        s: function (a, b) {
            var c = this._keyStr.charAt(37);
            return A.J(c + a)
        }
    };
    
    // step 1: 下面的这一段在js加载的时候就先执行
    
    
    // 只是定义了个k函数,在A.w里面调用了一下这个
    var k = function (a) {
        // step 4:
        // 就是获取页面上第一个form的q属性
        // 在console上执行 document.getElementsByTagName("form")[0];
        // 它的q属性是类似于这样的: 07BBCD432D5102A1B885F27E8988AAB4AC8BF81B26C74F565501E65C69
    
        var f = $('form').first();
        if (f == undefined)
            return "";
        var b = f.attr('id');
        if (b == undefined)
            f.attr('id', a);
        return f.attr('q')
    };
    
    // 然后是一个立即执行的函数,这个函数给一个表单及一些链接添加了ek参数,但是似乎也并没用到,先不管
    $(function () {
        //
        var b = $('<input type="hidden" name="ek"/>');
        b.val(A.B());
        $('form[name="frmlogin"]').append(b);
        $('a[class^="by"]').each(function () {
            var a = $(this).attr("href") + "&ek=" + encodeURIComponent(A.B());
            $(this).attr("href", a)
        })
    });
    

    逻辑很清晰了,就不需要扣代码,根据这些逻辑用python实现即可。

    三、编码实现

    #!/usr/bin/env python3
    # encoding: utf-8
    """
    @author: CC11001100
    """
    import base64
    import datetime
    from urllib.parse import quote
    
    import requests
    from bs4 import BeautifulSoup
    
    session = requests.session()
    
    
    def crawl(url):
        headers = {
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7",
            "Host": "www.tvmao.com",
            "Pragma": "no-cache",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
        }
        # 节目单的上半部分没有加密,这里不再解析
        html = session.get(url, headers=headers).text
    
        # for debug
        with open("./response-01.html", "w", encoding="UTF-8") as f:
            f.write(html)
    
        p = get_param_p(html)
        print(f"计算出 p = {p}")
    
        headers["Referer"] = url
        headers["X-Requested-With"] = "XMLHttpRequest"
        url = "https://www.tvmao.com/api/pg?p=" + quote(p)
        response = session.get(url, headers=headers).json()
    
        # for debug
        with open("./response-02.html", "w", encoding="UTF-8") as f:
            f.write(response[1])
    
        print(response)
    
    
    def get_param_p(html):
        doc = BeautifulSoup(html, features="html.parser")
        form = doc.select_one("form")
    
        d = datetime.datetime.now()
        week = d.weekday() - 1
        if week == 0:
            week = 7
        week = week * week
        f = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="[week]
    
        x = form.select_one("button[type=submit]")["id"]
        t1 = b64_s(x + "|" + form["a"])
    
        v = b64_s("|" + form["q"])
    
        return f + t1 + v
    
    
    def b64_s(s):
        """
        各种算算看的晕晕,为了避免混淆视听,将不重要内容尽量缩短
        :param s:
        :return:
        """
        return base64.b64encode(s.encode("UTF-8")).decode("UTF-8")
    
    
    if __name__ == "__main__":
        crawl("https://www.tvmao.com/program/CCTV-CCTV1-w3.html")
    


    仓库:

    https://github.com/CC11001100/misc-crawler-public/tree/master/001-anti-crawler-js-re/01-004-www.tvmao.com


    请注意爬虫文章具有时效性,本文写于2020-11-25日。

  • 相关阅读:
    保证测试通过的ip正则,antdIP/IP段的校验方法,antd的textArea中可以输入多个以换行分隔的ip/IP段,并自动检测出错行的原因
    TP5接口出错只能返回500
    UDP服务只能本机访问问题
    有出现了找半天的小BUG
    PHP本地安装redis扩展
    MYSQL数据库和es数据库同步
    QQ互联应用申请失败
    axios跨域问题解决
    elastic和kibana安装心得
    自增运算符理解
  • 原文地址:https://www.cnblogs.com/cc11001100/p/14038028.html
Copyright © 2011-2022 走看看