fix(data): akshare top_holders Length mismatch bug 根治

bug 根因 (实证 akshare 1.18.x stock_gdfx_em.py:418):
  报告期未披露 (如 sz002673 的 20260630 在 2026-07-26 还未发) 时,
  东财 PageSDLTGD 接口返 sdltgd=[], akshare pd.DataFrame([])+reset_index()
  得 1 列 df, 然后 columns=[12 列] 抛
  ValueError('Length mismatch: Expected axis has 1 elements, new values have 12')

  这是确定性无数据 (非瞬时故障), 但 call_ak_with_retry 当网络错重试 3 次
  (14s 退避) + 噪声 ERROR 日志, 浪费时间且消耗断路器配额。

修复:
  新增 _safe_top_10_em 包装 ak.stock_gdfx_free_top_10_em: 子串匹配
  'Length mismatch' (pandas 错误信息稳定) → 返空 df 带 8 列 schema
  (TOP_HOLDERS_COLUMNS), 不抛; 其他 ValueError/ConnectionError 透传重试。
  fetch_top_holders_one_period 改调此包装。

验证:
  - Mac: sz002673/20260630 从 ~14s (3 重试) 降到 0.04s, 返 0×8 空 df
  - 有效期 sz002673/20251231, sh600519/20250930 正常返 10×8 数据
  - 9/9 新单测通过 (tests/data_platform/test_top_holders_parse.py)
This commit is contained in:
2026-07-26 11:54:19 +08:00
parent b06af336c3
commit 11f383a8e2
2 changed files with 240 additions and 2 deletions
@@ -463,13 +463,53 @@ def fetch_financial_abstract(symbol: str) -> pd.DataFrame:
# ======================== top_holders (per-stock × per-period) ========================
# akshare stock_gdfx_free_top_10_em 成功时返回的 8 列 schema
# (源码 stock_gdfx_em.py 实测). 报告期未披露 (sdltgd=[]) 时 akshare 抛
# ValueError("Length mismatch"), 这里捕获后用此 schema 返空 df,
# 保证 parquet 列与有效期一致 (downstream reader 不会列数漂移).
TOP_HOLDERS_COLUMNS = [
"名次", "股东名称", "股东性质", "股份类型",
"持股数", "占总流通股本持股比例", "增减", "变动比率",
]
def _safe_top_10_em(symbol: str, date: str) -> pd.DataFrame:
"""akshare stock_gdfx_free_top_10_em 包装: 容忍空 sdltgd 响应。
bug 根因 (实证 akshare 1.18.x stock_gdfx_em.py):
报告期未披露 (如当年 Q2 季报未发) 时东财 PageSDLTGD 接口返 sdltgd=[],
akshare pd.DataFrame([]).reset_index() 得 1 列 df, 然后 columns=[12 列]
抛 ValueError("Length mismatch: Expected axis has 1 elements, new values have 12").
这是确定性无数据 (非瞬时故障), 但 call_ak_with_retry 会当网络错重试 3 次
(14s 退避) + 噪声 ERROR 日志。
本包装预判该特定 ValueError (子串匹配 pandas 错误信息, 稳定):
- Length mismatch → 返空 df (带 TOP_HOLDERS_COLUMNS schema), 不抛
- 其他 ValueError / ConnectionError → 透传给 call_ak_with_retry 走重试
"""
try:
return ak.stock_gdfx_free_top_10_em(symbol=symbol, date=date)
except ValueError as e:
if "Length mismatch" in str(e):
logger.debug(
"stock_gdfx_free_top_10_em(%s, %s) Length mismatch → sdltgd 空 (报告期未披露), 返空 df",
symbol, date,
)
return pd.DataFrame(columns=TOP_HOLDERS_COLUMNS)
raise
def fetch_top_holders_one_period(
symbol: str, period: str,
) -> pd.DataFrame:
"""stock_gdfx_free_top_10_em(symbol='sh600519', date='20250930') 单期。
symbol 小写前缀, period YYYYMMDD."""
symbol 小写前缀, period YYYYMMDD.
通过 _safe_top_10_em 包装: 报告期未披露 (sdltgd=[]) 时 akshare 抛
Length mismatch ValueError, 这里捕获返空 df (避免 3 次无意义重试, 确定性
无数据不应消耗断路器配额)。"""
return _df_or_empty(call_ak_with_retry(
ak.stock_gdfx_free_top_10_em,
_safe_top_10_em,
f"top_holders/{symbol}/{period}",
symbol=symbol, date=period,
))