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















