记一次 yfinance 源码调试:SOCKS5 代理下 Chart API 正常,历史数据却一直超时
环境:Linux / Python 3.12 / yfinance 1.6.0(源码仓库)/ curl_cffi
最终成果:定位并修复 yfinance 的 4 处问题,补丁已提交官方 PR:https://github.com/ranaroussi/yfinance/pull/2953
一、需求背景
因为网络环境限制,需要通过本地 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------ 获取 Cookiehttps://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 提交上游:
- Fork 官方仓库 → 建分支
fix/cookie-crumb-proxy-resilience→ 只提交data.py和测试文件 - 用
gh pr create --repo ranaroussi/yfinance创建 PR:
https://github.com/ranaroussi/yfinance/pull/2953
中间还遇到一个小坑:SSH 22 端口被网络拦截(连到了一个假 IP),git push 一直报权限错误。解决方式是把 fork 远程改成 HTTPS,用 gh auth git-credential 做凭证推送。
七、下载修复版源码 / 使用临时本地版本
在官方合并 PR 之前,如果你的项目也被这个问题困扰,可以直接使用我 fork 里的修复分支:
修复版源码仓库: https://github.com/amosli/yfinance
分支:
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==<原版本号>
八、经验总结
- "直接请求没问题,进库就报错",八成是库改了你的配置。 这次就是 yfinance 在每次请求前无条件覆盖
session.proxies。遇到这类问题,尽早读库源码,别只在网络层打转。 - 给库传自定义 session 时,要弄清哪些属性会被库覆盖/修改。 代理、headers、cookies 都是高危区。
- 非关键依赖应该优雅降级。 yfinance 把 Cookie/crumb 这种"部分接口才需要的东西"做成了全量强依赖,代理环境下一颗老鼠屎坏一锅粥。
- 错误要区分对待。 代理不通、Yahoo 限流、辅助接口超时、目标接口失败,表现和处理方式完全不同,混在一起只会增加排障成本。
- 给开源项目报问题时带上根因分析和回归测试,合并的概率会大很多。
官方合并前,可从 https://github.com/amosli/yfinance 分支 fix/cookie-crumb-proxy-resilience 获取修复版;合并后建议切回 PyPI 官方版本。