爬虫笔记之电视猫节目单爬取
难度: ★☆☆☆☆ 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")
仓库:
请注意爬虫文章具有时效性,本文写于2020-11-25日。


浙公网安备 33010602011771号