难度: ★☆☆☆☆ 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,把无关请求清除掉,然后单击页面上的“查看更多”加载更多节目单:
捕捉到了懒加载的ajax请求:
这个ajax请求的地址为:
https://www.tvmao.com/api/pg?p=xxx
切换到Sources,然后给这个url打一个xhr断点:
然后刷新页面,重新点“加载更多”,让它进入xhr断点,然后格式化代码向前追溯调用栈,在一个匿名函数的栈帧里找找到了传参数发请求的地方:
将鼠标悬停到86行的A.d上,然后单击弹出框里的地址跟进入:
注意到这个代码的标题框是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(/\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实现即可。
三、编码实现#!/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日。