拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python抓取东方财富A股K线数据:无需Token的接口直连方案

Python抓取东方财富A股K线数据:无需Token的接口直连方案

先聊点实在的。做A股数据抓取,很多人一上来就想到去装tushare、baostock这类现成库,但它们要么需要注册token,要么对历史数据条数有限制,要么接口字段不够全。我在实际做量化回测和个人盯盘工具的时候,越来越倾向直接对接数据源本身。东方财富作为国内活跃度极高的财经平台,它的行情接口设计得很规整,返回的是标准JSON,不需要登录、不需要签名、不需要Token,而且字段覆盖了开高低收、成交量、成交额、换手率等常用指标。这篇文章就带你把这条路径跑通,目标是——在5分钟内,用Python稳定抓到东方财富的A股K线数据,拿到可以直接入库或用于分析的干净DataFrame。

适合谁来参考?如果你正在做股票数据可视化、写量化策略回测脚本,或者想给自己的分析工具补上实时行情源,这篇都很对口。如果你是完全零基础,也不用慌,代码可以直接跑通,我会把每一步的原理和坑点都拆开讲清楚。

1. 方案选型:为什么直接抓东方财富接口,而不是用tushare

1.1 现成库的痛点

很多人第一反应是:用tushare不就行了吗?确实,tushare做了很好的数据封装,但它有几个在实际使用中绕不开的麻烦。首先是权限问题,tushare很多高频或全量接口需要积分,积分要通过贡献或付费获取,对个人开发者来说门槛不低。其次是稳定性,我有段时间用tushare跑定时任务,偶尔会遇到服务端限流或返回超时,追查起来比较被动。当然tushare的数据质量是好的,这里只是说在“单纯想要最新行情数据”这个场景下,它未必是最轻量的选择。

再看baostock,它胜在免费且无需注册,但数据更新频率偏慢,分钟级数据支持也不够灵活。如果只是做日线级别的选股扫描,勉强能用;一旦涉及到盘中实时行情,或者需要自定义复权方式,它就比较吃力了。所以我干脆研究了一下东方财富网页端的请求链路,发现它有一套非常成熟的行情接口,直接通过HTTP请求就能拿数。

1.2 东方财富接口的核心优势

那东方财富这套接口到底好在哪?我总结了几个关键点:

  • 公开可用:普通用户在网页端看行情时,浏览器发起的就是这些请求,没有任何加密签名。
  • 数据全面:日线、周线、月线、分钟线都能给,字段包含前收盘、今开盘、最高、最低、成交量、成交额、振幅、换手率等,已经覆盖绝大部分分析需求。
  • 响应速度快:请求一个股票的全部日线数据,通常300毫秒左右就能返回,比渲染完整网页再解析要快一个数量级。
  • 不需要维护登录状态:Cookie、Session都不用管,requests直接打过去就是200。

这套方案的本质,是直接绕过网页渲染层,去拿数据接口返回的JSON。这种思路在爬虫里叫“接口直连”,比解析HTML或者用playwright模拟浏览器都要稳定,因为JSON结构是固定的,只要接口不升版本,代码就能一直跑。

1.3 什么时候才需要playwright这类渲染工具

想必你也在热搜词里看到了playwright。这里顺便说清楚适用场景。playwright这类浏览器自动化工具,适合对付那种数据是通过JavaScript动态渲染、并且接口有加密参数的网站。比如有些站点请求接口时要带动态token,token是前端JS算出来的,你直接requests请求拿不到数据,那就必须用playwright或pyppeteer去构造一个真实浏览器环境。

但在东方财富这个场景里,不需要走这条路。即便页面上的K线图是JS画的,它的数据源头还是那个JSON接口,URL可以直接拼出来。永远记得一个原则:优先找接口,找不到接口再考虑渲染。用playwright爬静态信息比较笨重,而且消耗资源。

2. 核心细节解析:看懂接口URL的每个参数

2.1 接口原型拆解

我们实际要用的接口长这样:

https://push2his.eastmoney.com/api/qt/stock/kline/get?secid=1.600519&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt=101&fqt=1&beg=0&end=20500101

看着长,其实规律很强。我做接口分析的习惯是先把一个完整URL丢到浏览器里,看返回的JSON长什么样,再回头反推每个参数的作用。这个接口的关键参数我整理成了下表:

参数含义示例值补充说明
secid证券唯一标识1.600519前缀1代表沪市,0代表深市,后面是股票代码
kltK线周期101101日线,102周线,103月线,60分钟线,30分钟线等
fqt复权方式10不复权,1前复权,2后复权
beg开始日期00表示从最早数据开始
end结束日期20500101用一个超大日期表示取到最新
fields1基础字段组f1-f6固定写法即可
fields2K线数据字段组f51-f61每一列对应的具体指标

fields2这组参数决定了数据返回什么,我逐个拆一下:f51是日期,f52是开盘价,f53是收盘价,f54是最高价,f55是最低价,f56是成交量,f57是成交额,f58是振幅,f59是涨跌幅,f60是涨跌额,f61是换手率。这个顺序是固定的,后面解析的时候直接对应列名即可。

2.2 secid编码规则与常见坑点

secid是这套接口里最容易出错的参数。规则是市场前缀加股票代码,1开头是上海证券交易所,0开头是深圳证券交易所。但这里有个细节特别坑:北交所的股票前缀是0还是1?我实测过,北交所用的是0。所以判断规则不能简单理解为“6开头就一定是1”,比如科创板股票688开头,它依然在上海交易所,前缀就是1。深市的000、002、300前缀都是0。

实际操作中,最稳的方法是用接口去自动匹配,而不是人肉判断。东方财富有一个搜索接口,输入股票名称或代码,返回里带secid:

https://searchapi.eastmoney.com/api/suggest/get?input=贵州茅台&type=14&token=D43BF722C8E33BDC906FB84D85E326E8

我平时会先跑这个搜索接口拿secid,再交给行情接口去拉数据。这样写代码就会更健壮,不怕用户输入的是“贵州茅台”还是“600519”,统一转换成标准secid。

2.3 复权因子的选择逻辑

复权这里值得多说几句。fqt参数有三个值:0是不复权,1是前复权,2是后复权。做技术分析的朋友最好统一用前复权数据,因为前复权会以当前价格为基准,对历史价格做调整,这样均线、MACD这些指标的计算才不会因为除权除息出现假信号。

但如果你要做的是计算真实收益率或回测资金曲线,前复权的历史价格是失真的——因为它是站在今天往回看的视角。后复权则相反,它保持历史价格是真实价格,调整的是当前价格。我做回测时,习惯同时拉一份不复权和一份前复权,不复权用于计算真实涨跌幅,前复权用于画技术指标图,各有分工。

3. 完整代码实现:从零搭建一个可复用的爬虫脚本

3.1 基础版:核心请求逻辑

先把最核心的部分写出来。这一步的目标就是——用最小代码量,把数据完整拿下来。

import requests import pandas as pd def get_kline(secid: str, klt: int = 101, fqt: int = 1) -> pd.DataFrame: url = "https://push2his.eastmoney.com/api/qt/stock/kline/get" params = { "secid": secid, "klt": klt, "fqt": fqt, "beg": "0", "end": "20500101", "fields1": "f1,f2,f3,f4,f5,f6", "fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61", } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() if data.get("data") is None or data["data"].get("klines") is None: raise ValueError(f"接口返回异常: {data}") rows = [line.split(",") for line in data["data"]["klines"]] df = pd.DataFrame(rows, columns=[ "date", "open", "close", "high", "low", "volume", "amount", "amplitude", "pct_chg", "change", "turnover" ]) for col in ["open", "close", "high", "low", "volume", "amount", "amplitude", "pct_chg", "change", "turnover"]: df[col] = pd.to_numeric(df[col], errors="coerce") return df if __name__ == "__main__": df = get_kline("1.600519", klt=101, fqt=1) print(df.tail(10))

这段代码跑起来是什么效果?你会看到最近10个交易日的日K线数据,日期、开高低收、成交量、涨跌幅都在里面。从发起请求到打印结果,没有多余的噪音,整个流程能控制在半秒以内。

3.2 为什么用params而不是直接拼URL

注意我上面用的是requests的params参数,而不是把完整的URL字符串直接拼出来。这个习惯很重要。当你的URL参数比较多的时候,如果手动拼字符串,很容易漏掉某个参数,或者因为某个值里带了特殊字符导致URL解析失败。requests的params会自动帮你处理URL编码,同时还能保持代码更易读。

还有一点,requests默认会带上Accept-Encoding,服务器返回的数据通常是gzip压缩过的。requests会自动解压,这一层我们不需要手动处理。headers里加一个常见的User-Agent就行,主要是为了模拟浏览器环境,有些反爬策略会拦截默认的python-requests标识。

3.3 进阶版:自动识别secid与批量拉取

基础版跑通之后,我建议直接把它升级成带自动识别secid的版本。这一版在实际工程里更好用,因为你不需要记住前缀规则,直接丢代码或者名字给它就行。

import requests import pandas as pd from typing import Union SEARCH_URL = "https://searchapi.eastmoney.com/api/suggest/get" def get_secid(keyword: str) -> str: params = { "input": keyword, "type": "14", "token": "D43BF722C8E33BDC906FB84D85E326E8", } resp = requests.get(SEARCH_URL, params=params, timeout=10) data = resp.json() items = data.get("QuotationCodeTable", {}).get("Data", []) if not items: raise ValueError(f"未找到标的: {keyword}") item = items[0] market_type = str(item.get("MarketType", "")) code = item.get("Code", "") secid = f"{market_type}.{code}" return secid def get_kline_by_keyword(keyword: str, klt: int = 101, fqt: int = 1) -> pd.DataFrame: secid = get_secid(keyword) return get_kline(secid, klt, fqt) if __name__ == "__main__": df = get_kline_by_keyword("贵州茅台", klt=101, fqt=1) print(df.tail(5))

搜索接口返回的数据里有一个MarketType字段,这个字段的值就是secid前缀。我实测过,贵州茅台返回1,平安银行返回0,跟规则完全吻合。不过要注意的是,搜索接口可能会返回多个匹配项,比如你搜“茅台”,可能同时返回贵州茅台和茅台相关基金。稳妥的做法是取第一条,或者根据用户输入是6位数字还是中文字符做不同处理。如果输入的是纯数字代码,直接判断首位:6开头用1前缀,0、2、3开头用0前缀,这也是一种可靠的兜底方案。

3.4 分钟级数据的玩法

除了日线,分钟级数据其实是很多做短线的朋友关心的高频数据。klt参数这里需要重点说明,我整理出一份常用对照表:

klt值周期适用场景
11分钟线日内T+0分析
55分钟线短线择时
1515分钟线波段判断
3030分钟线日内趋势
6060分钟线日内趋势
101日线标准技术分析
102周线中期趋势
103月线长期趋势

分钟级数据有一点要注意:beg和end参数对分钟线的数据条数有实际影响。东方财富的分钟线接口单次最多返回大约57000条数据,对于1分钟线来说,大约能覆盖近3个月。如果需求是拉取一年以上的分钟数据,建议按时间切片分段请求,最后再拼接。

def get_kline_period(secid: str, klt: int, start: str, end: str, fqt: int = 1) -> pd.DataFrame: url = "https://push2his.eastmoney.com/api/qt/stock/kline/get" params = { "secid": secid, "klt": klt, "fqt": fqt, "beg": start, "end": end, "fields1": "f1,f2,f3,f4,f5,f6", "fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61", } headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() rows = [line.split(",") for line in data["data"]["klines"]] df = pd.DataFrame(rows, columns=[ "date", "open", "close", "high", "low", "volume", "amount", "amplitude", "pct_chg", "change", "turnover" ]) return df

调用方式也很简单:get_kline_period("1.600519", 1, "2024-01-01", "2024-03-01"),一次性把2024年头两个月的1分钟线全拉下来。如果你要跑连续几个月的分钟数据,就在外面套一个日期列表循环,每次请求之间sleep 0.5秒,避免给服务器造成压力。

4. 实操中遇到的典型问题与排查技巧

4.1 返回格式不对或字段缺失

这个是最常见的问题。核心原因通常是fields2参数写错了,或者接口更新了返回结构。遇到这种情况,我的排查思路很固定:先直接在浏览器里打开接口URL,看原始JSON结构。如果浏览器能正常显示,说明接口本身没问题,那就是Python请求的参数出了问题。拿浏览器里的JSON和代码返回的JSON逐字段对比,很容易定位到差异。

另外注意,接口返回的data字段为空时,大概率是secid不对。比如把深市股票配成了1.000001,接口不会报错,但返回的data是null。这种“静默失败”特别容易让人怀疑是网络问题,其实是市场前缀搞错了。

4.2 请求频率限制与封禁风险

东方财富的接口对普通的低频率请求比较宽松,但你若用for循环一次性拉几千只股票,每只间隔不到100毫秒,很容易触发服务端的限流,表现是请求返回超时或者直接拒绝连接。我实测下来,其实不需要去试探它的极限在哪里,更合理的做法是:把批量任务控制在每秒1到2次请求,并对每只股票做一次指数退避重试。

import time import random def fetch_with_retry(secid: str, retries: int = 3) -> pd.DataFrame: for i in range(retries): try: return get_kline(secid) except Exception as e: wait = (i + 1) * 1 + random.random() print(f"第{i+1}次请求失败,{wait:.1f}秒后重试: {e}") time.sleep(wait) raise RuntimeError(f"重试{retries}次仍然失败: {secid}")

这种重试机制能大幅度提升批量任务的成功率。重试间隔用随机值而不是固定值,是为了避免多个线程同时重试时再次撞在一起形成“惊群效应”,这是并发编程里的一个教训。

4.3 pandas列类型数值化

从接口拿到的原始数据,所有字段都是字符串。如果你直接用df["close"]去做计算,会得到一堆字符串拼接的结果,比如“1699”+“1”得到“16991”。这就尴尬了。

解决方式是在DataFrame创建后立即做类型转换。我在上面的代码里用pd.to_numeric把所有价格和成交量字段一次性转成数值。这一步是标准操作,不要偷懒。坐标轴画图、计算收益率、做因子分析,全部依赖于数值列。

4.4 时区与日期边界问题

东方财富返回的日期字符串是东八区时间,不携带时区信息。如果你在本地跑脚本,且本地时区不是东八区,直接用date列去做时间聚合时可能会出现偏移。建议在拿到数据后统一清洗一次:

df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").tz_localize("Asia/Shanghai")

对于做A股日线分析,把索引统一成带时区的东八区时间,后续跟其他数据源做对齐时能少踩很多坑。另外要注意交易日历问题,A股有节假日休市,数据中的日期就是实际的交易日,不需要自己额外过滤周末。

5. 扩展场景:从K线数据到完整的量化分析流程

5.1 计算技术指标

数据抓回来之后,必然要做的一件事是算指标。很多人会引入TA-Lib,但TA-Lib在Windows上安装比较折腾。我建议日常用pandas直接算,比如20日均线、MACD、RSI这些都不复杂。

df["ma20"] = df["close"].rolling(window=20).mean() df["ma60"] = df["close"].rolling(window=60).mean() ema12 = df["close"].ewm(span=12, adjust=False).mean() ema26 = df["close"].ewm(span=26, adjust=False).mean() df["dif"] = ema12 - ema26 df["dea"] = df["dif"].ewm(span=9, adjust=False).mean() df["macd"] = (df["dif"] - df["dea"]) * 2

这些列加进去之后,你就可以直接画K线叠加均线和MACD副图。整个分析链路从数据获取到可视化,不需要依赖任何第三方数据库,一个脚本全搞定。

5.2 批量抓取全市场股票列表

除了单只股票K线,有时需要全市场股票列表。东方财富同样提供了接口,一秒就能拉回所有A股代码:

https://push2.eastmoney.com/api/qt/clist/get?pn=1&pz=5000&po=1&np=1&fltt=2&invt=2&fid=f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23&fields=f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f14

fs参数的含义我解释一下:m:0代表深市,m:1代表沪市,t:6是深市主板,t:80是创业板,t:2是沪市主板,t:23是科创板。这样组合起来,正好覆盖所有A股标的。返回的f12是股票代码,f14是股票名称,f2是最新价。拿到这个列表后,再配合前面的get_kline,就能实现全市场数据扫描。

5.3 数据落库与增量更新

等到数据量大了,建议从DataFrame落盘到SQLite或CSV。我自己喜欢用SQLite,因为轻量、不需要单独起服务,一个文件搞定。增量更新时先查一下库里已有的最大日期,再从这个日期开始拉取新数据,直接append,效率高也不会重复存储。

import sqlite3 def save_to_sqlite(df: pd.DataFrame, db_path: str, table_name: str) -> None: conn = sqlite3.connect(db_path) df.to_sql(table_name, conn, if_exists="append", index=False) conn.close()

配合一个定时任务工具,每交易日收盘后自动拉一次数据入库,就形成了一套完整的个人行情数据库。

6. 合规与长期使用的几点建议

无论是抓数据做个人分析,还是用于学习研究,都要注意几个边界问题。首先,东方财富这些接口本身是网页端公开使用的数据通道,个人以合理频率取数用于学习和研究,风险和压力相对较小。但千万不要拿去做成对外提供数据服务的商业产品,这会直接触及数据版权问题。

其次是代码的维护成本。接口字段和URL有变动可能,建议代码里把URL和字段列表集中定义为常量,不要散落在各个函数里。这样接口一旦调整,只需要改一处。

最后,关于“因爬虫入狱”这类热搜词的提醒:爬虫本身不违法,但需要关注抓取对象的协议、频率和个人隐私信息。做A股行情数据抓取,只取公开行情,不碰用户信息,不绕过权限控制,频次保持在合理区间,就不会触及红线。用技术做正经事,这条路才能走得长。

根据我个人的经验,东方财富这套接口是个人开发者获取A股行情的一个非常顺手的源头,比起注册各种数据平台帐号要省心得多。用requests直连拿JSON,代码写起来干净,跑起来稳定,扩展空间也很大。建议你照着上面的代码先跑通一只股票,再逐步扩展成自己的全市场行情工具箱。

返回列表