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

发布时间:2026/9/19 11:33:18
东方财富JSONP行情接口抓取:回调解析与批量稳定实战 1. 从一个抓包请求说起这个接口到底难在哪很多人第一次接触行情数据抓取都是从浏览器开发者工具的 Network 面板开始的。打开一个行情页面筛选 XHR 或者 JS 请求会看到一堆返回 JSON 的地址。大部分接口复制出来改个参数就能直接用。但东方财富的行情接口不太一样——你复制出来的请求地址直接丢进浏览器新标签页打开返回的往往不是 JSON而是一段被包裹在函数调用里的文本类似cb({...})这种形式。这就是所谓的JSONP 回调。JSONP 不是什么新东西它是早年为了绕过浏览器同源策略而诞生的一种数据交换方式。服务端不直接返回纯 JSON而是返回一段可执行的 JavaScript 代码把数据作为参数传给前端预先定义好的回调函数。浏览器用script标签加载这段代码脚本执行时回调函数被调用数据就到手了。东方财富的很多行情接口至今保留着这套机制回调函数名通常带jQuery前缀比如jQuery112409...后面跟一长串数字这是 jQuery 内部自动生成的回调标识。这个机制给抓取带来的直接麻烦有三层。第一层返回内容不是标准 JSON直接json.loads会报错得先把外层函数调用剥掉。第二层回调函数名是动态的每次请求都不一样如果你硬编码一个名字服务端可能不认或者返回的内容对不上。第三层这类接口往往还带着cb、callback、_这样的查询参数其中_通常是时间戳用来防止缓存缺了或者格式不对都可能拿不到数据。所以这篇内容要解决的就是把这套带 jQuery 回调的接口彻底拆开搞清楚它的请求结构、回调参数怎么处理、返回内容怎么解析、批量抓取时怎么保持稳定。适合有一定 Python 基础、做过简单爬虫、但被 JSONP 卡住的朋友。我会从实际请求出发把每一步的意图和踩过的坑都讲清楚代码可以直接拿去改。提示本文讨论的是公开行情数据的读取与解析思路重点在技术原理和工程处理。实际使用时请控制请求频率遵守目标站点的访问规则不要对服务造成压力。2. 拆解请求结构URL、参数与回调名的生成逻辑2.1 一个典型请求长什么样先看一个真实的请求形态。以某只股票的实时行情为例请求地址大致是这样的结构https://push2.eastmoney.com/api/qt/stock/get?cbjQuery112409...secid1.600519fieldsf43,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写成一个固定值比如cbmyCallback然后自己解析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 fjQuery{rand_part}_{ts}生成之后请求时把它同时放进cb参数解析时再用同一个名字去匹配。这样请求和解析就对齐了。2.3 时间戳参数_的作用_参数的值是毫秒级时间戳。它的作用是让每次请求的 URL 都不同从而绕过浏览器和中间层的缓存。对于抓取来说这个参数必须带上而且要用当前时间否则可能拿到旧数据。有些朋友为了稳定把时间戳固定住结果发现数据一直不变还以为是接口坏了其实就是缓存命中。请求头方面建议至少带上User-Agent和Referer。Referer设成行情页面的地址能让请求看起来更自然。这不是为了伪装而是很多站点的接口会检查来源缺失时可能返回空数据。参数是否必需说明secid必需市场.代码如 1.600519fields建议按需指定字段减少数据量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里的键是f43、f44这种编号直接看不知道是什么。你需要维护一张映射表把编号翻译成可读的字段名。常用的几个字段编号含义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, interval0.3): results {} for code in codes: try: results[code] fetch_one(code) except Exception as e: print(f{code} 失败: {e}) time.sleep(interval) return results4.2 重试要区分错误类型不是所有失败都值得重试。连接超时、返回空内容这类可以重试但如果返回的是明确的参数错误比如 secid 格式不对重试多少次都没用只会浪费时间。所以重试逻辑里要判断错误类型。def fetch_with_retry(code, max_retry3): 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_FIELDS、FUND_FLOW_FIELDS而不是混在一张大表里。这样查起来清晰也不容易串。6.4 日志和监控不能省批量抓取时一定要记录每次请求的结果成功、失败、耗时、返回数据量。这些日志在排查问题时非常有用。比如你发现某段时间失败率突然升高看日志就知道是限流了还是接口变了。没有日志只能靠猜。import logging logging.basicConfig(levellogging.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、fieldsJSON 解析失败回调名匹配响应文本实际格式批量中途失败请求频率IP 限制数值计算报错数据清洗字段类型中文乱码响应编码请求头编码8. 关于这套方案的边界和个人体会这套基于 JSONP 解析的抓取方案核心价值在于把看起来不能直接用的接口变成可以稳定解析的数据源。它的适用范围是那些仍然保留回调机制的公开行情接口。如果哪天接口改成了纯 JSON 返回剥壳这一步就可以省掉但参数构造、频率控制、重试、日志这些工程层面的东西依然适用。我个人在实际使用中最大的体会是抓取代码的难点从来不在解析本身而在稳定性。解析逻辑写一次就固定了但频率控制、错误处理、日志监控这些才是决定你能不能长期跑下去的关键。很多人卡在单次能跑通就以为完成了结果一上批量就各种问题。把重试、退避、日志这些基础设施先搭好后面会省很多事。另外一点字段映射表要随用随补不要试图一次搞全。你用到哪个字段就查哪个慢慢积累比一开始就追求完整映射实际得多。行情接口的字段编号有几百个全记下来既没必要也记不住。最后分享一个小技巧调试阶段把每次请求的完整 URL 和原始响应都打印出来存到文件里。出问题时直接翻文件比重新发请求复现快得多。这个习惯帮我省了大量排查时间。