1. 项目概述一次典型的金融数据接口逆向实战最近在做一个量化策略的辅助工具需要实时获取A股市场的情绪热度数据。东方财富网的人气榜作为一个反映个股实时关注度的风向标自然进入了我的视线。这个榜单数据直观更新频率高对于短线情绪分析很有价值。然而和大多数主流财经网站一样东方财富并没有提供官方、稳定的数据API接口。这意味着如果你想程序化地、自动化地获取这份榜单唯一的途径就是对其网页或客户端进行逆向工程找到数据源的真实接口。这听起来有点“黑客”的味道但实际上在现代Web开发中这更像是一种常规的数据获取技术探索。整个过程就是一场与前端工程师的“猫鼠游戏”他们用JavaScript混淆、参数加密、动态令牌来保护接口我们则通过浏览器开发者工具、网络抓包、代码调试等手段一步步揭开这些保护层还原出最原始的HTTP请求。这次针对东方财富人气榜的逆向就是一个非常典型的案例涵盖了从抓包定位、参数逆向、签名破解到最终稳定请求的全流程。我踩了不少坑也总结了一套行之有效的方法下面就把完整的踩坑过程、技术细节和可直接运行的Python代码分享出来。2. 逆向目标分析与环境准备2.1 明确目标与数据定位我们的核心目标是模拟浏览器行为通过程序发送HTTP请求获取到与在东方财富网“人气榜”页面通常是一个不断刷新的列表看到的完全一致的、结构化的JSON数据。首先我们需要在浏览器中手动访问东方财富网找到人气榜页面。通常你可以在个股行情页的侧边栏或者专门的“热度”、“资金”板块找到它。打开Chrome或Edge浏览器的开发者工具F12切换到Network网络标签页并勾选上“Preserve log”保留日志和“Disable cache”禁用缓存。然后刷新页面或者触发榜单的刷新如果有刷新按钮。这时网络面板会刷出一系列请求。我们的任务是找到那个真正返回榜单数据的请求。通常这类数据请求具有以下特征请求类型 大概率是XHR或Fetch。响应内容Preview预览或Response响应标签页里能看到清晰的JSON结构包含股票代码、名称、排名、人气值等字段。请求URL 可能包含api、data、quote等关键词或者是一个看起来有规律但非页面的地址。请求频率 如果榜单是定时刷新的那么这个请求也会周期性出现。经过一番查找我定位到的关键接口形如https://push2.eastmoney.com/api/qt/ulist.np/get。这就是我们本次逆向的主战场。2.2 工具链准备工欲善其事必先利其器。逆向分析不需要特别高深的装备但以下几样是必不可少的浏览器开发者工具 Chrome/Edge DevTools。这是我们的主武器用于网络抓包、JS调试、DOM查看。抓包与调试工具Charles/Fiddler 可选。对于HTTPS流量抓取和更复杂的请求重放、断点调试有帮助但对于这个案例浏览器自带的工具基本够用。Node.js 强烈建议安装。用于在本地执行和调试关键的JavaScript代码片段特别是涉及加密算法的部分。编程语言与环境Python 3.7 我们的最终实现语言。需要安装requests库用于发送HTTP请求。Jupyter Notebook / VS Code 方便的交互式环境用于逐步测试和验证。逆向辅助思路搜索关键词 在开发者工具的Sources源代码标签页中全局搜索CtrlShiftF接口URL中的关键路径如ulist.np/get或请求参数名如ut、fltt等这是定位加密逻辑的捷径。“Hook”思想 在Console中可以通过重写XMLHttpRequest.prototype.send或fetch函数来拦截所有网络请求并打印详细信息对于动态生成的请求尤其有效。注意 整个逆向过程必须遵守网站的服务条款Robots协议。本技术分享仅用于学习交流获取的数据请勿用于商业用途或对目标服务器造成压力的高频请求。在实际应用中务必添加合理的延时如1-3秒一次并考虑使用代理IP池来分散请求。3. 核心接口参数逆向与解密定位到接口后在Network面板点击该请求查看其Headers和Payload在Fetch/XHR请求下可能是Query String Parameters或Form Data或Request 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参数是接口防爬的核心。我们需要找到它是如何计算出来的。全局搜索 在开发者工具的Sources面板全局搜索ut或fa5fd1943c7b386f172d6893dbfba10b这个具体的值。运气好的话可以直接定位到生成它的函数。调用栈分析 在Network面板找到目标请求右键选择Copy - Copy as cURL可能只能得到静态参数。更好的方法是在发起请求的瞬间于Sources面板给XHR的send方法或fetch打上断点然后刷新页面。当断点触发时调用栈Call Stack会显示当前执行到的所有函数一步步回溯就能找到参数组装和ut生成的地方。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。第一步是使用开发者工具自带的{}格式化代码按钮让代码变得可读。即使格式化后逻辑可能依然绕来绕去。这时需要耐心关注核心的加密函数入口比如函数名包含encrypt、sign、getToken、ut等。踩坑记录2依赖浏览器环境生成ut的JavaScript函数很可能依赖浏览器的某些内置对象或全局变量比如window、document、或者页面中预先注入的一些全局变量如_、$。直接把这个函数抠出来在Node.js里跑可能会报错“xxx is not defined”。解决办法是在Node.js环境中用global对象模拟window并补全缺失的变量。更稳妥的办法是使用PyExecJS或js2py库在Python中直接执行这段JS代码但效率较低。最优解是彻底理解算法后用Python的加密库如hashlib,hmac重新实现。3.3 参数secid与fields的获取secid 这个值通常不是固定的。它可能通过页面初始化的另一个接口获取或者隐藏在页面的某个全局变量或script标签的初始化数据中。你可以搜索BK0814或secid来找到它的来源。有时它可能对应着不同榜单如“今日人气榜”、“周人气榜”需要你根据需求替换。fields 这个需要你根据数据需求自己定义。通过观察多个请求和响应可以归纳出常用的字段组合。例如f12是股票代码f14是股票名称f3是涨跌幅可能还有一个特定的字段比如f100代表人气的具体数值或排名。这需要你仔细对比网页显示的数据和接口返回的JSON。4. 完整请求构建与Python实现在破解了ut的生成逻辑后我们就可以用Python来模拟整个请求了。这里假设我们已经成功将生成ut的JS函数翻译成了Python函数generate_ut()。4.1 请求头Headers的模拟仅仅有正确的参数是不够的请求头Headers也很重要特别是User-Agent和Referer。一些简单的反爬会检查这些信息。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: fjQuery{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;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, } # 3. 发送请求 try: response requests.get(base_url, paramsparams, headersheaders, timeout10) 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[start1: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(fJSON解析失败: {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 beastmoney_secret_2024 # 这只是一个示例 # 假设的原始字符串拼接规则例如 method timestamp path # 你需要根据逆向结果确定拼接顺序和内容 raw_string fGET{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 数据存储方案对于实时监控你可以选择CSV文件 简单易用适合短期、小批量数据记录。每次运行追加一行时间戳和榜单数据可以只存前N名。import pandas as pd import datetime def save_to_csv(data_list, filenamepopularity_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_indexTrue) except FileNotFoundError: final_df df final_df.to_csv(filename, indexFalse, encodingutf_8_sig)数据库 推荐方案适合长期、结构化存储和后续分析。使用SQLite轻量或MySQL/PostgreSQL。import sqlite3 import datetime def init_db(db_patheastmoney.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的cronWindows的任务计划程序或Python库如schedule、APScheduler来定期运行你的爬虫脚本。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。排查步骤检查ut参数 这是最常见的原因。首先确认你的generate_ut函数是否与当前网站版本同步。网站可能会更新加密算法。重新抓包对比你生成的ut和浏览器实际发送的ut是否完全一致。检查时间戳 确认时间戳的格式10位还是13位和取值是否与JS逻辑一致。服务器时间可能与本地时间有微小偏差可以尝试用服务器时间从其他接口的响应头获取来校准。检查secid 确认这个板块代码是否有效。它可能已经过期或对应了错误的榜单。检查请求头 特别是Referer和User-Agent有些反爬会校验它们。尝试使用与抓包时完全一致的Headers。检查IP限制 短时间内请求过多IP可能被限制。尝试更换IP或增加请求间隔。6.2ut生成函数依赖浏览器环境现象 将JS函数抠出来在Node.js或Python的ExecJS中运行报错ReferenceError: window is not defined。解决方案环境模拟 在Node.js中可以通过global.window global;或定义缺失的全局变量来模拟。但这种方法比较 hacky。算法重写 这是最根本、最稳定的方法。彻底理解JS中的加密逻辑比如是标准的HMAC-MD5还是AES加密或者是自定义的位运算然后用Python的加密库hashlib,hmac,Crypto重新实现。这需要较强的代码分析能力。使用无头浏览器 作为备选方案可以使用Selenium或Playwright控制一个真正的浏览器来加载页面并执行JS然后从浏览器上下文中提取数据。这种方法稳定但资源消耗大、速度慢不适合高频请求。6.3 接口变更与代码维护金融网站的接口和反爬策略不是一成不变的。监控机制 给你的脚本添加健康检查。如果连续多次请求失败或返回数据异常应触发报警如发送邮件、微信消息。版本隔离 将关键配置如接口URL、secid、字段映射、加密密钥放在配置文件如config.yaml或config.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逆向、算法还原、模拟请求。这套方法在爬取其他有类似保护的网站时同样适用。最后再次强调技术用于学习使用数据请务必遵守法律法规和网站规则保持克制的请求频率。