
1. 爬虫代理池为什么越维护越累requests 与 Scrapy 中间件各管一套的坑做爬虫的朋友大概率都经历过这个阶段一开始用 requests 写几个脚本随手从免费代理站抓一批 IP 塞进 pymysql跑得挺欢后来量上来了换成 Scrapy 做分布式又得在中间件里再写一套代理逻辑。结果就是同一个项目里requests 的proxies字典和 Scrapy 的request.meta[proxy]各维护一份 IP 池鉴权信息散落在三四个文件里今天这个 IP 挂了要删明天那个账号额度用完了要换维护成本直接翻倍。我试过最原始的做法就是 excerpt 里那种用 requests 爬免费 IPinsert project_ip写进 MySQL然后GetIP类里judge_ip一个个试get_random_ip随机取一条。这套逻辑本身没毛病问题出在三个地方。第一免费 IP 的可用率极低judge_ip里那个requests.get(http_url, proxiesproxy_dict)经常超时删库删到手软。第二ORDER BY RAND()在数据量上万之后性能断崖式下跌每次取 IP 都要全表扫描。第三也是最要命的requests 脚本和 Scrapy 中间件用的是两套完全独立的取 IP 逻辑你在 requests 里删掉的失效 IPScrapy 那边根本不知道还在继续用。更现实的问题是鉴权。当你从免费 IP 转向付费通道时每个请求都要带Authorization头或者 API Key这时候如果还是每个脚本、每个中间件各写一遍鉴权改一次密钥就要全局搜索替换漏一个地方就 401。而且 Scrapy 的中间件是异步的requests 是同步的两边的重试、超时、并发控制策略完全不一样想统一管理几乎不可能。所以真正要解决的不是怎么抓更多 IP而是怎么把出口和鉴权收敛到一个地方。把代理出口统一到一个稳定的通道上本地只保留一份配置requests 和 Scrapy 都指向它pymysql 里的代理表从存 IP改成存通道状态和统计这样维护成本才能降下来。下面我就按这个思路把改造过程拆成可复制的步骤。2. TaoToken 统一通道前置准备Base URL、API Key 与 Model ID 三件套在动手改代码之前先把通道侧的东西准备好。TaoToken 在这里扮演的角色是统一的请求出口和鉴权层你本地不再需要维护一堆 IP只需要在配置里写清楚三样东西Base URL、API Key、Model ID。这三件套是后面所有配置的基础缺一个都会在验证阶段报错。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数直接作为请求前缀。API Key 需要你去控制台生成路径是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content进去之后新建一个 Key复制出来保存好后面配置里会用到。Model ID 根据你实际要调用的模型填比如做文本处理类的爬虫后处理就填对应的模型标识。这里要强调一点不要把 API Key 硬编码在爬虫脚本里。正确的做法是放到环境变量或者独立的配置文件代码里通过os.environ读取。我见过太多人把 Key 直接写在settings.py里然后提交到 Git结果第二天就被人刷爆了。你可以建一个.env文件内容大概是这样TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的实际key TAOTOKEN_MODEL_ID你的模型ID然后在 Python 里用python-dotenv加载或者直接在启动脚本里export。Scrapy 项目的话可以在settings.py里通过os.getenv读取这样本地开发和线上部署用的是同一套代码只是环境变量不同。另外如果你之前用的是 Claude Code 或者类似的编码工具可能会遇到 OAuth 相关的配置。TaoToken 的接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content里面有各语言的示例建议先扫一遍确认 Base URL 的拼接方式。有些工具要求 Base URL 结尾不带斜杠有些要求带/v1这个细节不注意的话请求会直接 404 而不是 401排查起来很浪费时间。准备好这三件套之后先别急着改爬虫代码用最简单的 curl 或者 requests 发一个请求验证通道是否通。这一步能帮你排除掉 80% 的配置问题比如 Key 复制多了空格、Base URL 写错、Model ID 不存在等等。验证命令我会在第四节给出这里你先确保手上有这三个值。3. 可复制配置pymysql 代理表改造 requests 与 Scrapy 中间件统一指向这一节是核心我会给出三份可直接复制的配置pymysql 的表结构改造、requests 的封装、Scrapy 中间件。三份配置共用同一套环境变量改一处全局生效。先说 pymysql 的表结构。原来的project_ip表存的是ip、port、speed、proxy_type现在改成存通道状态和调用统计。建表语句如下CREATE TABLE channel_pool ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, channel_name VARCHAR(64) NOT NULL DEFAULT taotoken, base_url VARCHAR(255) NOT NULL, model_id VARCHAR(128) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可用 0禁用, success_count INT UNSIGNED NOT NULL DEFAULT 0, fail_count INT UNSIGNED NOT NULL DEFAULT 0, last_used_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_channel (channel_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里不再存 API KeyKey 只放在环境变量里数据库只记录通道的可用状态和成功率。这样即使数据库被拖也不会泄露密钥。初始化一条记录INSERT INTO channel_pool (channel_name, base_url, model_id, status) VALUES (taotoken, https://taotoken.net/api, 你的模型ID, 1);接下来是 requests 的封装。建一个channel_client.py内容如下import os import requests from dotenv import load_dotenv load_dotenv() BASE_URL os.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api) API_KEY os.getenv(TAOTOKEN_API_KEY) MODEL_ID os.getenv(TAOTOKEN_MODEL_ID) def build_headers(): return { Authorization: fBearer {API_KEY}, Content-Type: application/json, } def call_channel(payload, timeout30): url f{BASE_URL}/v1/chat/completions resp requests.post(url, headersbuild_headers(), jsonpayload, timeouttimeout) if resp.status_code 401: raise RuntimeError(鉴权失败检查 TAOTOKEN_API_KEY) if resp.status_code 429: raise RuntimeError(触发限流稍后重试) resp.raise_for_status() return resp.json()这段代码里BASE_URL和API_KEY都从环境变量读call_channel统一处理 401 和 429。你的爬虫脚本里原来写requests.get(..., proxiesproxy_dict)的地方现在改成调用call_channel代理出口就统一到 TaoToken 了。然后是 Scrapy 中间件。在middlewares.py里加一个类import os from scrapy import signals class TaoTokenChannelMiddleware: def __init__(self): self.base_url os.getenv(TAOTOKEN_BASE_URL, https://taotoken.net/api) self.api_key os.getenv(TAOTOKEN_API_KEY) self.model_id os.getenv(TAOTOKEN_MODEL_ID) classmethod def from_crawler(cls, crawler): mw cls() crawler.signals.connect(mw.spider_opened, signalsignals.spider_opened) return mw def spider_opened(self, spider): spider.logger.info(TaoToken channel ready: %s, self.base_url) def process_request(self, request, spider): request.headers[Authorization] fBearer {self.api_key} request.headers[X-Model-Id] self.model_id request.meta[channel_base] self.base_url return None在settings.py里启用DOWNLOADER_MIDDLEWARES { your_project.middlewares.TaoTokenChannelMiddleware: 543, }这样 Scrapy 的每个请求都会自动带上鉴权头不再需要request.meta[proxy]那套随机取 IP 的逻辑。requests 和 Scrapy 现在共用同一套环境变量改 Key 只需要改.env一个文件。如果你用的是 Cline MCP 或者 Codex 的auth.json配置思路一样Base URL 填https://taotoken.net/apiKey 填环境变量里的值Model ID 填对应标识。三件套对齐通道就统一了。4. 验证切换是否生效用 401 和 429 响应码做可复制测试配置改完之后必须验证通道是否真的生效。最直接的办法是用响应码来测故意传一个错误的 Key看是否返回 401正常请求看是否返回 200高频请求看是否触发 429。下面给出可复制的测试步骤。第一步测 401。临时把环境变量里的 Key 改成一个错误值然后跑curl -i -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-wrong-key \ -H Content-Type: application/json \ -d {model:你的模型ID,messages:[{role:user,content:ping}]}预期返回HTTP/1.1 401 Unauthorized。如果返回的是 404说明 Base URL 拼错了如果返回 200说明鉴权没生效检查中间件是否真的加上了Authorization头。第二步测正常请求。把 Key 换回正确的再跑一次同样的 curl预期返回 200 并且 body 里有choices字段。这一步能确认通道完全打通。第三步测 429。写一个循环脚本快速发请求import os, requests, time from dotenv import load_dotenv load_dotenv() url f{os.getenv(TAOTOKEN_BASE_URL)}/v1/chat/completions headers {Authorization: fBearer {os.getenv(TAOTOKEN_API_KEY)}} payload {model: os.getenv(TAOTOKEN_MODEL_ID), messages: [{role: user, content: hi}]} for i in range(50): r requests.post(url, headersheaders, jsonpayload) print(i, r.status_code) if r.status_code 429: print(触发限流通道生效) break time.sleep(0.05)如果看到 429说明限流策略在通道侧生效了你的本地代码不需要再自己维护重试队列。如果一直 200 没有 429可能是你的额度足够大或者限流阈值较高这属于正常情况。第四步验证 Scrapy 中间件。启动一个最小 spider在parse里打印response.status观察日志里是否出现 401 或 200。如果 Scrapy 日志里出现local proxy failed之类的报错说明你旧的代理中间件还在生效需要把RandomProxyMiddleware从DOWNLOADER_MIDDLEWARES里移除。实测下来这四步走完通道切换基本就确认了。数据库里的channel_pool表可以加一个定时任务每次请求后更新success_count和fail_count这样你能直观看到通道的健康度。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照改造过程中最容易撞上的几个报错我按实际遇到的频率排一下并给出排查路径。第一个是 401 Unauthorized。这个最常见原因通常是 Key 没读到、Key 过期、或者Authorization头格式不对。检查顺序先确认.env文件在项目根目录且被load_dotenv()加载再确认os.getenv(TAOTOKEN_API_KEY)返回的不是None最后确认请求头是Bearer sk-xxx而不是sk-xxx。Scrapy 中间件里如果用了request.headers[Authorization]注意 Scrapy 的 headers 是 bytes 类型最好用request.headers[bAuthorization]或者直接赋值字符串让它自动编码。第二个是local proxy failed。这个报错说明你本地还在走旧的代理逻辑请求被转发到了一个已经失效的 IP。排查方法是全局搜索request.meta[proxy]和proxies把相关代码删掉或注释。Scrapy 的process_request里如果返回了request而不是None也会导致中间件链异常检查返回值。第三个是reading choices相关的报错通常出现在解析响应时。如果你把 TaoToken 的响应直接当爬虫目标页解析response.json()[choices]可能因为返回结构不同而 KeyError。正确做法是先判断resp.status_code 200再取resp.json().get(choices, [])加默认值避免崩溃。第四个是 OAuth 相关报错比如OAuth token expired或invalid_grant。如果你之前用 Claude Code 的 OAuth 流程接入了别的服务现在切到 TaoToken 的 API Key 模式需要把旧的 OAuth 配置清掉。检查~/.claude/settings.json或者项目里的auth.json把oauth相关字段删掉改成api_key字段。TaoToken 的接入文档里有各工具的配置示例对照改就行。还有一个隐蔽的坑pymysql 连接没有设置autocommitTrue导致success_count更新不生效。建议在连接时加上autocommitTrue或者每次execute后手动commit。另外ORDER BY RAND()如果还在用建议改成ORDER BY last_used_at ASC LIMIT 1避免全表扫描。6. 从代理池到统一通道后续维护与扩展建议把代理出口和鉴权统一到 TaoToken 之后你的爬虫项目结构会清爽很多。requests 脚本和 Scrapy 中间件不再各自维护 IP 池pymysql 里的表从存 IP变成存通道状态维护成本主要就剩下监控成功率和处理限流了。后续如果要扩展有几个方向可以考虑。一是把channel_pool表做成多通道比如同时配置两个不同模型 ID 的通道按任务类型路由channel_name字段就是路由键。二是加一个简单的健康检查脚本定时跑一次call_channel把结果写回success_count和fail_count这样你能在数据库里直接看到通道可用率。三是把重试逻辑收敛到call_channel里遇到 429 时用指数退避重试而不是在每个爬虫脚本里各写一遍。如果你还在用 Cline MCP 或者 Codex 做辅助编码记得把auth.json里的 Base URL 也改成https://taotoken.net/apiKey 用同一套环境变量Model ID 对齐。这样你的编码工具和爬虫项目走的是同一个通道排查问题时只需要看一个地方。最后提醒一句API Key 一定要定期轮换控制台里可以随时删除旧 Key 生成新的。轮换的时候只需要改.env文件代码一行都不用动这就是统一通道带来的最大好处。