更多>>关于我们
西安鲲之鹏网络信息技术有限公司从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()