news 2026/8/27 5:36:29

爬虫进阶:解密黑猫投诉平台JS加密参数实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
爬虫进阶:解密黑猫投诉平台JS加密参数实战

1. 从抓包到定位:找到加密参数的“老巢”

做爬虫的朋友都知道,最头疼的不是写解析代码,而是当你信心满满地发起请求时,服务器冷冷地回你一个“参数错误”。黑猫投诉平台的搜索接口就属于这种“硬骨头”。我第一次尝试爬取时,直接复制浏览器里的请求参数,结果换来的只有空数据。这让我意识到,它的signaturetsrs这几个参数背后肯定有文章。

我的习惯是,遇到这种问题,先打开浏览器的开发者工具(F12),切换到 Network(网络)面板,然后进行一次搜索操作。这时候,你会看到列表里蹦出来一堆请求,我们需要找到那个真正返回投诉列表数据的请求。通常,它名字里带“s”或者“search”,并且响应体是 JSON 格式。点开它,在 Headers(标头)标签页里,我们重点看 Query String Parameters(查询字符串参数)这一块。

喏,问题参数就在这里了:ts看起来像时间戳,rs是一串随机的字母数字组合,而signature则是一长串看起来毫无规律的十六进制字符串。最关键的是,你刷新页面再请求一次,rssignature全变了,只有ts相对稳定(但也会随时间变)。这说明,signature很可能就是利用tsrs以及其他一些固定或半固定的参数,通过某种加密算法计算出来的一个“签名”。服务器收到请求后,会用同样的逻辑再算一遍,对不上就认为你是非法请求。

那么,这个签名算法藏在哪里呢?肯定在前端 JavaScript 代码里。接下来就是经典的“搜代码”环节。在 Sources(源代码)面板里,按Ctrl+Shift+F进行全局搜索。因为signature这个变量名比较独特,直接搜它。你会发现,可能有好几个 JS 文件都包含这个字符串。这时候别慌,我们挨个点进去看。

通常,生成关键参数的代码不会在那些库文件里(比如 jquery.min.js),而是在网站自己写的业务逻辑文件里。我当时的经验是,找到一个名字里带“index”或者“search”的 JS 文件,在里面再次搜索signature。这次,我们不是搜字符串,而是搜这个变量被赋值的地方,也就是找signature =或者var signature =这样的代码段。

找到疑似位置后,就在那一行打上断点(点击行号即可)。然后回到网页,触发一次新的搜索请求。如果代码执行到你的断点处停住了,那么恭喜你,找到“老巢”了。我第一次打了好几个断点才命中,这个过程需要一点耐心。当你看到调试器在断点处暂停,并且右侧的 Scope(作用域)里能清楚地看到tsrssignature这些变量的值时,那种感觉就像侦探找到了关键线索,特别有成就感。

2. 抽丝剥茧:逆向分析签名生成逻辑

代码执行在断点处停下后,我们才算真正进入了“解密”的核心战场。你不能只看当前这一行,得往上翻,看看这些参数是怎么来的。我当时看到的代码结构大致是这样的:

var l = Date.now(); // 这就是 ts var p = someFunction(true, 4, 16); // 这就是 rs,一个随机字符串 var b = '$d6eb7ff91ee257475%'; // 一个固定的常量 var h = PAGE_CONFIG.keywords; // 搜索关键词,比如“外卖 食品安全” var c = '10'; // 每页大小 page_size var e = 1; // 一个固定值,用于拼接 var g = u([l, p, b, h, c, d["type" + e]].sort().join("")); // g 就是最终的 signature

看明白了吗?signature(也就是变量g)并不是凭空产生的,它是将l(时间戳ts)、p(随机串rs)、b(固定常量)、h(关键词)、c(页面大小)和另一个由d对象根据e计算出的值(其实就是页码page)这六个元素,先放到一个数组里,然后对这个数组进行排序,再拼接成一个字符串,最后把这个字符串扔进一个叫u的函数里加工出来的。

这里有几个关键点,我踩过坑,你们一定要注意:

  1. 排序[l, p, b, h, c, d].sort()这一步非常关键。JavaScript 的数组sort()方法,在不传比较函数时,默认是按字符串的 Unicode 码点排序,而不是按数字大小。这意味着10(页码)和2(某个值)排序时,10会排在2前面,因为比较的是字符"1""2"。我们后面用 Python 实现时,必须完全复现这个行为。
  2. 拼接:排序后,用的是.join(""),中间没有分隔符,直接拼成一个长字符串。少一个字符或者顺序不对,最后算出来的签名都会天差地别。
  3. u函数:这是最后的“黑盒”,也是最核心的加密环节。我们需要跟进去看看它到底是什么。

怎么跟进u函数?简单,在调试器里,把鼠标悬停在函数名u上,或者直接点击它,通常会跳转到它的定义处。如果没跳转,就在当前文件里搜索function u的定义。跟进去之后,你可能会发现它里面又调用了其他函数。这时候就需要耐心地一步步跟踪,或者结合 console 打印一些中间值来观察。

我当初跟进去后发现,这个u函数本质上就是执行了一次SHA-256 哈希算法。它可能做了一些额外的处理,比如把字符串转成了某种格式,但核心是 SHA-256。在 Python 里,我们有现成的hashlib库来实现它,所以只要确认了算法,这部分就解决了。

至于rs参数,它是由一个函数生成的随机字符串。我们不需要完全理解那个函数每一行在干嘛(有时候是混淆过的),只需要在调试状态下,把生成rs的函数代码块复制出来,然后借助一些工具(比如 ChatGPT)帮我们把它“翻译”成功能等价的 Python 函数。重点是看它的输入参数和输出结果,确保我们模拟出来的函数,在相同输入下,能产生和浏览器里一模一样的随机字符串。这里要注意随机种子的问题,不过好在它通常用的是Math.random(),我们在 Python 里用random模块模拟即可。

3. 动手实现:用Python复现加密过程

分析清楚了,就该动手写了。我们用 Python 来完全复现浏览器里的那套逻辑。首先,把几个“原料”准备好:

import time import random import hashlib # 1. 生成 ts (就是 l) ts = str(int(time.time() * 1000)) # 精确到毫秒的13位时间戳 # 2. 生成 rs (就是 p) def generate_rs(e=True, t=4, r=16): """ 模拟JS端的随机字符串生成函数 e: 控制是否随机长度,True时长度在[t, r]之间随机 t: 最小长度 r: 最大长度 """ chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" if e: length = random.randint(t, r) else: length = t return ''.join(random.choice(chars) for _ in range(length)) rs = generate_rs(True, 4, 16) # 生成一个16位(或4-16位间)的随机字符串 # 3. 固定常量 b b = '$d6eb7ff91ee257475%' # 4. 搜索关键词 h (从你的实际搜索词来) keywords = "外卖 食品安全" h = keywords # 5. 每页大小 c page_size = '10' c = page_size # 6. 页码 d (注意,这里对应的是 d["type"+e],e固定为1,所以就是 d["type1"],其值就是页码) page = '1' # 假设爬第一页 d = page

原料齐了,接下来就是最关键的一步:按照完全相同的顺序和规则生成签名。

# 将六个参数放入列表,注意,这里放的是它们的字符串形式 param_list = [ts, rs, b, h, c, d] # 模拟JavaScript的 array.sort() 默认排序(按字符串Unicode排序) # Python的 sorted() 默认也是按字符顺序,但为了绝对一致,我们指定 key=str param_list_sorted = sorted(param_list, key=str) # 拼接成字符串,不留任何分隔符 signature_raw_string = ''.join(param_list_sorted) # 进行SHA-256哈希计算 def get_sha256(text): """计算字符串的SHA-256哈希值,并返回十六进制字符串""" sha256_hash = hashlib.sha256() sha256_hash.update(text.encode('utf-8')) return sha256_hash.hexdigest() signature = get_sha256(signature_raw_string)

最后,组装成请求参数字典:

params = { 'ts': ts, 'rs': rs, 'signature': signature, 'keywords': h, 'page_size': c, 'page': d, }

你可以写个简单的测试脚本,用这个params去发起请求,和浏览器开发者工具里捕获的请求参数对比一下。如果tsrs你设置成和浏览器某次请求一样的值,那么计算出来的signature必须完全一致。这是检验你逆向是否成功的唯一标准。我第一次就栽在排序上,Python 的排序结果和 JS 的默认排序对数字字符串的处理有细微差别,调试了好久。

4. 构建完整爬虫:处理请求与数据解析

参数生成搞定,爬虫就成功了一大半。接下来就是常规操作了,但也有一些细节需要注意。

首先,你需要从浏览器里复制出完整的headerscookiesheaders里,User-AgentRefererAccept等字段最好都带上,特别是Referer,有时候服务器会校验。cookies是维持会话的关键,直接复制过来用就行。这里我建议使用requests.Session()来保持会话,这样会自动处理 cookies。

import requests from bs4 import BeautifulSoup import json sess = requests.Session() # 替换成你自己从浏览器复制的 headers 和 cookies headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', 'Referer': 'https://tousu.sina.com.cn/', # ... 其他 headers } cookies = { 'SUB': '_2AkMS...', # ... 其他 cookies } sess.headers.update(headers) sess.cookies.update(requests.utils.cookiejar_from_dict(cookies))

然后,我们可以构造一个循环来爬取多页数据。在循环里,每次生成新的tsrssignature。这里有个非常重要的坑:频率控制。黑猫投诉平台这类网站肯定有反爬机制,你不能请求得太快。

我的策略是,在请求列表页的代码块外加一个try...except,如果请求失败(比如返回了错误码,或者 JSON 解析失败),就等待一段时间(比如 30 秒或 5 分钟)再重试。甚至可以在每爬取若干页(比如 50 页)后,主动time.sleep(60)休息一分钟,模拟人工操作。

base_url = 'https://tousu.sina.com.cn/api/index/s' all_complaints_data = [] for page_num in range(1, 101): # 假设爬100页 print(f"正在爬取第 {page_num} 页...") # 生成当前页的动态参数 ts, rs, signature = generate_dynamic_params(keywords="外卖 食品安全", page=str(page_num)) params = { 'ts': ts, 'rs': rs, 'signature': signature, 'keywords': keywords, 'page_size': '10', 'page': str(page_num), } max_retries = 3 for retry in range(max_retries): try: resp = sess.get(base_url, params=params, timeout=10) resp.raise_for_status() # 检查HTTP错误 data_json = resp.json() # 提取列表数据 complaint_list = data_json['result']['data']['lists'] for item in complaint_list: # 这里可以提取列表页已有的信息,比如标题、投诉编号 # 但详情通常要点进去看,所以这里主要收集详情页URL detail_url = item['main']['url'] if detail_url: # 调用函数爬取详情页 detail_data = parse_detail_page('https:' + detail_url, sess) all_complaints_data.append(detail_data) break # 成功则跳出重试循环 except (requests.RequestException, json.JSONDecodeError, KeyError) as e: print(f"第{page_num}页请求失败,第{retry+1}次重试。错误:{e}") if retry < max_retries - 1: time.sleep(30) # 等待30秒后重试 else: print(f"第{page_num}页重试{max_retries}次后仍失败,跳过。") time.sleep(300) # 长时间失败后等待更久 break # 每爬完一页,稍微休息一下 time.sleep(random.uniform(2, 5))

列表页返回的 JSON 数据里,通常只包含投诉的摘要信息。要获取完整的投诉内容、处理过程、商家回复等,需要进入每个投诉的详情页。详情页的 URL 一般在列表项的数据里。爬取详情页就是普通的 HTML 解析工作了,用 BeautifulSoup 或者 lxml 都很方便。

def parse_detail_page(url, session): """解析投诉详情页,提取所需信息""" try: resp = session.get(url, timeout=10) soup = BeautifulSoup(resp.text, 'html.parser') # 这里需要你查看详情页的HTML结构,找到对应的选择器 # 例如,投诉时间可能在一个 class 为 'u-date' 的 span 里 complaint_time = soup.find('span', class_='u-date').text.strip() if soup.find('span', class_='u-date') else 'N/A' # 投诉编号可能在某个列表项里 complaint_number = 'N/A' ul_list = soup.find('ul', class_='ts-q-list') if ul_list: lis = ul_list.find_all('li') if lis: complaint_number = lis[0].get_text(strip=True) # 假设第一个li是编号 # 投诉内容 content_div = soup.find('div', class_='complaint-content') complaint_content = content_div.get_text(strip=True) if content_div else 'N/A' return { '投诉时间': complaint_time, '投诉编号': complaint_number, '投诉内容': complaint_content, '详情页链接': url } except Exception as e: print(f"解析详情页 {url} 失败: {e}") return None

最后,把爬取到的数据all_complaints_data保存下来,可以用pandas存成 CSV,或者直接用json模块存成 JSON 文件,方便后续分析。

import pandas as pd df = pd.DataFrame(all_complaints_data) df.to_csv('黑猫投诉数据.csv', index=False, encoding='utf-8-sig') print(f"爬取完成,共获取 {len(df)} 条投诉数据。")

整个流程走下来,从抓包分析、逆向加密逻辑、用 Python 复现、到构建稳健的爬虫,每一步都需要细心和耐心。尤其是逆向那部分,就像解谜一样,需要反复调试和验证。但一旦跑通,看到数据哗哗地下来,那种解决问题的快感,就是技术人最大的乐趣。记住,核心秘诀就是“模仿”:让你的 Python 代码在生成参数时,行为要和浏览器里的 JavaScript一模一样,分毫不差。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:01:23

ST-LINK烧录stm32程序全流程解析与常见问题排查

1. 从零开始&#xff1a;认识你的ST-LINK与STM32 大家好&#xff0c;我是老张&#xff0c;一个在嵌入式圈子里摸爬滚打了十来年的“老电工”。今天咱们不聊那些高深的理论&#xff0c;就实实在在地聊聊怎么用ST-LINK这个小工具&#xff0c;把你辛辛苦苦写的代码“灌”进STM32单…

作者头像 李华
网站建设 2026/7/14 17:01:24

音频功放电路全解析:从A类到T类的设计与应用

1. 音频功放电路&#xff1a;不只是“放大声音”那么简单 很多朋友一听到“功放”&#xff0c;第一反应可能就是家里那个笨重的、发热量巨大的“大块头”音响设备。其实&#xff0c;功放电路无处不在&#xff0c;从你口袋里蓝牙耳机的驱动芯片&#xff0c;到广场舞大妈手里拉杆…

作者头像 李华
网站建设 2026/7/14 17:01:37

Chapter 21 驾驭JSON:从数据交换到实战解析

1. JSON&#xff1a;现代软件开发的“世界语” 如果你刚开始学编程&#xff0c;或者刚接触Web开发&#xff0c;可能会经常听到一个词&#xff1a;JSON。它无处不在&#xff0c;从你手机App里刷新的新闻列表&#xff0c;到网页上弹出的登录框&#xff0c;再到你电脑里某个软件的…

作者头像 李华
网站建设 2026/7/14 17:01:34

Qwen-Image新手指南:无需代码,3分钟体验AI绘画的魅力

Qwen-Image新手指南&#xff1a;无需代码&#xff0c;3分钟体验AI绘画的魅力 1. 引言&#xff1a;从想象到画面&#xff0c;只需一句话 你是否曾经有过这样的想法&#xff1a;脑子里冒出一个绝妙的画面&#xff0c;却苦于不会画画&#xff0c;无法把它变成现实&#xff1f;或…

作者头像 李华
网站建设 2026/7/14 17:01:36

富文本编辑器如何集成ueditor的PPT动画导入?

教育CMS系统Word导入功能开发实录——PHP程序员视角 一、需求拆解与技术选型 作为独立开发者&#xff0c;与客户进行了2轮需求确认会议&#xff0c;明确核心需求&#xff1a; 教师用户&#xff1a;需将备课教案&#xff08;含化学公式、教学图表&#xff09;无损转为网页内容…

作者头像 李华