爬虫笔记之电视猫节目单爬取
难度: ★☆☆☆☆ 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加密之类的,但我们已经拿到代码了,所以就不去管那些了。
然后把这段代码拷贝出来,做个静态分析即可:
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" ) |
仓库:
请注意爬虫文章具有时效性,本文写于2020-11-25日。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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游戏《谈判专家》