1. 爬虫代理池为什么越维护越累:requests 与 Scrapy 中间件各管一套的坑
做爬虫的朋友大概率都经历过这个阶段:一开始用 requests 写几个脚本,随手从免费代理站抓一批 IP 塞进 pymysql,跑得挺欢;后来量上来了,换成 Scrapy 做分布式,又得在中间件里再写一套代理逻辑。结果就是同一个项目里,requests 的proxies字典和 Scrapy 的request.meta['proxy']各维护一份 IP 池,鉴权信息散落在三四个文件里,今天这个 IP 挂了要删,明天那个账号额度用完了要换,维护成本直接翻倍。
我试过最原始的做法,就是 excerpt 里那种:用 requests 爬免费 IP,insert project_ip写进 MySQL,然后GetIP类里judge_ip一个个试,get_random_ip随机取一条。这套逻辑本身没毛病,问题出在三个地方。第一,免费 IP 的可用率极低,judge_ip里那个requests.get(http_url, proxies=proxy_dict)经常超时,删库删到手软。第二,ORDER BY RAND()在数据量上万之后性能断崖式下跌,每次取 IP 都要全表扫描。第三,也是最要命的,requests 脚本和 Scrapy 中间件用的是两套完全独立的取 IP 逻辑,你在 requests 里删掉的失效 IP,Scrapy 那边根本不知道,还在继续用。
更现实的问题是鉴权。当你从免费 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,进去之后新建一个 Key,复制出来保存好,后面配置里会用到。Model ID 根据你实际要调用的模型填,比如做文本处理类的爬虫后处理,就填对应的模型标识。
这里要强调一点:不要把 API Key 硬编码在爬虫脚本里。正确的做法是放到环境变量或者独立的配置文件,代码里通过os.environ读取。我见过太多人把 Key 直接写在settings.py里然后提交到 Git,结果第二天就被人刷爆了。你可以建一个.env文件,内容大概是这样:
TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的实际key TAOTOKEN_MODEL_ID=你的模型ID然后在 Python 里用python-dotenv加载,或者直接在启动脚本里export。Scrapy 项目的话,可以在settings.py里通过os.getenv读取,这样本地开发和线上部署用的是同一套代码,只是环境变量不同。
另外,如果你之前用的是 Claude Code 或者类似的编码工具,可能会遇到 OAuth 相关的配置。TaoToken 的接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意这里不再存 API Key,Key 只放在环境变量里,数据库只记录通道的可用状态和成功率。这样即使数据库被拖,也不会泄露密钥。初始化一条记录:
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": f"Bearer {API_KEY}", "Content-Type": "application/json", } def call_channel(payload, timeout=30): url = f"{BASE_URL}/v1/chat/completions" resp = requests.post(url, headers=build_headers(), json=payload, timeout=timeout) 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(..., proxies=proxy_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, signal=signals.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"] = f"Bearer {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/api,Key 填环境变量里的值,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": f"Bearer {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, headers=headers, json=payload) 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[b"Authorization"]或者直接赋值字符串让它自动编码。
第二个是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 连接没有设置autocommit=True,导致success_count更新不生效。建议在连接时加上autocommit=True,或者每次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/api,Key 用同一套环境变量,Model ID 对齐。这样你的编码工具和爬虫项目走的是同一个通道,排查问题时只需要看一个地方。
最后提醒一句:API Key 一定要定期轮换,控制台里可以随时删除旧 Key 生成新的。轮换的时候只需要改.env文件,代码一行都不用动,这就是统一通道带来的最大好处。