news 2026/9/19 11:33:07

东方财富JSONP行情接口抓取:回调解析与批量稳定实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
东方财富JSONP行情接口抓取:回调解析与批量稳定实战

1. 从一个抓包请求说起:这个接口到底难在哪

很多人第一次接触行情数据抓取,都是从浏览器开发者工具的 Network 面板开始的。打开一个行情页面,筛选 XHR 或者 JS 请求,会看到一堆返回 JSON 的地址。大部分接口复制出来,改个参数就能直接用。但东方财富的行情接口不太一样——你复制出来的请求地址,直接丢进浏览器新标签页打开,返回的往往不是 JSON,而是一段被包裹在函数调用里的文本,类似cb({...})这种形式。这就是所谓的JSONP 回调

JSONP 不是什么新东西,它是早年为了绕过浏览器同源策略而诞生的一种数据交换方式。服务端不直接返回纯 JSON,而是返回一段可执行的 JavaScript 代码,把数据作为参数传给前端预先定义好的回调函数。浏览器用<script>标签加载这段代码,脚本执行时回调函数被调用,数据就到手了。东方财富的很多行情接口至今保留着这套机制,回调函数名通常带jQuery前缀,比如jQuery112409...后面跟一长串数字,这是 jQuery 内部自动生成的回调标识。

这个机制给抓取带来的直接麻烦有三层。第一层,返回内容不是标准 JSON,直接json.loads会报错,得先把外层函数调用剥掉。第二层,回调函数名是动态的,每次请求都不一样,如果你硬编码一个名字,服务端可能不认,或者返回的内容对不上。第三层,这类接口往往还带着cbcallback_这样的查询参数,其中_通常是时间戳,用来防止缓存,缺了或者格式不对都可能拿不到数据。

所以这篇内容要解决的,就是把这套带 jQuery 回调的接口彻底拆开:搞清楚它的请求结构、回调参数怎么处理、返回内容怎么解析、批量抓取时怎么保持稳定。适合有一定 Python 基础、做过简单爬虫、但被 JSONP 卡住的朋友。我会从实际请求出发,把每一步的意图和踩过的坑都讲清楚,代码可以直接拿去改。

提示:本文讨论的是公开行情数据的读取与解析思路,重点在技术原理和工程处理。实际使用时请控制请求频率,遵守目标站点的访问规则,不要对服务造成压力。

2. 拆解请求结构:URL、参数与回调名的生成逻辑

2.1 一个典型请求长什么样

先看一个真实的请求形态。以某只股票的实时行情为例,请求地址大致是这样的结构:

https://push2.eastmoney.com/api/qt/stock/get?cb=jQuery112409...&secid=1.600519&fields=f43,f44,f45&_=1700000000000

拆开看几个关键部分。push2.eastmoney.com是行情推送的域名,/api/qt/stock/get是具体接口路径。查询参数里,secid是标的的唯一标识,格式是市场代码.股票代码,比如1.600519里的1代表沪市,0代表深市。fields指定要返回哪些字段,东方财富用f加数字的方式编码字段,比如f43是最新价,f44是最高价,f45是最低价。cb就是回调函数名,_是时间戳。

这里有个容易忽略的点:fields不是随便填的。你不传这个参数,接口会返回一大堆默认字段,数据量大且很多用不上;你传了不存在的字段编号,返回里对应的键可能直接缺失。所以正确做法是先摸清常用字段的编号含义,按需索取。

2.2 回调名为什么不能写死

很多人图省事,直接把cb写成一个固定值,比如cb=myCallback,然后自己解析myCallback({...})。实测下来,部分接口确实能返回,但另一些接口会校验回调名的格式,或者干脆忽略你传的值,用自己生成的 jQuery 风格名字返回。这就导致解析时匹配不上。

更稳妥的做法是动态生成一个符合 jQuery 风格的回调名,每次请求都换一个。jQuery 的命名规律大致是jQuery+ 一串数字 + 下划线 + 时间戳,比如jQuery112409876543210_1700000000000。你不需要完全复刻它的算法,只要生成一个结构相似、每次唯一的字符串即可。服务端一般只关心这个参数存在且格式合理,不会去校验它是不是真的由 jQuery 生成。

import time import random def gen_callback(): # 模拟 jQuery 风格的回调名,保证每次唯一 rand_part = random.randint(1000000000000000000, 9999999999999999999) ts = int(time.time() * 1000) return f"jQuery{rand_part}_{ts}"

生成之后,请求时把它同时放进cb参数,解析时再用同一个名字去匹配。这样请求和解析就对齐了。

2.3 时间戳参数_的作用

_参数的值是毫秒级时间戳。它的作用是让每次请求的 URL 都不同,从而绕过浏览器和中间层的缓存。对于抓取来说,这个参数必须带上,而且要用当前时间,否则可能拿到旧数据。有些朋友为了"稳定",把时间戳固定住,结果发现数据一直不变,还以为是接口坏了,其实就是缓存命中。

请求头方面,建议至少带上User-AgentRefererReferer设成行情页面的地址,能让请求看起来更自然。这不是为了伪装,而是很多站点的接口会检查来源,缺失时可能返回空数据。

参数是否必需说明
secid必需市场.代码,如 1.600519
fields建议按需指定字段,减少数据量
cb必需回调函数名,需动态生成
_必需毫秒时间戳,防缓存
User-Agent建议标识客户端
Referer建议标识来源页面

3. 剥壳与解析:把 JSONP 还原成可用的字典

3.1 返回内容的真实结构

请求成功后,拿到的响应文本大概是这样:

jQuery112409876543210_1700000000000({"rc":0,"rt":4,"data":{"f43":1680.5,"f44":1700.0,"f45":1650.0}});

注意末尾有个分号,前面是函数调用,参数是一个 JSON 对象。有些接口返回的可能是cb(...)不带分号,或者带额外的空白字符。解析的核心思路就是:找到第一个左括号和最后一个右括号,取中间的部分,就是纯 JSON

这里有个坑:如果数据字段里本身包含括号字符(比如某些文本字段),用简单的字符串查找可能会截错。更稳的方式是用正则匹配,或者先定位回调名,再从回调名之后找第一个(和最后一个)

import json import re def parse_jsonp(text, callback_name): # 方式一:按回调名精确定位 prefix = callback_name + "(" if text.startswith(prefix): inner = text[len(prefix):] # 去掉末尾的 ); 或 ) inner = inner.rstrip() if inner.endswith(";"): inner = inner[:-1] if inner.endswith(")"): inner = inner[:-1] return json.loads(inner) # 方式二:正则兜底 match = re.search(r'^[^(]*\((.*)\)[;\s]*$', text, re.S) if match: return json.loads(match.group(1)) raise ValueError("无法解析的 JSONP 响应")

3.2 为什么优先用回调名匹配

正则兜底虽然通用,但在数据量大、字段复杂时,贪婪匹配.*可能带来性能问题,也可能因为文本里的特殊字符出错。用回调名精确匹配的好处是:你明确知道外层包裹是什么,剥离动作是确定性的,不依赖正则的模糊匹配。只有在回调名对不上(比如服务端忽略了你的cb)时,才退回正则。

实测中还有一种情况:服务端返回的回调名和你传的不完全一致,可能多了或少了下划线部分。这时候可以先从响应文本里提取出实际的回调名,再用它来剥离。提取方法很简单,找第一个(之前的内容即可。

3.3 字段编号的映射处理

剥出 JSON 后,data里的键是f43f44这种编号,直接看不知道是什么。你需要维护一张映射表,把编号翻译成可读的字段名。常用的几个:

字段编号含义
f43最新价
f44最高价
f45最低价
f46开盘价
f47成交量
f48成交额
f60昨收价
f169涨跌额
f170涨跌幅

这张表不用一次记全,按你的需求逐步补充即可。建议把它写成一个字典常量,解析后做一次转换,后续处理就都用可读的键名。

FIELD_MAP = { "f43": "latest", "f44": "high", "f45": "low", "f46": "open", "f47": "volume", "f48": "amount", "f60": "prev_close", "f169": "change", "f170": "change_pct", } def normalize(data): return {FIELD_MAP.get(k, k): v for k, v in data.items()}

注意:不同接口的字段编号含义可能不同,比如行情接口和资金流接口的f编号体系是分开的。不要拿一张表套所有接口,用之前先验证。

4. 批量抓取时的稳定性设计:频率、重试与并发

4.1 单次请求跑通不等于批量可用

单只股票请求成功,和批量抓几百只股票稳定运行,是两码事。批量场景下最容易出的问题有三个:请求太密被限流、偶发失败没有重试、并发太高导致连接被拒。

先说频率。东方财富的行情接口对单 IP 的请求频率是有容忍度的,但绝不是无限制。实测下来,每秒 3 到 5 次请求在多数时段是安全的,再高就可能出现返回空数据或连接超时。这个数字不是固定的,盘中高峰时段会更敏感。所以批量抓取时,一定要在请求之间加间隔,用time.sleep控制节奏。

import time def fetch_with_delay(codes, interval=0.3): results = {} for code in codes: try: results[code] = fetch_one(code) except Exception as e: print(f"{code} 失败: {e}") time.sleep(interval) return results

4.2 重试要区分错误类型

不是所有失败都值得重试。连接超时、返回空内容,这类可以重试;但如果返回的是明确的参数错误(比如 secid 格式不对),重试多少次都没用,只会浪费时间。所以重试逻辑里要判断错误类型。

def fetch_with_retry(code, max_retry=3): for attempt in range(max_retry): try: resp = do_request(code) if resp and resp.get("data"): return resp # 返回空数据,可能是限流,退避后重试 time.sleep(1 * (attempt + 1)) except TimeoutError: time.sleep(1 * (attempt + 1)) except ValueError as e: # 参数类错误,直接抛出,不重试 raise e return None

退避策略用递增间隔,第一次等 1 秒,第二次等 2 秒,第三次等 3 秒。这样既给了服务端缓冲,也避免了自己在短时间内反复冲击。

4.3 并发不是越高越好

有人为了快,直接开几十个线程并发请求。结果往往是大量请求失败,甚至 IP 被临时限制。对于这类公开接口,并发数控制在 3 到 5 个线程比较稳妥。再配合每个线程内部的请求间隔,整体吞吐量其实够用。

如果确实需要抓大量标的,更好的思路是分批 + 长间隔。比如每批 50 只,批内用 3 个线程,批间休息几秒。这样对服务端友好,自己的成功率也高。

策略并发数单请求间隔适用场景
保守10.5s少量标的,追求稳定
均衡30.3s中等批量,日常使用
激进50.2s短时批量,需监控失败率

提示:无论用哪种策略,都建议记录失败列表,事后单独补抓,而不是在失败时无限重试拖慢整体进度。

5. 那些文档不会告诉你的实操细节

5.1 secid 的市场代码别搞反

secid的格式是市场.代码,沪市是1,深市是0。这个顺序很多人会记反,写成600519.1,结果接口返回空。判断方法很简单:6 开头的股票是沪市,0 和 3 开头的是深市。北交所的标的用0还是别的代码,需要单独验证,不要想当然。

另外,指数、基金、债券也各有自己的 secid 规则。比如上证指数是1.000001,深证成指是0.399001。抓之前先确认标的类型,别拿股票的规则去套指数。

5.2 返回数据里的-要当心

行情接口在非交易时段或者标的停牌时,某些字段会返回-而不是数字。如果你直接拿去做数值计算,会报类型错误。解析后要做一次清洗,把-转成None或者 0,视你的业务逻辑而定。

def clean_value(v): if v == "-" or v is None: return None return v

这个细节在盘后批量处理时特别重要,因为盘后很多字段都是-,不清洗的话整个流程会崩。

5.3 回调名里的数字长度有讲究

前面生成回调名时用了 19 位随机数,这是模仿 jQuery 的实际格式。如果你只生成几位数字,虽然多数情况下也能用,但偶尔会遇到服务端返回的回调名和你传的不一致。实测发现,位数接近 jQuery 真实格式时,匹配成功率更高。这不是玄学,而是服务端可能对回调名做了格式校验,位数太短会被判定为非法。

5.4 编码问题别忽视

响应内容默认是 UTF-8,但如果你用某些 HTTP 库没正确设置编码,中文可能变成乱码。虽然行情数据主要是数字,但标的名称等字段是中文。请求后显式设置resp.encoding = "utf-8",能避免很多莫名其妙的乱码问题。

6. 从单接口到通用框架的演进思路

6.1 把回调处理抽象成独立模块

当你抓的不止一个接口时,会发现 JSONP 的处理逻辑是通用的。这时候应该把它抽成一个独立函数或类,输入是响应文本和回调名,输出是解析后的字典。这样每个接口的抓取代码只需要关注 URL 和参数,解析部分复用。

class JsonpParser: def __init__(self, callback_name): self.callback_name = callback_name def parse(self, text): # 复用前面的解析逻辑 ...

6.2 参数构造也值得封装

不同接口的 URL 路径不同,但参数构造有共性:都要 secid、都要 cb、都要时间戳。可以写一个基础请求构造器,把公共参数处理好,各接口只传差异部分。这样新增接口时,改动量很小。

6.3 字段映射按接口分组维护

前面提到不同接口的字段编号体系不同。建议按接口分组维护映射表,比如STOCK_QUOTE_FIELDSFUND_FLOW_FIELDS,而不是混在一张大表里。这样查起来清晰,也不容易串。

6.4 日志和监控不能省

批量抓取时,一定要记录每次请求的结果:成功、失败、耗时、返回数据量。这些日志在排查问题时非常有用。比如你发现某段时间失败率突然升高,看日志就知道是限流了还是接口变了。没有日志,只能靠猜。

import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") def fetch_one(code): start = time.time() try: data = do_fetch(code) logging.info(f"{code} 成功 耗时{time.time()-start:.2f}s") return data except Exception as e: logging.error(f"{code} 失败 {e}") raise

这套东西看起来简单,但真正批量跑起来之后,你会发现日志是你唯一能依赖的排查依据。

7. 几个常见报错的排查路径

遇到问题不要急着改代码,先按路径排查。下面是我实际遇到过的几类报错和对应的排查顺序。

返回内容为空或只有回调壳没有数据。先检查 secid 格式对不对,市场代码有没有搞反。然后看fields参数是不是传了不存在的编号。最后确认请求头里的Referer有没有带。这三步能解决大部分空数据问题。

解析时报 JSON 格式错误。把原始响应文本打印出来看,确认外层包裹是不是你预期的回调名。如果回调名对不上,说明服务端忽略了你传的cb,需要用正则兜底或者从响应里提取实际回调名。

批量抓取中途大量失败。先看失败是不是集中在某个时间段,如果是,大概率是限流,降低频率即可。如果是一直失败,检查是不是 IP 被临时限制了,换个时间再试。不要在这个时候加大重试力度,只会更糟。

数值字段出现-导致计算报错。这是数据清洗没做好,在解析后加一层清洗逻辑,把非数值内容统一处理掉。

中文乱码。检查响应编码设置,显式指定 UTF-8。

这张排查表可以贴在代码注释里,出问题时按顺序过一遍,比盲目搜索高效得多。

现象优先排查次要排查
返回空数据secid 格式Referer、fields
JSON 解析失败回调名匹配响应文本实际格式
批量中途失败请求频率IP 限制
数值计算报错数据清洗字段类型
中文乱码响应编码请求头编码

8. 关于这套方案的边界和个人体会

这套基于 JSONP 解析的抓取方案,核心价值在于把"看起来不能直接用"的接口变成"可以稳定解析"的数据源。它的适用范围是那些仍然保留回调机制的公开行情接口。如果哪天接口改成了纯 JSON 返回,剥壳这一步就可以省掉,但参数构造、频率控制、重试、日志这些工程层面的东西依然适用。

我个人在实际使用中最大的体会是:抓取代码的难点从来不在解析本身,而在稳定性。解析逻辑写一次就固定了,但频率控制、错误处理、日志监控这些,才是决定你能不能长期跑下去的关键。很多人卡在"单次能跑通"就以为完成了,结果一上批量就各种问题。把重试、退避、日志这些基础设施先搭好,后面会省很多事。

另外一点,字段映射表要随用随补,不要试图一次搞全。你用到哪个字段就查哪个,慢慢积累,比一开始就追求完整映射实际得多。行情接口的字段编号有几百个,全记下来既没必要也记不住。

最后分享一个小技巧:调试阶段把每次请求的完整 URL 和原始响应都打印出来,存到文件里。出问题时直接翻文件,比重新发请求复现快得多。这个习惯帮我省了大量排查时间。

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

从写Demo到落地中后台:Vue 3学习路线与工程化实践

从“照着文档能写出计数器”到“真拿到一个中后台项目就发懵”&#xff0c;这个断层我猜很多学 Vue 的人都经历过。后来我才想明白&#xff0c;问题不在于 API 背得少&#xff0c;而是把 Vue 当成了一个接受“点对点输入”的工具&#xff0c;没有把它放进真实工程里去理解边界。…

作者头像 李华
网站建设 2026/9/19 11:32:16

MCP协议与Serverless融合:重构企业AI工具调度架构

简介&#xff1a;在AI Agent大规模落地企业场景的进程中&#xff0c;模型能力本身往往不是瓶颈&#xff0c;真正决定智能化上限的&#xff0c;是模型能否稳定、安全地调用内部工具与服务。MCP&#xff08;Model Context Protocol&#xff09;正是为解决这一痛点而生的标准化协议…

作者头像 李华
网站建设 2026/9/19 11:31:11

火场监控去烟:残差检测与Dense U-Net串联网络复现

简介&#xff1a;这份PDF文献面向图像处理、计算机视觉方向的研究者与消防救援技术相关人员&#xff0c;聚焦火场灰度图像中烟雾干扰导致监控画面模糊、对比度下降的问题&#xff0c;提出一套基于深度学习的去烟算法方案。资源为单篇学术论文&#xff0c;压缩包内仅含1个PDF文件…

作者头像 李华
网站建设 2026/9/19 11:31:07

Flutter for OpenHarmony网络层封装与dio实战

1. Flutter for OpenHarmony 网络层封装实战在跨平台开发领域&#xff0c;Flutter for OpenHarmony 作为华为推出的创新解决方案&#xff0c;为开发者提供了全新的技术可能性。作为一名长期深耕 Flutter 生态的开发者&#xff0c;我在实际项目中深刻体会到网络层封装的重要性。…

作者头像 李华
网站建设 2026/9/19 11:30:56

OpenClaw部署避坑实录:本地折腾不如云服务器稳定

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华