{"record":{"id":"48d29e891150dd36","repo":"ZhuLinsen/daily_stock_analysis","slug":"akshare-etf-e","errorCode":null,"errorMessage":"Akshare 获取 ETF 数据失败: {e}","messagePattern":"Akshare 获取 ETF 数据失败: (.+?)","errorType":"exception","errorClass":"DataFetchError","httpStatus":null,"severity":"error","filePath":"data_provider/akshare_fetcher.py","lineNumber":726,"sourceCode":"            if df is not None and not df.empty:\n                logger.info(f\"[API返回] ak.fund_etf_hist_em 成功: 返回 {len(df)} 行数据, 耗时 {api_elapsed:.2f}s\")\n                logger.info(f\"[API返回] 列名: {list(df.columns)}\")\n                logger.info(f\"[API返回] 日期范围: {df['日期'].iloc[0]} ~ {df['日期'].iloc[-1]}\")\n                logger.debug(f\"[API返回] 最新3条数据:\\n{df.tail(3).to_string()}\")\n            else:\n                logger.warning(f\"[API返回] ak.fund_etf_hist_em 返回空数据, 耗时 {api_elapsed:.2f}s\")\n            \n            return df\n            \n        except Exception as e:\n            error_msg = str(e).lower()\n            \n            # 检测反爬封禁\n            if any(keyword in error_msg for keyword in ['banned', 'blocked', '频率', 'rate', '限制']):\n                logger.warning(f\"检测到可能被封禁: {e}\")\n                raise RateLimitError(f\"Akshare 可能被限流: {e}\") from e\n            \n            raise DataFetchError(f\"Akshare 获取 ETF 数据失败: {e}\") from e\n    \n    def _fetch_us_data(self, stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:\n        \"\"\"\n        获取美股历史数据\n        \n        数据来源：ak.stock_us_daily()（新浪财经接口）\n        \n        Args:\n            stock_code: 美股代码，如 'AMD', 'AAPL', 'TSLA'\n            start_date: 开始日期，格式 'YYYY-MM-DD'\n            end_date: 结束日期，格式 'YYYY-MM-DD'\n            \n        Returns:\n            美股历史数据 DataFrame\n        \"\"\"\n        import akshare as ak\n        \n        # 防封禁策略 1: 随机 User-Agent","sourceCodeStart":708,"sourceCodeEnd":744,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/data_provider/akshare_fetcher.py#L708-L744","documentation":"Raised by AkshareFetcher._fetch_etf_data when ak.fund_etf_hist_em throws an exception that does not match the rate-limit keyword list ('banned', 'blocked', '频率', 'rate', '限制'). It is a DataFetchError wrapping the original akshare exception, so the DataFetcherManager fallback chain can catch it and move to the next data source. The keyword check runs first, so genuine anti-crawler bans surface as RateLimitError instead of this error.","triggerScenarios":"Calling fetch on an ETF code (e.g. '510300') where ak.fund_etf_hist_em raises a non-rate-limit exception: network timeout, missing akshare optional dependency, akshare interface signature change after upgrade, or an East Money endpoint returning malformed JSON. Any exception whose lowercased message lacks the ban keywords lands here.","commonSituations":"akshare piped to a version where fund_etf_hist_em renamed arguments; proxy/DNS failures in CI; East Money occasionally returning HTML error pages that break JSON parsing; running on a network where eastmoney.com is unreachable.","solutions":["Inspect the wrapped cause (e.__cause__) in logs — the akshare exception text identifies the real failure (timeout vs TypeError vs JSONDecodeError).","If it is a TypeError/AttributeError, pin or upgrade akshare to the version the fetcher was written against and retest with `ak.fund_etf_hist_em` in a REPL.","If it is network-related, verify connectivity to eastmoney.com and increase the request timeout / retry via the fetcher's base options.","Let DataFetcherManager fall through to the next configured source for ETF data rather than retrying akshare immediately."],"exampleFix":"// before\ntry:\n    df = fetcher.fetch('510300', '2025-01-01', '2025-06-30')\nexcept DataFetchError as e:\n    print(e)  # cause lost in print\n\n// after\nimport traceback\ntry:\n    df = fetcher.fetch('510300', '2025-01-01', '2025-06-30')\nexcept DataFetchError as e:\n    logging.error('ETF fetch failed: %s | cause: %s', e, e.__cause__)\n    df = manager.fetch('510300', ...)  # fallback chain","handlingStrategy":"fallback","validationCode":"import akshare as ak\n# smoke-test the ETF interface before a batch run\ndf = ak.fund_etf_hist_em(symbol='510300', period='daily', start_date='20250101', end_date='20250630')\nassert not df.empty","typeGuard":"def is_akshare_etf_available() -> bool:\n    try:\n        import akshare as ak\n        return callable(getattr(ak, 'fund_etf_hist_em', None))\n    except ImportError:\n        return False","tryCatchPattern":"try:\n    df = akshare_fetcher.fetch(etf_code, start, end)\nexcept RateLimitError:\n    raise  # let rate-limit policy handle it\nexcept DataFetchError as e:\n    logger.warning('akshare ETF failed (%s); falling back', e)\n    df = manager.fetch(etf_code, start, end)","preventionTips":["Smoke-test akshare interfaces after every akshare upgrade — they change frequently.","Keep DataFetcherManager fallback chains configured for ETF codes so one source failing never kills the run.","Log e.__cause__ alongside the wrapper message for diagnosable failures."],"tags":["akshare","etf","data-provider","network","fallback"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}