Fork me on GitHub

爬虫笔记之电视猫节目单爬取

难度: ★☆☆☆☆ 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加密之类的,但我们已经拿到代码了,所以就不去管那些了。

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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(/\r\n/g, "\n");
        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实现即可。

三、编码实现

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/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日。

posted @   CC11001100  阅读(1837)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
历史上的今天:
2019-11-25 flash逆向练习:以逆向的方式通关flash游戏《谈判专家》
点击右上角即可分享
微信分享提示