东方财富上证50成分股接口记录
这次为了给本地 DuckDB 行情库补 2026 年上证50成分股,抓了一下东方财富的数据页面:
https://data.eastmoney.com/other/index/
页面本身不是直接把表格写在 HTML 里,而是前端 JS 调用东方财富 datacenter 接口。对上证50来说,实际用到的接口是:
https://datacenter-web.eastmoney.com/api/data/v1/get
接口参数
核心参数如下:
params = {
"reportName": "RPT_INDEX_TS_COMPONENT",
"columns": (
"SECUCODE,SECURITY_CODE,TYPE,SECURITY_NAME_ABBR,CLOSE_PRICE,"
"INDUSTRY,REGION,WEIGHT,EPS,BPS,ROE,TOTAL_SHARES,FREE_SHARES,FREE_CAP"
),
"quoteColumns": "f2,f3",
"quoteType": "0",
"source": "WEB",
"client": "WEB",
"filter": '(TYPE="2")',
"pageNumber": "1",
"pageSize": "100",
"sortColumns": "SECURITY_CODE",
"sortTypes": "1",
}
headers = {
"Referer": "https://data.eastmoney.com/other/index/sz50.html",
"User-Agent": "Mozilla/5.0",
}
几点要注意:
reportName=RPT_INDEX_TS_COMPONENT是指数成分股报表。- 上证50对应
TYPE="2"。这个是在页面 JS 里看到的分支逻辑:indexcode == '000016' ? '2'。 sortTypes不能传"asc"或"desc",要传数字字符串,比如"1"。否则接口会返回“排序顺序字段不能为非数字类型”。- 返回结果里
SECUCODE已经带交易所后缀,例如600519.SH。
最小抓取代码
import requests
url = "https://datacenter-web.eastmoney.com/api/data/v1/get"
params = {
"reportName": "RPT_INDEX_TS_COMPONENT",
"columns": (
"SECUCODE,SECURITY_CODE,TYPE,SECURITY_NAME_ABBR,CLOSE_PRICE,"
"INDUSTRY,REGION,WEIGHT,EPS,BPS,ROE,TOTAL_SHARES,FREE_SHARES,FREE_CAP"
),
"quoteColumns": "f2,f3",
"quoteType": "0",
"source": "WEB",
"client": "WEB",
"filter": '(TYPE="2")',
"pageNumber": "1",
"pageSize": "100",
"sortColumns": "SECURITY_CODE",
"sortTypes": "1",
}
headers = {
"Referer": "https://data.eastmoney.com/other/index/sz50.html",
"User-Agent": "Mozilla/5.0",
}
r = requests.get(url, params=params, headers=headers, timeout=30)
r.raise_for_status()
payload = r.json()
rows = payload["result"]["data"]
print(len(rows))
print(rows[0])
2026-07-07 抓取时返回 50 条记录。
字段含义
这次我主要保留以下字段作为来源记录:
| 字段 | 含义 |
|---|---|
SECUCODE |
带交易所后缀的证券代码,例如 600519.SH |
SECURITY_CODE |
六位证券代码 |
TYPE |
指数类型编码,上证50为 2 |
SECURITY_NAME_ABBR |
证券简称 |
CLOSE_PRICE |
当前展示收盘价/最新价字段 |
INDUSTRY |
行业 |
REGION |
地区 |
WEIGHT |
指数权重 |
EPS |
每股收益 |
BPS |
每股净资产 |
ROE |
净资产收益率 |
TOTAL_SHARES |
总股本 |
FREE_SHARES |
自由流通股本 |
FREE_CAP |
自由流通市值 |
写入本地 DuckDB 的原则
我的本地库里已经有一个历史成分表:
choice_sse50_constituents_2022_2025
虽然表名里带 2022_2025,但它实际承担的是上证50年度快照表的角色。策略只应该读这一张 canonical 表,不应该因为换了来源就多读一张临时表。
所以这次导入原则是:
- 东方财富接口返回保存成 CSV/JSON 文件,作为来源凭证。
- DuckDB 里只把标准化后的
snapshot_year=2026成分写入历史成分表。 - 不在 DuckDB 里保留
eastmoney_*这类临时输入表,避免以后策略层误用。
本次标准化后的字段:
index_key
index_name
choice_sector_code
snapshot_year
snapshot_date
code
local_code
stock_code
stock_name
continuous_start_snapshot_date
continuous_start_precision
其中:
code = 600519.SH
local_code = sh600519
DuckDB 写入 SQL
导入时先删除同一年旧快照,再写入新快照:
delete from choice_sse50_constituents_2022_2025
where index_key = 'sse50'
and snapshot_year = 2026;
然后把标准化后的 DataFrame 注册成 DuckDB 临时视图,插入 canonical 表:
con.register("choice_import", df[choice_cols])
con.execute("""
insert into choice_sse50_constituents_2022_2025
select * from choice_import
""")
con.unregister("choice_import")
校验
导入后用下面的 SQL 检查每年快照数量:
select
snapshot_year,
count(*) rows_n,
count(distinct code) codes,
min(snapshot_date) min_snapshot,
max(snapshot_date) max_snapshot
from choice_sse50_constituents_2022_2025
where index_key = 'sse50'
group by 1
order by 1;
2026-07-07 这次导入结果:
snapshot_year rows_n codes min_snapshot max_snapshot
2026 50 50 2026-07-07 2026-07-07
再检查代码格式:
select code, stock_name
from choice_sse50_constituents_2022_2025
where snapshot_year = 2026
and not regexp_matches(code, '^[0-9]{6}\.SH$');
结果为空,说明本次成分都是上交所代码,格式符合 QMT 代码口径。
完整导入脚本位置
本机脚本:
C:\workspace\qmt-484base\experment\import_eastmoney_sse50_2026.py
本机保存的来源文件:
C:\workspace\qmt-484base\experment\eastmoney_sse50_2026\eastmoney_sse50_2026.csv
C:\workspace\qmt-484base\experment\eastmoney_sse50_2026\eastmoney_sse50_2026_raw.json
本地 DuckDB:
C:\data-tick\duckdb\qmt_mock.duckdb
这套处理的重点不是多建数据表,而是把不同来源统一收敛到同一张策略使用的历史成分表里。来源可以换,但策略查询入口不要变。