You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
110 lines
3.4 KiB
110 lines
3.4 KiB
"""东方财富 CHOICE API 主营构成(按产品/按地区) 抓取工具。
|
|
- 输入: 股票代码列表 (如 ['603233.SH'])
|
|
- 输出: pandas DataFrame (按报告期/产品分类的主营构成数据)
|
|
- 不需要登录, 不需要 cookie, 无文件落盘
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pandas as pd
|
|
import requests
|
|
|
|
from utils.fin_summary import HEADERS, TIMEOUT
|
|
|
|
|
|
# ZYGC_AFLZS 返回一个带 urls 的跳转结构, 真正的数据在
|
|
# datacenter-choice 的 RPT_HSF9_FN_MAINOPBUSINESS 报表里.
|
|
_ZYGC_URL = "https://emchoicews2.eastmoney.com/stock/f9/ZYGC_AFLZS"
|
|
|
|
# 默认报告期类型: 1=一季报, 5=中报, 3=三季报, 6=年报 (注意: 与财务摘要的编码不同)
|
|
_DEFAULT_REPORT_PERIOD_TYPE = '"1","5","3","6"'
|
|
|
|
|
|
def _build_params(
|
|
secucode: str,
|
|
start_date: str,
|
|
end_date: str,
|
|
classify_type: str,
|
|
report_period_type: str,
|
|
) -> dict[str, str]:
|
|
"""构造 ZYGC_AFLZS 请求参数."""
|
|
return {
|
|
"SecurityCode": secucode,
|
|
"StartDate": start_date,
|
|
"EndDate": end_date,
|
|
"ReportPeriodType": report_period_type,
|
|
"Magnitude": "4",
|
|
"ClassifyType": classify_type,
|
|
"ReportShowForm": "0",
|
|
"ExchangeRate": "1",
|
|
"Digits": "2",
|
|
"Sort": "-1",
|
|
"IsShowEmptyRow": "true",
|
|
}
|
|
|
|
|
|
def _fetch_data_url(
|
|
secucode: str,
|
|
start_date: str,
|
|
end_date: str,
|
|
classify_type: str,
|
|
report_period_type: str,
|
|
) -> str:
|
|
"""调用 ZYGC_AFLZS, 返回真正的主营构成数据接口 URL."""
|
|
params = _build_params(
|
|
secucode, start_date, end_date, classify_type, report_period_type
|
|
)
|
|
r = requests.get(_ZYGC_URL, headers=HEADERS, params=params, timeout=TIMEOUT)
|
|
r.raise_for_status()
|
|
payload = r.json()
|
|
urls = payload.get("urls") or []
|
|
if not urls:
|
|
raise RuntimeError(f"{secucode}: ZYGC_AFLZS 未返回数据 URL: {payload}")
|
|
return urls[0]
|
|
|
|
|
|
def _fetch_one(
|
|
secucode: str,
|
|
classify_type: str,
|
|
years: int,
|
|
report_period_type: str,
|
|
) -> pd.DataFrame:
|
|
"""拉一只股票的主营构成."""
|
|
today = pd.Timestamp.today()
|
|
start = (today - pd.DateOffset(years=years)).strftime("%Y-%m-%d")
|
|
end = today.strftime("%Y-%m-%d")
|
|
|
|
data_url = _fetch_data_url(secucode, start, end, classify_type, report_period_type)
|
|
response = requests.get(data_url, headers=HEADERS, timeout=TIMEOUT)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
if not payload.get("success"):
|
|
raise RuntimeError(f"{secucode}: 接口返回失败: {payload}")
|
|
|
|
rows = payload.get("result", {}).get("data") or []
|
|
df = pd.DataFrame(rows)
|
|
if not df.empty:
|
|
df["SECUCODE"] = secucode
|
|
df["CLASSIFY_TYPE"] = classify_type # 冗余一列, 方便区分产品/地区
|
|
return df
|
|
|
|
|
|
def get_main_business(
|
|
secucodes: list[str],
|
|
classify_type: str = "产品",
|
|
years: int = 3,
|
|
report_period_type: str = _DEFAULT_REPORT_PERIOD_TYPE,
|
|
) -> pd.DataFrame:
|
|
"""抓取指定股票的主营构成(按产品或按地区)并按股票合并。"""
|
|
codes = [code.strip() for code in secucodes if code.strip()]
|
|
if not codes:
|
|
return pd.DataFrame()
|
|
|
|
frames: list[pd.DataFrame] = []
|
|
for code in codes:
|
|
df = _fetch_one(code, classify_type, years, report_period_type)
|
|
if not df.empty:
|
|
frames.append(df)
|
|
|
|
if not frames:
|
|
return pd.DataFrame()
|
|
return pd.concat(frames, ignore_index=True)
|
|
|