tags: 量化 python # 东方财富上证50成分股接口记录 这次为了给本地 DuckDB 行情库补 2026 年上证50成分股,抓了一下东方财富的数据页面: ```text https://data.eastmoney.com/other/index/ ``` 页面本身不是直接把表格写在 HTML 里,而是前端 JS 调用东方财富 datacenter 接口。对上证50来说,实际用到的接口是: ```text https://datacenter-web.eastmoney.com/api/data/v1/get ``` ## 接口参数 核心参数如下: ```python 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", } ``` 几点要注意: 1. `reportName=RPT_INDEX_TS_COMPONENT` 是指数成分股报表。 2. 上证50对应 `TYPE="2"`。这个是在页面 JS 里看到的分支逻辑:`indexcode == '000016' ? '2'`。 3. `sortTypes` 不能传 `"asc"` 或 `"desc"`,要传数字字符串,比如 `"1"`。否则接口会返回“排序顺序字段不能为非数字类型”。 4. 返回结果里 `SECUCODE` 已经带交易所后缀,例如 `600519.SH`。 ## 最小抓取代码 ```python 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 的原则 我的本地库里已经有一个历史成分表: ```sql choice_sse50_constituents_2022_2025 ``` 虽然表名里带 `2022_2025`,但它实际承担的是上证50年度快照表的角色。策略只应该读这一张 canonical 表,不应该因为换了来源就多读一张临时表。 所以这次导入原则是: 1. 东方财富接口返回保存成 CSV/JSON 文件,作为来源凭证。 2. DuckDB 里只把标准化后的 `snapshot_year=2026` 成分写入历史成分表。 3. 不在 DuckDB 里保留 `eastmoney_*` 这类临时输入表,避免以后策略层误用。 本次标准化后的字段: ```text 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 ``` 其中: ```text code = 600519.SH local_code = sh600519 ``` ## DuckDB 写入 SQL 导入时先删除同一年旧快照,再写入新快照: ```sql delete from choice_sse50_constituents_2022_2025 where index_key = 'sse50' and snapshot_year = 2026; ``` 然后把标准化后的 DataFrame 注册成 DuckDB 临时视图,插入 canonical 表: ```python 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 检查每年快照数量: ```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 这次导入结果: ```text snapshot_year rows_n codes min_snapshot max_snapshot 2026 50 50 2026-07-07 2026-07-07 ``` 再检查代码格式: ```sql select code, stock_name from choice_sse50_constituents_2022_2025 where snapshot_year = 2026 and not regexp_matches(code, '^[0-9]{6}\.SH$'); ``` 结果为空,说明本次成分都是上交所代码,格式符合 QMT 代码口径。 ## 完整导入脚本位置 本机脚本: ```text C:\workspace\qmt-484base\experment\import_eastmoney_sse50_2026.py ``` 本机保存的来源文件: ```text 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: ```text C:\data-tick\duckdb\qmt_mock.duckdb ``` 这套处理的重点不是多建数据表,而是把不同来源统一收敛到同一张策略使用的历史成分表里。来源可以换,但策略查询入口不要变。