拓冰建站拓冰建站
首页 / 资讯中心 / 正文

东方财富股票数据接口逆向:JSONP回调机制与Python实战

1. 拆解这个股票数据接口它到底在做什么东方财富网作为国内主流的财经信息平台其页面上的股票行情数据并不是静态写死在 HTML 里的而是通过前端 JavaScript 异步向后台接口请求拿到 JSON 数据后再渲染到页面上。这个接口有一个非常典型的特征它返回的不是纯 JSON而是被一层函数调用包裹起来的 JSONP 格式也就是类似callbackFunction({...})这样的结构。标题里说的带 jQuery 回调指的就是这个包裹层——jQuery 在处理跨域 AJAX 时会自动生成一个形如jQuery3xxxxx_1234567890的回调函数名服务端返回的数据会被这个函数名包住浏览器执行时相当于调用该函数并把数据作为参数传入。这个项目的核心目标就是搞清楚这个接口的请求规律、参数含义、回调机制最终能够用代码稳定地拿到股票数据。它解决的问题很实际官方并不提供公开的、文档齐全的免费数据 API但页面上展示的数据总得从某个地方来找到那个地方就能拿到结构化的数据。适合有一定 HTTP 基础、看得懂浏览器开发者工具、会一点 Python 或 JavaScript 的读者来参考。哪怕你之前没接触过 JSONP只要跟着思路走也能理解整个链路。我先把结论放在前面这类接口的逆向本质上就是观察请求、理解参数、复现请求三步。难点不在技术本身而在于参数里往往藏着一些需要推导的字段以及服务端对请求头、Referer 有一定校验。下面我会把每一步拆开讲清楚。2. 接口逆向的整体思路与方案选型2.1 为什么选择从浏览器网络面板入手逆向一个 Web 接口最直接的入口就是浏览器的开发者工具。打开东方财富网的行情页面切到 Network 面板筛选 XHR 或 JS 请求刷新页面你就能看到一堆请求飞出去。这里面有的是加载页面框架的有的是拉广告的有的是真正取数据的。判断哪个是目标接口有几个经验性的信号返回内容里包含你关心的字段比如股票代码、最新价、涨跌幅请求的 URL 里带有push2、api、get这类字样以及响应体是 JSONP 而不是 HTML。我个人的习惯是先按响应大小排序数据接口通常返回的体量中等偏小几十 KB 以内。然后再看响应内容一眼就能认出哪个是行情数据。这一步不需要任何工具浏览器自带的就够了这也是为什么我推荐新手从这里开始——所见即所得不用猜。2.2 JSONP 回调机制为什么值得单独讲很多人第一次看到jQuery3410xxxxx_1699xxxxxx({...})这种返回会懵觉得这是什么加密。其实不是。这是 JSONPJSON with Padding的标准形态。它的诞生背景是浏览器的同源策略早期跨域请求 AJAX 会被拦截但script标签的 src 不受同源限制。于是大家想了个办法——把数据当成 JS 脚本加载服务端返回一段调用某个函数的代码函数名由前端通过查询参数传过去前端提前在全局定义好这个函数脚本一加载函数就被执行数据就到手了。jQuery 把这个过程封装了你写$.ajax({dataType: jsonp})它自动帮你生成回调函数名、挂到 window 上、拼到 URL 里。所以你在请求 URL 里会看到cbjQuery3410xxxxx_1699xxxxxx这样的参数。理解这一点之后你自己用 Python 复现时就可以自己指定一个回调名比如cbmyCallback然后手动把返回结果里myCallback(和最后的)剥掉剩下的就是纯 JSON。2.3 工具选型为什么用 Python requests 而不是别的复现请求这一步工具选择很多。用浏览器控制台直接 fetch 也行用 Postman 也行用 Python 的 requests 也行。我倾向推荐 requests原因是它方便做批量、方便做参数化、方便把结果落库。而且 requests 对请求头的控制很直接遇到服务端校验 Referer 或 User-Agent 时改起来一目了然。如果你更熟悉 Node.js用 axios 或 node-fetch 也完全可以逻辑一模一样。核心不在于用什么语言而在于你能不能把浏览器发出的那个请求一比一地复制出来。下面我会以 Python 为例但思路是通用的。3. 核心细节解析参数、请求头与回调3.1 请求 URL 的结构拆解一个典型的行情接口 URL 长这样域名和路径我做了抽象实际以你抓到的为准https://push2.example.com/api/qt/stock/get?cbjQuery3410xxxxx_1699xxxxxxsecid1.600519fieldsf43,f44,f45,f46,f47,f48_1699xxxxxx拆开看几个关键参数cb回调函数名JSONP 的核心服务端会用它包裹返回数据。secid证券标识格式是市场代码.股票代码。这里的市场代码很关键沪市通常是 1深市通常是 0具体以实测为准。fields要返回的字段列表用逗号分隔。这是这个接口很聪明的地方——它允许你按需索取字段减少传输量。_时间戳防止浏览器缓存值本身不影响业务逻辑。fields这个设计值得多说两句。服务端内部对每个字段都有编号比如 f43 可能是最新价f44 可能是最高价f45 可能是最低价f46 可能是开盘价f47 可能是成交量f48 可能是成交额。这些编号不是官方文档给的而是通过对比返回值和页面显示值反推出来的。反推的方法很朴素请求一批字段看返回的 JSON 里每个 key 对应的值再和页面上显示的数字对照对上了就记下来。3.2 请求头里哪些是必须的不是所有请求头都重要但有几个漏了就会被拒请求头作用是否必须User-Agent标识客户端类型建议带上模拟真实浏览器Referer标识请求来源页面多数情况下必须缺失可能返回空Accept声明可接受的响应类型建议带上Accept-Language语言偏好可选Referer 这一项是很多人踩坑的地方。服务端会检查请求是不是从自己的页面发起的如果你直接用 requests 裸请求Referer 为空很可能拿到一个空响应或者错误码。解决办法就是手动把 Referer 设成对应的行情页面地址。提示请求头不要凭感觉编最稳妥的做法是从浏览器 Network 面板里把真实请求的 headers 复制出来逐条对照。3.3 回调名的生成规律jQuery 生成的回调名格式是jQuery 随机数字 _ 时间戳。这个格式本身不重要因为服务端并不校验回调名的具体内容它只是把你传进去的名字原样包在数据外面。所以你完全可以传cbabc返回就是abc({...})。但有一个细节要注意有些接口对回调名的字符集有要求只接受字母、数字、下划线。你传个带特殊符号的名字可能被过滤或报错。所以自己指定时用纯字母数字最保险。3.4 返回数据的剥离方法拿到abc({data:{...}})这样的字符串后剥离逻辑很简单import json raw abc({data:{f43:1800,f44:1820}}) # 找到第一个左括号和最后一个右括号 start raw.index(() end raw.rindex()) json_str raw[start1:end] data json.loads(json_str) print(data)这段代码的核心就是定位包裹层。用index找第一个(用rindex找最后一个)中间的就是纯 JSON。为什么用rindex而不是index因为 JSON 内部可能也有括号用最后一个右括号才能保证剥干净。4. 实操过程从抓包到稳定取数4.1 第一步定位目标接口打开行情页面F12 打开开发者工具切到 Network勾选 XHR。刷新页面观察请求列表。找到返回内容包含股票数据的那个请求右键选择 Copy as cURL这样能把完整的请求信息URL、headers、cookie一次性复制出来。把 cURL 命令粘贴到文本编辑器里你能清楚看到所有请求头和参数。这一步是整个逆向的地基地基打牢了后面就是照葫芦画瓢。4.2 第二步用 Python 复现请求把 cURL 翻译成 requests 代码。我一般会写成这样import requests import json import time def fetch_stock(secid, fields, callbackmycb): base https://push2.example.com/api/qt/stock/get params { cb: callback, secid: secid, fields: fields, _: int(time.time() * 1000), } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Referer: https://quote.example.com/, Accept: */*, } resp requests.get(base, paramsparams, headersheaders, timeout10) resp.raise_for_status() raw resp.text start raw.index(() end raw.rindex()) return json.loads(raw[start1:end]) result fetch_stock(1.600519, f43,f44,f45,f46,f47,f48) print(result)几个要点timeout一定要设不然网络卡住会一直挂着raise_for_status帮你快速发现 4xx/5xx回调名自己指定成mycb剥离时逻辑一致。4.3 第三步字段编号的反推这是最花时间但也最有意思的一步。我的做法是先请求一大批字段编号比如 f1 到 f100 全要拿到返回后把每个 key 和 value 列出来再打开页面看实际显示的数字一一对照。比如返回里f43: 1800页面上最新价显示1800.00那 f43 就是最新价。f47: 12345页面上成交量显示12345那 f47 就是成交量。反推出来的映射关系建议用表格记下来下次直接查表。字段编号含义备注f43最新价可能带小数位缩放f44最高价同上f45最低价同上f46开盘价同上f47成交量单位可能是手f48成交额单位可能是元注意价格字段有时候返回的是整数需要除以一个缩放因子比如 100才是真实价格。这个缩放因子也要通过对照页面确认。4.4 第四步批量与频率控制单只股票取数跑通后很自然会想批量取。这时候要注意频率。服务端对高频请求是有感知的请求太密可能触发限流表现为返回空、返回错误码或者直接断连。我的经验是单线程请求间隔控制在 0.3 到 0.5 秒批量任务加上重试机制。重试不要无脑重试遇到连续失败先停下来等几秒再继续。下面是一个带重试的简单封装def fetch_with_retry(secid, fields, retries3): for i in range(retries): try: return fetch_stock(secid, fields) except Exception as e: print(fattempt {i1} failed: {e}) time.sleep(1 i) return None重试间隔用1 i做递增避免所有失败请求在同一时刻集中重发。5. 常见问题与排查技巧实录5.1 返回空数据或错误码最常见的原因有三个Referer 缺失、secid 格式错误、字段编号不存在。排查顺序建议是先确认 Referer再确认 secid 的市场代码对不对最后检查 fields 里有没有拼错的编号。5.2 回调剥离报错如果raw.index(()抛异常说明返回内容里根本没有括号可能是服务端返回了纯 JSON 或者错误页。这时候先把raw打印出来看看别急着往下走。如果json.loads报错多半是剥离时多剥或少剥了字符检查一下start和end的位置。5.3 请求被限流表现是前几个请求正常后面开始返回空。解决办法是降频、加随机间隔、必要时换请求头里的 User-Agent。不要用固定间隔猛刷加一点随机性更稳。问题现象可能原因解决方向返回空Referer 缺失补上 Referer返回错误码secid 格式错确认市场代码剥离失败返回非 JSONP打印原始响应请求被限频率过高降频加随机间隔5.4 字段值对不上页面大概率是缩放因子没处理。价格类字段经常是整数存储需要除以 100 或 1000。成交量可能是手也可能是股单位要确认。这个没有捷径只能对照页面逐个核对。注意字段编号和缩放因子可能随接口版本变化今天对的明天不一定对建议定期回归验证。6. 实操心得与后续扩展我在实际做这类接口对接时最大的体会是不要一上来就想着写一个完美的封装库先把单个请求跑通再逐步抽象。很多人卡在第一步就是因为想太多结果连一个能跑的请求都没写出来。另外把反推出来的字段映射和参数规律记在一个文档里比记在脑子里靠谱得多。接口这种东西隔一个月再看细节全忘有文档就能快速捡起来。后续如果想扩展可以考虑把取到的数据存到本地数据库做历史对比或者把字段映射做成配置文件接口变了只改配置不改代码。这些都是很自然的演进方向但前提是先把最基础的那条链路走通。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门