另辟蹊径js逆向爬取百度翻译
本文作者:张延丰(投稿)
文字编辑:孙晓玲
1.技术分析
2.爬取思路——寻找sign函数
我们整理一下解析的思路,其实通过输入固定的sign值也可以爬取当前页面的内容,但是我们不能爬取任意的网页内容,因此我们需要知道如何寻找登录的接口,知道如何确定js文件的位置,知道如何观察js的执行过程及方法,进而调出js代码来破解sign值。
网上有很多用JavaScript逆向爬取百度翻译的例子,概括一下通常包含两种方法:第一种是直接通过搜索请求url里的参数v2transapi来定位函数位置,乍一看挺突兀的,为什么会选择它来定位函数呢,后来想了一下,觉得可能是在写js脚本的时候会在构建url地址时,要将该参数写出来,因此可以通过其来定位函数位置。
第二种是通过搜索sign来定位函数,原因与上述一样。但是这种情况,往往是个例,在使用JavaScript逆向爬取中国空气质量在线监测平台的数据时,上述两种方法就完全失效了。首先,搜索url地址里的特殊参数,只有一个文件且并没有反馈出有用的信息,另外在通过其url地址中的唯一可变参数d来定位,那就更是大海捞针了。
经过之前的分析我们知道,我们捕获到的AJAX请求是通过点击了页面中输入词汇后的查询按钮后(其实一输入就可以呈现翻译,但我们必须得点击“翻译”,进而分析操作)触发的,也就是说该查询按钮上一定绑定了某个点击事件且触发了对应AJAX请求发送的事件。
那么接下来我们可以通过火狐浏览器(火狐浏览器可以分析页面某个元素的绑定事件以及定位到具体的代码在哪一行,Chrome也可以)去检测该查询按钮上到底绑定了哪些事件。
首先,我们通过在html定位翻译键发现,的确绑定有事件event,点击event发现有两个click事件:第一个显示为jQuery,因此我们通过分析第一个click事件的函数fuction()来解析翻译键的作用。点击jQuery左边的在调试器中打开并点击左下角的{},将页面调整为规范格式,得到以下页面:
定位到了,click事件的函数位置后,我们接下来分析函数function(){returne.apply(t || this, r.concat(it.call(arguments)))}。从函数中可以知道是返回,向对象e输入(t || this, r.concat(it.call(arguments))参数后的值,而apply.()中传入的第一个参数为新建对象(类似Python中,e为建立的类,第一个参数为对象),第二个参数为输入的值,即执行的是e(r)函数。因此我们在当前页面搜索function e,但并没有结果。
因此跳转network页面,全局搜索function e(在火狐中不知道原因打不了断点,因此以下操作在Chrome中执行),发现只有两处有e(r)函数,通过在第一个e(r)函数打断点发现其作用类似储存输入过的值,接下来,我们单击第二个e(r)的文件位置,并在函数中打断点,得到以下页面:
发现r为输入解释的内容,在标红的for行后面输出了p值630227,与我们在第一张图中的请求参数sign中数字的整数值一致,并且在最后一行标红内容中呈现为(A+‘.’+B)形式,判断为sign的浮点数形式,因此该段函数就是用于解析sign值的函数。
接下来我们将该函数function e(r)写入Pycharm中并命名为code.js,并另在一个Python文件写入运行函数。
def sign_number(query):
word = query
with open('code.js', 'r', encoding='utf-8') as f:
p = execjs.compile(f.read())#通过安装pyexecjs,输入execjs将JavaScript转换为Pyhon语言读取
sign = p.call('e', query)#调用e对象中的query值
return sign
发现输出结果显示,未定义i的值,多次运行不同的query发现i值固定为320305.131321201,因此直接定义变量var i = "320305.131321201"。再次运行函数呈现错误,未定义对象即r值,从上面截取r的函数录入code.js代码中,结果呈现正常。
Code.js代码如下:
function n(r, o) {
for (var t = 0; t < o.length - 2; t += 3) {
var a = o.charAt(t + 2);
a = a >= "a" ? a.charCodeAt(0) - 87 : Number(a),
a = "+" === o.charAt(t + 1) ? r >>> a : r << a,
r = "+" === o.charAt(t) ? r + a & 4294967295 : r ^ a
}
return r
}
var i = "320305.131321201"
function e(r) {
var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === o) {
var t = r.length;
t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
} else {
for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
"" !== e[C] && f.push.apply(f, a(e[C].split(""))),
C !== h - 1 && f.push(o[C]);
var g = f.length;
g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
}
var u = void 0
, l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
u = null !== i ? i : (i = window[l] || "") || "";
for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
var A = r.charCodeAt(v);
128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
S[c++] = A >> 18 | 240,
S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
S[c++] = A >> 6 & 63 | 128),
S[c++] = 63 & A | 128)
}
for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
p += S[b],
p = n(p, F);
return p = n(p, D),
p ^= s,
0 > p && (p = (2147483647 & p) + 2147483648),
p %= 1e6,
p.toString() + "." + (p ^ m)
}
接下来就正常的录入请求头,url,发起请求并呈现响应即可。全部代码如下:
import execjs
import requests
import json
def sign_number(query):#输入查询的词
word = query
with open('code.js', 'r', encoding='utf-8') as f:
p = execjs.compile(f.read())#通过安装pyexecjs,输入execjs将JavaScript转换为pyhon语言读取
sign = p.call('e', query)#调用e对象中的query值
return sign
def get_data(sign,query):
data={
'from': 'zh',#此模式为汉译英
'to': 'en',
'query': query,
'transtype': 'realtime',
'simple_means_flag': '3',
'sign': sign,
'token': '380c27bbe719bcb775945a4125b72721',
'domain': 'common'
}
headers = {
"Host": "fanyi.baidu.com",
"Connection": "keep-alive",
"Content-Length": "149",
"Accept": "*/*",
"Sec-Fetch-Dest": "empty",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "https://fanyi.baidu.com",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Referer": "https://fanyi.baidu.com/",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en,zh-CN;q=0.9,zh;q=0.8",
"Cookie": "BAIDUID=7E736869D29BDC0C480CFEF238B9E926:FG=1; PSTM=1535272183; BIDUPSID=AC2D3D05590936929DB7C1EBA13B7E50; H_WISE_SIDS=130610_126886_131439_132656_133470_114744_130510_132689_128143_133737_120188_133016_132910_133043_131246_132440_130763_132393_132378_132326_132212_131517_132260_118892_118862_118843_118821_118800_131650_131577_132841_131533_131529_132604_107315_133158_132590_132781_130121_133569_132708_133116_133351_132565_133479_133303_129647_124893_131862_132557_132540_133289_133473_131905_128891_132294_132552_132674_133387_129643_131423_132428_133414_133586_110085_127969_123290_133663_127417_131751_133179; BDUSS=GhMT0J0M2dKQkhnNTlKMU5kN0w1cVllVWNWekl0NFhRcGdNWnI5SFI3R1lSR2RlRVFBQUFBJCQAAAAAAAAAAAEAAABvg7MKbGl1eGluZ195dW5sdW8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJi3P16Ytz9eO; FANYI_WORD_SWITCH=1; REALTIME_TRANS_SWITCH=1; HISTORY_SWITCH=1; SOUND_PREFER_SWITCH=1; SOUND_SPD_SWITCH=1; Hm_lvt_afd111fa62852d1f37001d1f980b6800=1586486564; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1586486405,1586491138,1586502459; yjs_js_security_passport=93d819885978ce723898bf3af33b41f3455ce421_1586502457_js; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1586502474; __yjsv5_shitong=1.0_7_a3aac793117cc987de437de4ad9a7be2c043_300_1586502471488_111.227.124.90_8251440a"}
url = "https://fanyi.baidu.com/v2transapi?from=zh&to=en"
response = requests.post(url, headers = headers, data = data).text
return json.loads(response)['trans_result']['data'][0]['dst']
if __name__ == "__main__":
query = input("请输入要查询的单词:")
print(get_data(sign_number(query),query))
关于我们
微信公众号“Stata and Python数据分析”分享实用的stata、python等软件的数据处理知识,欢迎转载、打赏。我们是由李春涛教授领导下的研究生及本科生组成的大数据处理和分析团队。
1)必须原创,禁止抄袭;
2)必须准确,详细,有例子,有截图;
注意事项:
1)所有投稿都会经过本公众号运营团队成员的审核,审核通过才可录用,一经录用,会在该推文里为作者署名,并有赏金分成。
2)邮件请注明投稿,邮件名称为“投稿+推文名称”。
3)应广大读者要求,现开通有偿问答服务,如果大家遇到有关数据处理、分析等问题,可以在公众号中提出,只需支付少量赏金,我们会在后期的推文里给予解答。