更多>>关于我们
西安鲲之鹏网络信息技术有限公司从2010年开始专注于Web(网站)数据抓取领域。致力于为广大中国客户提供准确、快捷的数据采集相关服务。我们采用分布式系统架构,日采集网页数千万。我们拥有海量稳定高匿HTTP代理IP地址池,可以有效获取互联网任何公开可见信息。
您只需告诉我们您想抓取的网站是什么,您感兴趣的字段有哪些,你需要的数据是哪种格式,我们将为您做所有的工作,最后把数据(或程序)交付给你。
数据的格式可以是CSV、JSON、XML、ACCESS、SQLITE、MSSQL、MYSQL等等。
更多>>技术文章
有过阿里系采集经验的开发者都应该知道,某宝(某猫)H5版、1688、某宝司法拍卖H5版等阿里系网站,在Ajax请求中都会有一个sign签名参数(如下图1、2、3所示),要是值不正确将无法获取到有效的数据(例如返回“非法请求”提示)。如果我们无法构造出有效的sign,就只能通过“模拟浏览器操作”的方式来绕过签名验证,再结合"mitmproxy动态抓包脚本"来提取返回数据,这种方案效率太低,而且很不灵活。本文将介绍如何计算这个sign值以及给出对应的Python实现,这样就能实现通过直接HTTP交互抓取数据。
图1 天猫H5版Ajax请求中的sign参数
图2 1688网站Ajax请求中的sign参数
图3 某宝司法拍卖H5版Ajax请求中的sign参数
要想在自己的程序中计算出有效的sign值,就得分析一下这个sign是怎么产生的,分析过程如下:
1. 通过关键词“sign”在mtop.js文件中定位到sign的出生点,如下图所示。
2. 可以看出来,h()函数实现了sign的计算,所需的变量有token, i, g和data。
其中i为当前时间戳,g为固定值"12574478",data为请求的核心数据。难点在于token是如何产生的?
3. 通过js插桩发现:
(1)token(上述代码中的d.token)的值和请求头Cookie中的_m_h5_tk的第一部分是一致的。那么只要分析出来_m_h5_tk是如何产生的就能获取到token了。
(2)h()函数的算法如下。过程比较复杂,直接用Python还原难度太大,后面我们将使用Python执行JS代码的思路来实现。
function h(a) { function b(a, b) { return a << b | a >>> 32 - b } function c(a, b) { var c, d, e, f, g; return e = 2147483648 & a, f = 2147483648 & b, c = 1073741824 & a, d = 1073741824 & b, g = (1073741823 & a) + (1073741823 & b), c & d ? 2147483648 ^ g ^ e ^ f: c | d ? 1073741824 & g ? 3221225472 ^ g ^ e ^ f: 1073741824 ^ g ^ e ^ f: g ^ e ^ f } function d(a, b, c) { return a & b | ~a & c } function e(a, b, c) { return a & c | b & ~c } function f(a, b, c) { return a ^ b ^ c } function g(a, b, c) { return b ^ (a | ~c) } function h(a, e, f, g, h, i, j) { return a = c(a, c(c(d(e, f, g), h), j)), c(b(a, i), e) } function i(a, d, f, g, h, i, j) { return a = c(a, c(c(e(d, f, g), h), j)), c(b(a, i), d) } function j(a, d, e, g, h, i, j) { return a = c(a, c(c(f(d, e, g), h), j)), c(b(a, i), d) } function k(a, d, e, f, h, i, j) { return a = c(a, c(c(g(d, e, f), h), j)), c(b(a, i), d) } function l(a) { for (var b, c = a.length, d = c + 8, e = (d - d % 64) / 64, f = 16 * (e + 1), g = new Array(f - 1), h = 0, i = 0; c > i;) b = (i - i % 4) / 4, h = i % 4 * 8, g[b] = g[b] | a.charCodeAt(i) << h, i++; return b = (i - i % 4) / 4, ......(太多了,后面的省略)
4. 通过测试发现Cookie中的_m_h5_tk和_m_h5_tk_enc是必须参数,如果值无效将返回"非法令牌"。_m_h5_tk_enc应该是_m_h5_tk的的签名或者叫做校验码。
5. 通过分析Cookie的产生过程发现,当我们指定一个无效的_m_h5_tk和_m_h5_tk_enc,服务端会返回一个有效的_m_h5_tk和_m_h5_tk_enc,如下图所示。
token的有效获取方法掌握了,问题就都解决了。整理一下思路:
1. 通过提交一个含有无效的_m_h5_tk的请求(或者不带_m_h5_tk),获取服务端返回的有效的_m_h5_tk和_m_h5_tk_enc,进而得到token。
2. 通过h(token + '&' + i + '&' + '12574478' + '&' + data)计算出有效的签名sign。
3. 发送http请求,提取数据。token是可以复用的,不需要每个请求都重新获取一次token。但需要注意的是token是有有效期的,如果返回"令牌过期",就需要重新获取。
下面给出Python的实现过程,其中执行js代码是通过PyExecJS这个库实现的。
# coding: utf-8 # alibaba_h5_sign.py # 阿里系ajax接口sign签名机制分析及实现 import sys # pip install PyExecJS import execjs from webscraping import common, download class AlibabaH5Sign: """阿里系ajax接口sign签名实现 """ def __init__(self, proxy=None): self.proxy = None self.D = download.Download(read_cache=False, write_cache=False, delay=0.3, use_requests=True, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36') self.token = None self._m_h5_tk = self._m_h5_tk_enc = None self.__sign_js_code = '' with open('sign.js', 'rb') as f: self.__sign_js_code = f.read() self.jsctx = None def get_token(self): """获取有效的_m_h5_tk和_m_h5_tk_enc 提交一个无效的token,服务端会返回一个有效的token """ url = 'https://h5api.m.1688.com/h5/mtop.taobao.widgetservice.getjsoncomponent/1.0/?jsv=2.5.8&appKey=12574478&t=1634284176335&sign=1ae085904b015fe4e1f7c7be21d1e588&api=mtop.taobao.widgetService.getJsonComponent&v=1.0&ecode=1&type=jsonp&isSec=0&timeout=20000&dataType=jsonp&callback=mtopjsonp5&data=%7B%22cid%22%3A%22TpFacCoreInfosService%3ATpFacCoreInfosService%22%2C%22methodName%22%3A%22execute%22%2C%22params%22%3A%22%7B%5C%22facMemId%5C%22%3A%5C%22zjduowei%5C%22%7D%22%7D' self._m_h5_tk = self._m_h5_tk_enc = None #html = self.D.get(url, headers={'Cookie': '_m_h5_tk=b331d4ff8708d80d8ac280bc05c82ef2_1634294247978; _m_h5_tk_enc=3d5e5c6ee5f337ad5ff34f4da0611acc;'}, proxy=self.proxy) html = self.D.get(url, proxy=self.proxy) #print html #print self.D.response_headers if self.D.response_headers and 'Set-Cookie' in self.D.response_headers: new_cookies = self.D.response_headers['Set-Cookie'] self._m_h5_tk = common.regex_get(new_cookies, r'_m_h5_tk=([a-z\d_]+)', normalized=False) self._m_h5_tk_enc = common.regex_get(new_cookies, r'_m_h5_tk_enc=([a-z\d_]+)', normalized=False) if self._m_h5_tk and self._m_h5_tk_enc: common.logger.info('Successed to get _m_h5_tk({}) and _m_h5_tk_enc({}).'.format(self._m_h5_tk, self._m_h5_tk_enc)) self.token = self._m_h5_tk.partition('_')[0] else: common.logger.error('Failed to get token: {}'.format(html)) def sign(self, t, data): """计算签名 """ st = str(t) appKey = '12574478' if not self.jsctx: self.jsctx = execjs.compile(self.__sign_js_code) if not self.token: self.get_token() if self.token: sign_param = '&'.join([self.token, st, appKey, data]) return self.jsctx.call('h', sign_param) else: common.logger.error(u'未获取到token,计算签名失败.') def get_cookie(self): """返回http请求用的cookie""" return '_m_h5_tk={}; _m_h5_tk_enc={};'.format(self._m_h5_tk, self._m_h5_tk_enc) def test(): alisign = AlibabaH5Sign() # 获取token alisign.get_token() # 计算签名 sign_result = alisign.sign(t='1634528182860', data='{"cid":"TpFacCoreInfosService:TpFacCoreInfosService","methodName":"execute","params":"{\\"facMemId\\":\\"zjduowei\\"}"}') print sign_result # 发送http请求 # 如果token过期了,返回的内容含有"令牌过期"字样 # 只有token不过期就一直可以使用 if __name__ == '__main__': if '--test' in sys.argv: test()