"""东方财富 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)