ZhuLinsen/daily_stock_analysis · error · RateLimitError

Akshare 可能被限流: {e}

Error message

Akshare 可能被限流: {e}

What it means

RateLimitError raised by the ETF fetch path (ak.fund_etf_hist_em) when the caught exception's lowercased message contains anti-scraping keywords ('banned', 'blocked', '频率', 'rate', '限制') — same heuristic as the stock channel. Any other failure mode on this path is re-raised as DataFetchError ('Akshare 获取 ETF 数据失败').

Source

Thrown at data_provider/akshare_fetcher.py:724

            
            # 记录返回数据摘要
            if df is not None and not df.empty:
                logger.info(f"[API返回] ak.fund_etf_hist_em 成功: 返回 {len(df)} 行数据, 耗时 {api_elapsed:.2f}s")
                logger.info(f"[API返回] 列名: {list(df.columns)}")
                logger.info(f"[API返回] 日期范围: {df['日期'].iloc[0]} ~ {df['日期'].iloc[-1]}")
                logger.debug(f"[API返回] 最新3条数据:\n{df.tail(3).to_string()}")
            else:
                logger.warning(f"[API返回] ak.fund_etf_hist_em 返回空数据, 耗时 {api_elapsed:.2f}s")
            
            return df
            
        except Exception as e:
            error_msg = str(e).lower()
            
            # 检测反爬封禁
            if any(keyword in error_msg for keyword in ['banned', 'blocked', '频率', 'rate', '限制']):
                logger.warning(f"检测到可能被封禁: {e}")
                raise RateLimitError(f"Akshare 可能被限流: {e}") from e
            
            raise DataFetchError(f"Akshare 获取 ETF 数据失败: {e}") from e
    
    def _fetch_us_data(self, stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
        """
        获取美股历史数据
        
        数据来源:ak.stock_us_daily()(新浪财经接口)
        
        Args:
            stock_code: 美股代码,如 'AMD', 'AAPL', 'TSLA'
            start_date: 开始日期,格式 'YYYY-MM-DD'
            end_date: 结束日期,格式 'YYYY-MM-DD'
            
        Returns:
            美股历史数据 DataFrame
        """
        import akshare as ak

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Wait for the throttle window to pass (minutes) before retrying ETF fetches.
  2. Add spacing/jitter between ETF requests and cache results to avoid refetching.
  3. Catch RateLimitError at the caller and fall back to another data source for ETF quotes if configured.
  4. Reduce the symbol count per run or stagger schedules across instances.
  5. If the error persists across long waits, verify the message isn't a false positive (a different error containing the word '限制').

Example fix

# before
for etf in ['510300', '510500', '512100']:
    df = fetcher.fetch_stock_data(etf, start, end)

# after: pace + cache
import time
cache = {}
for etf in ['510300', '510500', '512100']:
    if etf not in cache:
        cache[etf] = fetcher.fetch_stock_data(etf, start, end)
        time.sleep(3)
Defensive patterns

Strategy: retry

Type guard

from data_provider.exceptions import RateLimitError, DataFetchError

def is_etf_rate_limited(exc: Exception) -> bool:
    return isinstance(exc, RateLimitError)

def is_etf_fetch_failed(exc: Exception) -> bool:
    return isinstance(exc, DataFetchError) and '获取 ETF 数据失败' in str(exc)

Try / catch

try:
    df = fetcher.fetch_stock_data(etf_code, start, end)
except RateLimitError:
    time.sleep(300)
    df = fetcher.fetch_stock_data(etf_code, start, end)  # single retry after backoff
except DataFetchError as e:
    if '获取 ETF 数据失败' in str(e):
        df = alternate_fetcher.fetch_stock_data(etf_code, start, end)
    raise

Prevention

When it happens

Trigger: Calling AkshareFetcher with an ETF code (detected by _is_etf_code) while Eastmoney throttles the fund_etf_hist_em endpoint: repeated ETF fetches in a short window produce an error containing a rate/ban keyword.

Common situations: Batch fetching a large ETF watchlist; CI or network smoke tests that hit the endpoint repeatedly; running multiple instances of the analyzer from the same IP concurrently.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/a0eb6e27fdb6d10d. Report an issue: GitHub.