当前位置: 首页 > news >正文

金融数据接口逆向实战:从JS加密到Python模拟请求的完整指南

1. 项目概述:一次典型的金融数据接口逆向实战

最近在做一个量化策略的辅助工具,需要实时获取A股市场的情绪热度数据。东方财富网的人气榜,作为一个反映个股实时关注度的风向标,自然进入了我的视线。这个榜单数据直观,更新频率高,对于短线情绪分析很有价值。然而,和大多数主流财经网站一样,东方财富并没有提供官方、稳定的数据API接口。这意味着,如果你想程序化地、自动化地获取这份榜单,唯一的途径就是对其网页或客户端进行逆向工程,找到数据源的真实接口。

这听起来有点“黑客”的味道,但实际上,在现代Web开发中,这更像是一种常规的数据获取技术探索。整个过程,就是一场与前端工程师的“猫鼠游戏”:他们用JavaScript混淆、参数加密、动态令牌来保护接口;我们则通过浏览器开发者工具、网络抓包、代码调试等手段,一步步揭开这些保护层,还原出最原始的HTTP请求。这次针对东方财富人气榜的逆向,就是一个非常典型的案例,涵盖了从抓包定位、参数逆向、签名破解到最终稳定请求的全流程。我踩了不少坑,也总结了一套行之有效的方法,下面就把完整的踩坑过程、技术细节和可直接运行的Python代码分享出来。

2. 逆向目标分析与环境准备

2.1 明确目标与数据定位

我们的核心目标是:模拟浏览器行为,通过程序发送HTTP请求,获取到与在东方财富网“人气榜”页面(通常是一个不断刷新的列表)看到的完全一致的、结构化的JSON数据。

首先,我们需要在浏览器中手动访问东方财富网,找到人气榜页面。通常,你可以在个股行情页的侧边栏或者专门的“热度”、“资金”板块找到它。打开Chrome或Edge浏览器的开发者工具(F12),切换到Network(网络)标签页,并勾选上“Preserve log”(保留日志)“Disable cache”(禁用缓存)。然后刷新页面,或者触发榜单的刷新(如果有刷新按钮)。

这时,网络面板会刷出一系列请求。我们的任务是找到那个真正返回榜单数据的请求。通常,这类数据请求具有以下特征:

  1. 请求类型: 大概率是XHRFetch
  2. 响应内容Preview(预览)或Response(响应)标签页里能看到清晰的JSON结构,包含股票代码、名称、排名、人气值等字段。
  3. 请求URL: 可能包含apidataquote等关键词,或者是一个看起来有规律但非页面的地址。
  4. 请求频率: 如果榜单是定时刷新的,那么这个请求也会周期性出现。

经过一番查找,我定位到的关键接口形如:https://push2.eastmoney.com/api/qt/ulist.np/get。这就是我们本次逆向的主战场。

2.2 工具链准备

工欲善其事,必先利其器。逆向分析不需要特别高深的装备,但以下几样是必不可少的:

  1. 浏览器开发者工具: Chrome/Edge DevTools。这是我们的主武器,用于网络抓包、JS调试、DOM查看。
  2. 抓包与调试工具
    • Charles/Fiddler: 可选。对于HTTPS流量抓取和更复杂的请求重放、断点调试有帮助,但对于这个案例,浏览器自带的工具基本够用。
    • Node.js: 强烈建议安装。用于在本地执行和调试关键的JavaScript代码片段,特别是涉及加密算法的部分。
  3. 编程语言与环境
    • Python 3.7+: 我们的最终实现语言。需要安装requests库用于发送HTTP请求。
    • Jupyter Notebook / VS Code: 方便的交互式环境,用于逐步测试和验证。
  4. 逆向辅助思路
    • 搜索关键词: 在开发者工具的Sources(源代码)标签页中,全局搜索(Ctrl+Shift+F)接口URL中的关键路径(如ulist.np/get)或请求参数名(如utfltt等),这是定位加密逻辑的捷径。
    • “Hook”思想: 在Console中,可以通过重写XMLHttpRequest.prototype.sendfetch函数,来拦截所有网络请求并打印详细信息,对于动态生成的请求尤其有效。

注意: 整个逆向过程必须遵守网站的服务条款(Robots协议)。本技术分享仅用于学习交流,获取的数据请勿用于商业用途或对目标服务器造成压力的高频请求。在实际应用中,务必添加合理的延时(如1-3秒一次),并考虑使用代理IP池来分散请求。

3. 核心接口参数逆向与解密

定位到接口后,在Network面板点击该请求,查看其HeadersPayload(在Fetch/XHR请求下,可能是Query String ParametersForm DataRequest Payload)。你会发现,东方财富的接口参数通常不是简单的明文。

3.1 请求参数拆解

以我找到的接口为例,一个典型的请求参数列表如下:

url: https://push2.eastmoney.com/api/qt/ulist.np/get fields: f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14 ut: fa5fd1943c7b386f172d6893dbfba10b fltt: 2 secid: 1.BK0814 invt: 2 cb: jsonp_callback_123456

我们来逐一分析:

  • fields: 这个相对好理解,它定义了需要返回哪些数据字段(f1, f2...)。你需要对照JSON响应,弄清楚每个f编号对应什么含义(如最新价、涨跌幅、人气值等)。
  • secid: 板块代码。1.BK0814可能代表“人气榜”这个特定的板块或分类。1通常代表沪深A股,BK开头可能是东方财富内部的板块编码。这个值需要从页面初始化或其他接口中获取。
  • fltt,invt: 这些可能是固定值或表示数据格式、类型的参数。
  • cb: JSONP回调函数名。如果接口是JSONP格式,这个参数是必须的,但我们的Python脚本可以直接处理JSON,可以构造一个固定的随机字符串。
  • ut:这是关键!这个长达32位的十六进制字符串,看起来就像一个MD5或类似算法的哈希值。它很可能是一个动态生成的令牌(token),用于验证请求的合法性,也是逆向中最难的一环。

3.2 关键参数ut的生成逻辑追踪

ut参数是接口防爬的核心。我们需要找到它是如何计算出来的。

  1. 全局搜索: 在开发者工具的Sources面板,全局搜索utfa5fd1943c7b386f172d6893dbfba10b这个具体的值。运气好的话,可以直接定位到生成它的函数。
  2. 调用栈分析: 在Network面板,找到目标请求,右键选择Copy -> Copy as cURL可能只能得到静态参数。更好的方法是,在发起请求的瞬间,于Sources面板给XHRsend方法或fetch打上断点,然后刷新页面。当断点触发时,调用栈(Call Stack)会显示当前执行到的所有函数,一步步回溯,就能找到参数组装和ut生成的地方。
  3. Hook 拦截: 在Console中输入以下代码,然后刷新页面或触发请求,可以打印出所有请求的详细信息,包括发起请求时的函数调用环境。
// Hook XMLHttpRequest (function() { var originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function() { console.trace('XHR send called', this._url || this.url); console.log('Request URL:', this._url || this.url); console.log('Request Method:', this._method || 'GET'); console.log('Request Headers:', this._headers); console.log('Request Body:', arguments[0]); return originalSend.apply(this, arguments); }; })(); // Hook fetch (function() { var originalFetch = window.fetch; window.fetch = function() { console.trace('Fetch called', arguments[0]); console.log('Fetch Request:', arguments); return originalFetch.apply(this, arguments); }; })();

通过以上方法,我最终追踪到ut的生成逻辑。它通常不是简单的对某个字符串做MD5,而是会结合一个密钥(secret)当前时间戳或其他动态因子,通过一个自定义的或标准的哈希算法(如HMAC-MD5)计算得出。这个密钥可能硬编码在某个巨大的、经过混淆的JavaScript文件里。

踩坑记录1:代码混淆与格式化找到的JS代码很可能是被压缩和混淆过的,变量名都是a, b, c, d。第一步是使用开发者工具自带的{}(格式化代码)按钮,让代码变得可读。即使格式化后,逻辑可能依然绕来绕去。这时需要耐心,关注核心的加密函数入口,比如函数名包含encryptsigngetTokenut等。

踩坑记录2:依赖浏览器环境生成ut的JavaScript函数,很可能依赖浏览器的某些内置对象或全局变量,比如windowdocument、或者页面中预先注入的一些全局变量(如_$)。直接把这个函数抠出来在Node.js里跑,可能会报错“xxx is not defined”。解决办法是,在Node.js环境中,用global对象模拟window,并补全缺失的变量。更稳妥的办法是,使用PyExecJSjs2py库,在Python中直接执行这段JS代码,但效率较低。最优解是,彻底理解算法后,用Python的加密库(如hashlib,hmac)重新实现。

3.3 参数secidfields的获取

  • secid: 这个值通常不是固定的。它可能通过页面初始化的另一个接口获取,或者隐藏在页面的某个全局变量或<script>标签的初始化数据中。你可以搜索BK0814secid来找到它的来源。有时,它可能对应着不同榜单(如“今日人气榜”、“周人气榜”),需要你根据需求替换。
  • fields: 这个需要你根据数据需求自己定义。通过观察多个请求和响应,可以归纳出常用的字段组合。例如,f12是股票代码,f14是股票名称,f3是涨跌幅,可能还有一个特定的字段(比如f100?)代表人气的具体数值或排名。这需要你仔细对比网页显示的数据和接口返回的JSON。

4. 完整请求构建与Python实现

在破解了ut的生成逻辑后,我们就可以用Python来模拟整个请求了。这里假设我们已经成功将生成ut的JS函数翻译成了Python函数generate_ut()

4.1 请求头(Headers)的模拟

仅仅有正确的参数是不够的,请求头(Headers)也很重要,特别是User-AgentReferer。一些简单的反爬会检查这些信息。

import requests import time import json from your_utils import generate_ut # 假设这是你实现的ut生成函数 def get_popularity_rank(): # 1. 构造请求URL和参数 base_url = "https://push2.eastmoney.com/api/qt/ulist.np/get" # 当前时间戳,可能是生成ut的因子之一 timestamp = int(time.time() * 1000) # 生成动态的 ut 参数 ut_token = generate_ut(timestamp) # 你需要实现这个函数 # 其他参数 params = { 'fields': 'f1,f2,f3,f4,f5,f6,f12,f13,f14,f100,f128,f136,f152', # 示例字段,需自行调整 'ut': ut_token, 'fltt': '2', 'secid': '1.BK0814', # 人气榜对应的secid 'invt': '2', 'cb': f'jQuery{timestamp}_{int(timestamp/1000)}', # 模拟一个jQuery JSONP回调名 '_': timestamp # 常见的防缓存参数 } # 2. 构造请求头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://quote.eastmoney.com/', # 人气榜页面所在的域名 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', } # 3. 发送请求 try: response = requests.get(base_url, params=params, headers=headers, timeout=10) response.raise_for_status() # 检查请求是否成功 # 4. 处理响应 (JSONP格式处理) resp_text = response.text # 响应通常是 jsonpCallback({...}) 格式,需要去掉包裹 # 找到第一个左括号和最后一个右括号 start = resp_text.find('(') end = resp_text.rfind(')') if start != -1 and end != -1: json_str = resp_text[start+1:end] data = json.loads(json_str) else: # 如果不是JSONP,尝试直接解析 data = response.json() # 5. 解析数据 # 数据结构通常是 data -> diff -> list stock_list = data.get('data', {}).get('diff', []) for stock in stock_list: code = stock.get('f12') # 股票代码 name = stock.get('f14') # 股票名称 rank_value = stock.get('f100') # 假设f100是人气值,需确认 print(f"代码: {code}, 名称: {name}, 人气值: {rank_value}") return stock_list except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return None except json.JSONDecodeError as e: print(f"JSON解析失败: {e}, 原始响应: {resp_text[:200]}") return None # 调用函数 if __name__ == '__main__': result = get_popularity_rank()

4.2generate_ut函数的Python实现示例

这是整个逆向的核心。由于涉及网站的具体算法(且可能变更),这里我给出一个假设性的通用框架。真实的算法需要你通过JS逆向得到。

import hashlib import hmac import time def generate_ut(timestamp): """ 模拟东方财富接口 ut 参数的生成算法。 注意:这是一个示例框架,真实算法需要通过JS逆向获得。 """ # 假设的密钥,真实情况需要从JS代码中提取 secret_key = b'eastmoney_secret_2024' # 这只是一个示例! # 假设的原始字符串拼接规则,例如: `method + timestamp + path` # 你需要根据逆向结果确定拼接顺序和内容 raw_string = f'GET{timestamp}/api/qt/ulist.np/get' # 假设使用 HMAC-MD5 算法,这也是常见的签名方式 signature = hmac.new(secret_key, raw_string.encode('utf-8'), hashlib.md5).hexdigest() # 有时还会进行二次处理,比如截取、大小写转换等 ut_token = signature.lower() # 假设最终转为小写 return ut_token

踩坑记录3:时间戳的格式与精度在逆向时,要特别注意JS代码里使用的时间戳是Date.now()(毫秒级,13位)还是Math.floor(Date.now() / 1000)(秒级,10位)。Python的time.time()返回浮点秒数,需要乘以1000取整才能得到13位毫秒时间戳。这个差异会导致签名错误。

踩坑记录4:字符串编码与拼接JavaScript和Python的字符串处理有时会有细微差别。确保在拼接用于签名的原始字符串时,空格、标点、顺序与JS端完全一致。最好将JS中生成签名的关键步骤用console.log打印出来,然后在Python中严格按照相同的步骤和中间结果进行比对调试。

5. 数据解析与持久化策略

成功获取到数据后,我们需要将其解析成可用的格式。

5.1 响应数据结构解析

东方财富接口返回的数据通常嵌套较深。你需要仔细查看data.diff这个列表。列表中的每个元素是一个字典,对应一只股票。字典的键就是fields参数里请求的那些f1,f2...。

你需要做一个字段映射表

FIELD_MAPPING = { 'f12': '股票代码', 'f14': '股票名称', 'f2': '最新价', 'f3': '涨跌幅(%)', 'f4': '涨跌额', 'f5': '成交量(手)', 'f6': '成交额', 'f100': '人气值', # 这个需要你确认 'f128': '排名', # 这个需要你确认 # ... 其他字段 }

然后遍历stock_list,根据这个映射表提取和重命名数据。

5.2 数据存储方案

对于实时监控,你可以选择:

  1. CSV文件: 简单易用,适合短期、小批量数据记录。每次运行追加一行时间戳和榜单数据(可以只存前N名)。
    import pandas as pd import datetime def save_to_csv(data_list, filename='popularity_rank.csv'): df = pd.DataFrame(data_list) df['更新时间'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 如果文件存在,追加写入,否则创建 try: existing_df = pd.read_csv(filename) final_df = pd.concat([existing_df, df], ignore_index=True) except FileNotFoundError: final_df = df final_df.to_csv(filename, index=False, encoding='utf_8_sig')
  2. 数据库: 推荐方案,适合长期、结构化存储和后续分析。使用SQLite(轻量)或MySQL/PostgreSQL。
    import sqlite3 import datetime def init_db(db_path='eastmoney.db'): conn = sqlite3.connect(db_path) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS popularity_rank (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME, stock_code TEXT, stock_name TEXT, rank INTEGER, popularity_value REAL, price REAL, change_percent REAL)''') conn.commit() conn.close() def save_to_db(data_list): conn = sqlite3.connect('eastmoney.db') c = conn.cursor() now = datetime.datetime.now() for item in data_list: c.execute('''INSERT INTO popularity_rank (timestamp, stock_code, stock_name, rank, popularity_value, price, change_percent) VALUES (?,?,?,?,?,?,?)''', (now, item['code'], item['name'], item['rank'], item['pop_value'], item['price'], item['change_pct'])) conn.commit() conn.close()

5.3 定时任务与自动化

使用系统的定时任务(如Linux的cron,Windows的任务计划程序)或Python库(如scheduleAPScheduler)来定期运行你的爬虫脚本。

import schedule import time def job(): print(f"开始获取人气榜数据 {time.strftime('%Y-%m-%d %H:%M:%S')}") data = get_popularity_rank() if data: save_to_db(data) # 或 save_to_csv print("数据获取完成") # 每5分钟运行一次 schedule.every(5).minutes.do(job) while True: schedule.run_pending() time.sleep(1)

重要提醒: 务必设置合理的请求间隔!过于频繁的请求(如每秒多次)会对东方财富的服务器造成压力,可能导致你的IP被暂时或永久封禁。建议间隔至少在1分钟以上,对于非实时性要求极高的策略,5-10分钟一次更为稳妥。可以考虑在请求间加入随机延时time.sleep(random.uniform(1, 3))来模拟更自然的人类行为。

6. 常见问题排查与维护技巧

即使代码写好了,在实际运行中也会遇到各种问题。这里记录了几个我踩过的坑和解决方法。

6.1 请求返回空数据或错误码

  • 现象data.diff为空列表,或者返回的JSON中有error_code
  • 排查步骤
    1. 检查ut参数: 这是最常见的原因。首先确认你的generate_ut函数是否与当前网站版本同步。网站可能会更新加密算法。重新抓包,对比你生成的ut和浏览器实际发送的ut是否完全一致。
    2. 检查时间戳: 确认时间戳的格式(10位还是13位)和取值是否与JS逻辑一致。服务器时间可能与本地时间有微小偏差,可以尝试用服务器时间(从其他接口的响应头获取)来校准。
    3. 检查secid: 确认这个板块代码是否有效。它可能已经过期或对应了错误的榜单。
    4. 检查请求头: 特别是RefererUser-Agent,有些反爬会校验它们。尝试使用与抓包时完全一致的Headers。
    5. 检查IP限制: 短时间内请求过多,IP可能被限制。尝试更换IP或增加请求间隔。

6.2ut生成函数依赖浏览器环境

  • 现象: 将JS函数抠出来在Node.js或Python的ExecJS中运行,报错ReferenceError: window is not defined
  • 解决方案
    1. 环境模拟: 在Node.js中,可以通过global.window = global;或定义缺失的全局变量来模拟。但这种方法比较 hacky。
    2. 算法重写: 这是最根本、最稳定的方法。彻底理解JS中的加密逻辑(比如是标准的HMAC-MD5,还是AES加密,或者是自定义的位运算),然后用Python的加密库(hashlib,hmac,Crypto)重新实现。这需要较强的代码分析能力。
    3. 使用无头浏览器: 作为备选方案,可以使用SeleniumPlaywright控制一个真正的浏览器来加载页面并执行JS,然后从浏览器上下文中提取数据。这种方法稳定但资源消耗大、速度慢,不适合高频请求。

6.3 接口变更与代码维护

金融网站的接口和反爬策略不是一成不变的。

  • 监控机制: 给你的脚本添加健康检查。如果连续多次请求失败或返回数据异常,应触发报警(如发送邮件、微信消息)。
  • 版本隔离: 将关键配置(如接口URL、secid、字段映射、加密密钥)放在配置文件(如config.yamlconfig.py)中,而不是硬编码在主逻辑里。这样当它们变化时,你只需要修改配置文件。
  • 定期复核: 每隔一两周,手动用浏览器抓一次包,对比一下请求参数和响应结构是否有变化。养成这个习惯可以让你在脚本完全失效前提前发现苗头。

6.4 数据清洗与异常值处理

爬取到的数据可能包含异常值,比如涨停/跌停时某些字段为特殊值(如None或字符串"-"),或者人气值突然出现极大/极小的异常点。

def clean_stock_data(item): """清洗单只股票的数据""" cleaned = {} for key, value in item.items(): if value is None: cleaned[key] = 0.0 if key in ['f2', 'f3', 'f100'] else '' elif isinstance(value, str) and value.strip() in ['-', '--', '']: cleaned[key] = 0.0 if key in ['f2', 'f3', 'f100'] else '' else: # 尝试转换为数值 try: cleaned[key] = float(value) except (ValueError, TypeError): cleaned[key] = value return cleaned

逆向工程是一个持续对抗和学习的過程。成功获取东方财富人气榜数据,不仅让你得到了一份有价值的数据源,更重要的是,你掌握了一套应对类似前端加密接口的通用方法论:抓包定位、参数分析、JS逆向、算法还原、模拟请求。这套方法在爬取其他有类似保护的网站时同样适用。最后再次强调,技术用于学习,使用数据请务必遵守法律法规和网站规则,保持克制的请求频率。

http://www.jsqmd.com/news/1097965/

相关文章:

  • 线上SQL性能突降排查指南:从CPU飙升到执行计划突变的完整路径
  • Java ECC算法实战:从原理到应用场景与避坑指南
  • Windows环境下使用John the Ripper与Hashcat破解压缩包密码实战指南
  • Java国密算法实战:基于Hutool与Bouncy Castle的SM2/SM3/SM4集成指南
  • AI编程不是提效神器,而是开发者认知升级的催化剂
  • Android应用安全测试入门:从环境搭建到漏洞挖掘实战指南
  • Android与iOS原生应用集成reCAPTCHA v3无感验证实战指南
  • 春秋云境CVE-2021-28164(极速版)
  • 前端安全实战:从XSS、CSRF到HTTPS的浏览器攻防体系构建
  • 零基础玩转Coze与Dify:从AI智能体到工作流的实战指南
  • DeepSeek界面更新背后的商业化技术逻辑解析
  • 2026抚顺黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • 深度学习优化器原理与实战:从SGD到Adam的调优心法
  • AUTOSAR CP IdsM实战:手把手教你配置R23-11版本的安全事件过滤器链
  • 文献梳理效率低?okbiye 专项 AI 文献综述功能适配各学段学术写作标准
  • 移动端性能测试实战:基于SoloPi的五大核心指标监控与分析方法
  • 蒸馏式论文精读:从复现到创造的四层漏斗方法
  • Burp Suite代理拦截与请求修改:Web安全测试的核心技能详解
  • AI反向训练人类:认知被悄然重塑的真相
  • Kali Linux 2026 虚拟机部署与汉化:VMware 环境下的渗透测试平台搭建指南
  • 数据增强的本质是构建可控的认知扰动场
  • AI Newsletter如何成为工程师的技术决策中枢
  • Agent Runtime:AI代理的“操作系统时刻”来临
  • 2026福州黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • X-diagnosis性能优化:减少系统开销的7个关键配置项
  • HAC分层强化学习:用目标重标定破解稀疏奖励难题
  • AI代理架构革命:事件日志驱动的可审计、可恢复、可伸缩Runtime
  • Python接口自动化测试框架2.0:从Postman到代码化的平滑进阶
  • VC++集成Crypto++实战:从编译配置到AES/RSA加密解密应用
  • 前端加密实战:TweetNaCl.js核心API与安全通信集成指南