记一次 yfinance 源码调试:SOCKS5 代理下 Chart API 正常,历史数据却一直超时

记一次 yfinance 源码调试:SOCKS5 代理下 Chart API 正常,历史数据却一直超时

环境:Linux / Python 3.12 / yfinance 1.6.0(源码仓库)/ curl_cffi 最终成果:定位并修复 yfinance 的 4 处问题,补丁已提交官方 PR:github.com/ranaroussi/...

一、需求背景

因为网络环境限制,需要通过本地 SOCKS5 代理访问 Yahoo Finance,并且希望用 curl_cffi 模拟 Firefox 指纹、走 HTTP/2。代码大概是这样:

python 复制代码
from curl_cffi import CurlHttpVersion, requests
import yfinance as yf

proxy = "socks5h://127.0.0.1:1080"
session = requests.Session(
    impersonate="firefox",
    http_version=CurlHttpVersion.V2TLS,
    proxies={"http": proxy, "https": proxy},
)

tk = yf.Ticker("005930.KS", session=session)
df = tk.history(start="2026-08-01", end="2026-08-23", interval="1d", auto_adjust=True)

诡异的地方来了------用同一个 session 直接请求 Chart API 一切正常

python 复制代码
resp = session.get(
    "https://query1.finance.yahoo.com/v8/finance/chart/005930.KS?range=5d&interval=1d",
    timeout=30,
)
print(resp.status_code, resp.http_version)
# 输出: 200 HTTP/3   (代理、指纹、HTTP/2 全部生效)

但一调 tk.history() 就挂:

text 复制代码
Cookie/crumb fetch failed (Timeout), continuing without crumb
Failed to get ticker '005930.KS' reason: Failed to perform,
curl: (28) Connection timed out after 10002 milliseconds.

Chart API 明明是通的,为什么 yfinance 内部就超时?

二、排查过程

2.1 先看失败调用栈

开启 yf.enable_debug_mode() 后观察日志,失败发生在 yfinance 内部初始化阶段:

text 复制代码
yfinance
  -> _get_ticker_tz()
  -> _get_cookie_and_crumb()
  -> _get_cookie_basic()      # 访问 https://fc.yahoo.com
  -> _get_crumb_basic()       # 访问 https://query1.../v1/test/getcrumb

也就是说 yfinance 在发真正的数据请求前,会先请求两个"辅助接口":

  • https://fc.yahoo.com ------ 获取 Cookie
  • https://query1.finance.yahoo.com/v1/test/getcrumb ------ 获取 crumb

在我的代理链路下,fc.yahoo.com 经常 curl: (28) 超时,getcrumb 有时返回 429。

2.2 疑点:为什么直接请求没事?

一开始以为是代理问题、指纹问题、HTTP 版本问题。但直接用 session 请求 getcrumb 也是成功的:

python 复制代码
resp = session.get("https://query1.finance.yahoo.com/v1/test/getcrumb", timeout=30)
print(resp.status_code, resp.http_version)   # 200 HTTP/3

同样的 session、同样的代理,yfinance 里就不行------说明问题在 yfinance 对 session 做了什么手脚

2.3 读源码找到真凶

yfinance/data.py,在 YfData._make_request() 开头发现了这一行:

python 复制代码
# sync with config
self._session.proxies = _normalize_proxy(YfConfig.network.proxy)

_normalize_proxy 是这样:

python 复制代码
def _normalize_proxy(proxy):
    if isinstance(proxy, str):
        return {"http": proxy, "https": proxy}
    return proxy      # None 进来还是 None!

破案了:

  • 我把代理配置在 session 上 ,但没有调 yf.set_config(proxy=...)
  • 于是全局配置 YfConfig.network.proxy 是默认的 None
  • _make_request() 每次请求前都执行"同步配置",把 None 赋给了 session.proxies
  • 我设置的 SOCKS5 代理被静默清空了
  • 之后所有请求裸连 Yahoo → curl: (28) timeout after 10002ms(history 默认 timeout=10 秒)

这也完美解释了为什么 fc.yahoo.com 那一步反而能降级------它走的是裸 session.get(),不经过 _make_request(),代理还在(虽然 fc.yahoo.com 本身经常慢)。

三、顺藤摸瓜:又发现 3 处健壮性问题

修复代理问题的过程中,发现 Cookie/crumb 流程还有几处设计缺陷:

问题 1:fc.yahoo.com 超时是致命的

python 复制代码
def _get_cookie_basic(self, timeout=30):
    ...
    try:
        self._session.get(url='https://fc.yahoo.com', ...)
    except requests.exceptions.DNSError as e:
        return False          # 只捕获 DNS 错误!
    ...

只捕获了 DNSError。而代理环境下更常见的是超时(curl error 28),直接往上抛,整个请求报废。可问题是------Chart API 根本不需要这个 Cookie

问题 2:getcrumb 返回 429 也致命

python 复制代码
if crumb_response.status_code == 429 or "Too Many Requests" in self._crumb:
    raise YFRateLimitError()

getcrumb 被 429 了就直接中止整个请求。但很多接口(比如 Chart API)没有 crumb 也能正常工作。

问题 3:重试分支会把 'crumb': None 写进参数

400+ 重试分支里二次取 crumb 失败时,request_args['params']['crumb'] = crumb 会写入 None,产生一个坏参数。

四、修复方案

全部改动集中在 yfinance/data.py,共 4 处,不改任何公开 API:

4.1 不再清空用户 session 上的代理(核心 bug)

python 复制代码
# sync with config, but never wipe proxies set directly on a
# user-supplied session (e.g. SOCKS5 via curl_cffi)
if YfConfig.network.proxy is not None:
    self._session.proxies = _normalize_proxy(YfConfig.network.proxy)

只有显式配置了全局代理才覆盖,用户的 session 配置优先。

4.2 fc.yahoo.com 超时降级

python 复制代码
except requests.exceptions.DNSError as e:
    ...
    return False
except Exception as e:
    if _is_transient_error(e):
        utils.get_yf_logger().warning(
            f"Cookie fetch from fc.yahoo.com failed ({type(e).__name__}), continuing without it")
        return False
    raise

超时/连接错误与 DNS 错误同等对待:记 warning,继续。

4.3 crumb 获取失败降级为无 crumb 请求

python 复制代码
try:
    crumb, strategy = self._get_cookie_and_crumb()
except YFRateLimitError:
    utils.get_yf_logger().warning(
        "Crumb fetch rate-limited (HTTP 429), continuing without crumb")
    crumb, strategy = None, self._cookie_strategy
except Exception as e:
    if _is_transient_error(e):
        crumb, strategy = None, self._cookie_strategy
    else:
        raise

需要 crumb 的接口(如 quoteSummary)拿不到 crumb 自然会失败,走已有的策略切换重试;目标接口真正的 429 依然抛 YFRateLimitError,不会掩盖限流。

4.4 重试分支不再写 'crumb': None

二次取 crumb 失败时直接省略该参数。

五、测试验证

新增 tests/test_cookie_crumb_degradation.py,6 个用例:

测试 验证内容
test_fc_yahoo_timeout_is_not_fatal fc.yahoo.com 超时 → 降级不抛异常
test_getcrumb_429_degrades_to_no_crumb_request getcrumb 429 → 无 crumb 继续,Chart 仍 200
test_transient_crumb_failure_degrades getcrumb 超时 → Chart 仍 200
test_target_429_still_raises_rate_limit 目标接口真 429 → 仍正确抛 YFRateLimitError
test_custom_proxies_not_overwritten 用户代理映射不被覆盖
test_make_request_does_not_wipe_session_proxies 回归:_make_request 不清空 session 代理
bash 复制代码
pytest tests/test_cookie_crumb_degradation.py tests/test_data.py -q
# 21 passed
ruff check yfinance/data.py tests/test_cookie_crumb_degradation.py
# All checks passed!

最终用真实代理实测原脚本:

text 复制代码
✅ 代理连通测试成功, HTTP 200, HTTP/3
✅ 获取 Crumb 成功, HTTP 200, HTTP/3
正在获取 005930.KS 历史数据...
✅ 成功获取 14 条历史数据

六、提交给官方

改动小、向后兼容、带测试,于是整理成英文 Issue + PR 提交上游:

  1. Fork 官方仓库 → 建分支 fix/cookie-crumb-proxy-resilience → 只提交 data.py 和测试文件
  2. gh pr create --repo ranaroussi/yfinance 创建 PR: github.com/ranaroussi/...

中间还遇到一个小坑:SSH 22 端口被网络拦截(连到了一个假 IP),git push 一直报权限错误。解决方式是把 fork 远程改成 HTTPS,用 gh auth git-credential 做凭证推送。

七、下载修复版源码 / 使用临时本地版本

在官方合并 PR 之前,如果你的项目也被这个问题困扰,可以直接使用我 fork 里的修复分支:

修复版源码仓库: github.com/amosli/yfin... 分支: fix/cookie-crumb-proxy-resilience(基于官方 main + 本文全部修复)

方式一:直接从 GitHub 安装(最简单,推荐)

bash 复制代码
# 激活你的项目虚拟环境后
source .venv/bin/activate

pip install --force-reinstall --no-deps \
    "yfinance @ git+https://github.com/amosli/yfinance@fix/cookie-crumb-proxy-resilience"
  • --no-deps:只替换 yfinance 本身,不动项目其他依赖
  • 验证安装来源:
bash 复制代码
python -c "import yfinance; print(yfinance.__file__)"
# 路径应位于 site-packages 中,版本号与提交一致

也可以写进项目的 pyproject.toml 固定依赖:

toml 复制代码
[project]
dependencies = [
    "yfinance @ git+https://github.com/amosli/yfinance@fix/cookie-crumb-proxy-resilience",
]

方式二:克隆源码 + 可编辑安装(方便自己再改)

bash 复制代码
# 1. 克隆修复分支
git clone -b fix/cookie-crumb-proxy-resilience \
    https://github.com/amosli/yfinance.git ~/yfinance-fix
cd ~/yfinance-fix
pip install -e .        # 建议在项目虚拟环境中执行

# 2. 验证指向本地目录
python -c "import yfinance; print(yfinance.__file__)"
# 应输出 .../yfinance-fix/yfinance/__init__.py

可编辑安装的好处:以后 cd ~/yfinance-fix && git pull 即可同步我的更新,所有使用该虚拟环境的项目即时生效,无需重装。

方式三:只打补丁,不换包(适合已深度定制环境的场景)

bash 复制代码
# 从 fork 下载补丁文件应用到现有 yfinance 源码
git clone -b fix/cookie-crumb-proxy-resilience \
    https://github.com/amosli/yfinance.git /tmp/yfinance-fix
cd <你项目虚拟环境的 site-packages>/yfinance
patch -p1 < /tmp/yfinance-fix/yfinance/data.py.diff   # 或直接覆盖 data.py

不推荐手改 .venv/lib/python3.12/site-packages/yfinance/------升级即丢失,且容易改出不一致状态。方式一/二更可控。

恢复官方版本

官方合并 PR 后(或想切回时):

bash 复制代码
pip uninstall yfinance
pip install yfinance          # 或指定版本: pip install yfinance==<原版本号>

八、经验总结

  1. "直接请求没问题,进库就报错",八成是库改了你的配置。 这次就是 yfinance 在每次请求前无条件覆盖 session.proxies。遇到这类问题,尽早读库源码,别只在网络层打转。
  2. 给库传自定义 session 时,要弄清哪些属性会被库覆盖/修改。 代理、headers、cookies 都是高危区。
  3. 非关键依赖应该优雅降级。 yfinance 把 Cookie/crumb 这种"部分接口才需要的东西"做成了全量强依赖,代理环境下一颗老鼠屎坏一锅粥。
  4. 错误要区分对待。 代理不通、Yahoo 限流、辅助接口超时、目标接口失败,表现和处理方式完全不同,混在一起只会增加排障成本。
  5. 给开源项目报问题时带上根因分析和回归测试,合并的概率会大很多。

官方合并前,可从 github.com/amosli/yfin... 分支 fix/cookie-crumb-proxy-resilience 获取修复版;合并后建议切回 PyPI 官方版本。

相关推荐
JimmtButler1 小时前
功能明明正常,架构为什么还是会腐化?<第一章>
后端·架构
步行cgn1 小时前
PageHelper 的用法
java·后端
foggyprojects1 小时前
业务系统有表格,BI 有看板,AI 问数还需要做什么?
后端
生锈的键盘1 小时前
深入理解 Socket 与 epoll:从内核数据结构到用户态回调的全链路解析
后端
Sam_Deep_Thinking1 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
程序员爱钓鱼2 小时前
Rust const泛型详解:让常量也成为泛型参数
后端·面试·rust
evans在进步2 小时前
Spring Boot 工程化核心详解:Parent、Starter、热部署、事务与多数据源
spring boot·后端·python
宫水三叶的刷题日记3 小时前
馋猫外卖:AI 让字节式试错没有天花板
后端
SomeB1oody3 小时前
【RustyML入门】7.4. 按需裁剪与模块化集成
开发语言·后端·机器学习·rust·教程