⑫ 两层选择:先「能不能干」,再「谁更合适」
目标:学一个通用决策框架——当你有多个候选可选时,别写成一堆 if/else,而是分两道关卡挑:第一关卡「能力门控」先筛掉干不了的,第二关卡「偏好打分」再在能干的里面排序选最优。yt-dlp 的网络层用这套从一个请求里挑出最合适的 HTTP 后端。
配套文档 ⑪(路由器 + 可插拔后端)。⑪讲"有哪些插头",这篇讲"总机怎么给请求挑插头"。
一、问题:选型最怕「能不能干的」和「谁更优的」混一起
假设要发一个走 socks 代理 的请求,你有 3 个后端可选:
- urllib、requests、curl_cffi
你会怎么选?最容易写崩的就是把所有条件搓成一个 if/else:
if sock代理 且 urllib 不支持: ...跳过 urllib
elif 请求要伪装 且 curl_cffi 支持: 选 curl_cffi
elif ...
条件一多,这个判断就写成一团谁都看不懂的乱麻,加一个新后端、加一个新条件,就崩一次。
为什么乱?因为它把两类完全不同的问题混在一起了:
- 能力问题:这后端能不能干(socks 代理 urllib 根本不支持——干不了就是干不了,不用比)
- 优劣问题:能干的里面,哪个更合适/更快/更匹配(伪装请求当然该优先 curl_cffi)
这两件事本该分开判断,却被你挤进同一个 if/else。
二、解法:分「两层」挑
yt-dlp 把它拆成两道独立的关卡:
第一层:能力门控(先筛掉不能干的)
每个后端声明自己支持啥(_SUPPORTED_*),不符合就明确拒绝:
# 后端声明自己的能力
class UrllibHandler:
_SUPPORTED_PROXY_SCHEMES = ('http', 'https') # 不支持 socks
# ...别的后端支持 socks...
# 门控:validate() 不满足 → 抛 UnsupportedRequest 跳过
for candidate in candidates:
if candidate.validate(request): # 能不能干?
usable.append(candidate)
走 socks 代理的请求一到,urllib 的
validate()发现socks不在_SUPPORTED_PROXY_SCHEMES→ 直接出局,根本进不了下一轮。
第二层:偏好打分(再在能干的里面选最优)
能干的候选,给它们各自打分,分高的优先:
# 每个后端可注册一个"偏好函数"给自己加分
preferences = {rh: sum(pref(rh, request) for pref in preferences_funcs)
for rh in usable}
best = max(usable, key=lambda rh: preferences[rh]) # 取最高分
伪装请求:curl_cffi 命中"伪装偏好" +1000 分,碾压性胜出。
三、两张表格看清「两层」各管什么
| 关卡 | 问的问题 | 输出 | 例子 |
|---|---|---|---|
| ① 能力门控 | 你能不能干? | 通过 / 出局 | urllib 不支持 socks,出局 |
| ② 偏好打分 | 能干的里面谁最合适? | 排序 | curl_cffi 支持伪装,+1000 分胜出 |
[!important] 记住分界:「干不了」用能力拒绝,「干得好」用打分排序。两个机制别混。
四、为什么分开好处大(3 个)
| # | 好处 | 具体 |
|---|---|---|
| 1 | 判断不缠在一起 | 能力是硬性二值,打分是软性排序,各管各的,不写成乱 if |
| 2 | 好扩展 | 加一种能力→ 声明 _SUPPORTED_*;加重某个偏好 → 加个打分函数。互不干扰 |
| 3 | 出错知道找谁 | 请求没人接(NoSupportingHandlers),一眼看出是"能力层全被筛掉"还是"没人打分" |
五、在哪用得上(这个思维极通用)
| 你的场景 | 第一层(能不能) | 第二层(谁更优) |
|---|---|---|
| HTTP 后端 | 该后端是否支持此协议/代理 | 伪装/速度/匹配度打分 |
| 租户/房产推荐 | 预算内有没有房 | 距工作地、租金性价比排序 |
| 支付渠道选路 | 该渠道支持此币种/地区吗 | 手续费/成功率打分 |
| 招聘筛选 | 资格是否符合硬条件 | 经验/技能匹配度排序 |
| 路由 / 负载均衡 | 节点健康吗 | 当前负载/延迟打分 |
通用规律:只要在「一堆候选中挑一个」,都先用硬性条件筛掉不能干的,再用软性偏好在里面排序——这两步分开,逻辑就清爽。
六、核心金句
选型别写成一坨 if/else。 都分两步:先做"能力门控"(干不了的明确拒绝),再做"偏好打分"(能干的里面按分数取最优)。把"能不能"和"好不好"拆开,系统才清晰、好扩展、好排查。
七、在 yt-dlp 里对应哪里(供回溯)
- 能力门控:
RequestHandler.validate()+_SUPPORTED_URL_SCHEMES/_SUPPORTED_PROXY_SCHEMES/_SUPPORTED_FEATURES - 偏好打分:
@register_preference+RequestDirector._get_handlers()(yt_dlp/networking/common.py) - 伪装偏好 +1000:
yt_dlp/networking/impersonate.py - 学习库 → [[Networking-Layer]]
八、一句话带走
当你要在多个候选中挑一个时,别把条件全塞进一个 if/else。学会两层选择:第一层"能力门控"硬性筛掉干不了的,第二层"偏好打分"软性在剩下的里面挑最优。以后做路由、选型、推荐、调度,都用这两道关卡——"能不能"和"好不好"永远分开算。