1. 爬虫选型先想清楚requests、Beautiful Soup、Selenium 各自能干什么写爬虫最怕的不是代码难而是工具选错。我见过太多人拿 Selenium 去抓一个纯静态的新闻列表页一个页面等 3 秒渲染1000 页跑了一晚上也见过有人硬用 requests 去怼一个 Vue 单页应用抓回来的 HTML 里只有一行div idapp/div然后怀疑人生。所以这篇不堆框架名词而是按「页面长什么样 → 该用哪个工具」这条线来讲。核心就三个角色requests负责发 HTTP 请求拿回原始 HTML/JSON。它不管解析也不管渲染就是个「快递员」。Beautiful Soup负责解析 HTML/XML从文档树里把你要的字段抠出来。它不管请求你给它一段 HTML 它就能干活。Selenium负责驱动真实浏览器等 JS 执行完、DOM 渲染好之后再取内容。它慢但能搞定动态页面。这三者不是竞争关系而是流水线协作关系。一个典型的采集链路是requests 拿列表页 → Beautiful Soup 解析出详情页链接 → 对动态详情页用 Selenium 渲染 → 再用 Beautiful Soup 解析渲染后的 DOM。那什么时候该切换给你一张判断表直接照着选页面特征推荐工具原因静态 HTML字段在源码里requests Beautiful Soup最快无浏览器开销返回 JSON 接口requests 直接解析 JSON根本不用解析 HTML内容由 JS 异步加载Selenium / Playwright源码里没有数据需要登录、点按钮、翻页交互Selenium能模拟真实操作反爬有 JS 挑战、指纹校验Selenium 真实浏览器环境更接近真人大规模并发采集requests asyncio/aiohttp浏览器扛不住并发这张表是我实际踩坑后总结的。判断标准只有一个你要的数据在「查看网页源代码」里能不能搜到。能搜到 → requests搜不到但「检查元素」里有 → Selenium。本文除了讲这三个工具的协作还会解决一个很现实的痛点当你同时用 requests、Selenium、甚至后面接大模型做内容清洗时每个工具都要配一套 API Key、Base URL、超时重试管理起来很乱。我会演示怎么用 TaoToken 的统一 Key 和 API 通道把这些调用凭证收敛到一处顺便给出请求成功率和耗时的验证步骤。适合谁看刚学 Python 爬虫、纠结选型的人已经会写但采集链路散乱、想统一管理的人以及想给爬虫接上模型能力比如自动摘要、字段抽取的人。2. 环境准备与 TaoToken 统一 Key 前置配置先把依赖装好。我习惯用虚拟环境避免污染全局python -m venv spider-env source spider-env/bin/activate # Windows 用 spider-env\Scripts\activate pip install requests beautifulsoup4 lxml selenium openai python-dotenv这里解释下几个包beautifulsoup4是解析库本体lxml是它推荐的解析器比内置 html.parser 快很多selenium负责浏览器驱动openai是用来调模型接口的 SDKpython-dotenv用来管理环境变量。Selenium 还需要浏览器驱动。Chrome 用户装webdriver-manager可以自动匹配版本pip install webdriver-manager接下来是重点统一凭证管理。爬虫项目里你往往不止一个外部调用——可能要用模型做正文抽取、做摘要、做分类。如果每个脚本都硬编码 Key改一次要翻十个文件。我的做法是把所有模型调用收敛到 TaoToken 的 API 通道一个 Key 管全部。先去控制台创建一个 API Key# 控制台地址创建 Key、查看额度 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite创建好之后在项目根目录建一个.env文件# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api注意 Base URL 是https://taotoken.net/api不带任何多余路径。然后在代码里这样读取import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) def extract_fields(html_text: str) - str: resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是网页字段抽取助手只输出JSON。}, {role: user, content: f从下面HTML里抽取标题、作者、发布时间\n{html_text[:3000]}}, ], timeout30, ) return resp.choices[0].message.content这样写的好处是requests 抓页面、Selenium 渲染、模型抽取全走同一套凭证。你换模型只改model字段换通道只改.env代码一行不动。提示.env一定要加进.gitignore别把 Key 提交到仓库。这是新手最容易犯的错。如果你后面要做长期编码或 Agent 类采集任务可以了解下 Coding Plan额度更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite前置配置到这就够了。核心原则凭证集中、代码解耦、环境隔离。下面进入三个工具的具体写法。3. 可复制配置requests、Beautiful Soup、Selenium 最小示例与切换这一节给你三份能直接跑的最小示例以及一份可复制的配置片段。3.1 requests Beautiful Soup 抓静态页先看最经典的组合。假设要抓一个静态博客列表页import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_static(url: str) - list[dict]: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止中文乱码 soup BeautifulSoup(resp.text, lxml) items [] for card in soup.select(article.post): items.append({ title: card.select_one(h2 a).get_text(stripTrue), link: card.select_one(h2 a)[href], date: card.select_one(time).get_text(stripTrue), }) return items if __name__ __main__: for row in fetch_static(https://example.com/blog): print(row)几个关键点raise_for_status()让 4xx/5xx 直接抛异常别让错误静默apparent_encoding处理中文站点编码select用 CSS 选择器比find_all直观。3.2 Selenium 处理动态渲染如果列表页是 JS 渲染的requests 拿不到数据换 Seleniumfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager def fetch_dynamic(url: str) - list[dict]: options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) driver webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions, ) try: driver.get(url) WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, article.post)) ) cards driver.find_elements(By.CSS_SELECTOR, article.post) return [ { title: c.find_element(By.CSS_SELECTOR, h2 a).text, link: c.find_element(By.CSS_SELECTOR, h2 a).get_attribute(href), } for c in cards ] finally: driver.quit()WebDriverWait是必须的别用time.sleep硬等既慢又不稳。finally里quit()保证浏览器进程不残留。3.3 统一配置片段把公共配置抽出来方便三个工具共用。用 JSON 存一份{ request: { timeout: 10, retries: 3, backoff: 1.5, headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0 } }, selenium: { headless: true, page_load_timeout: 20, wait_selector: article.post, wait_timeout: 15 }, taotoken: { base_url: https://taotoken.net/api, model: gpt-4o-mini, timeout: 30 } }读取时import json with open(config.json, encodingutf-8) as f: CFG json.load(f)这样切换工具时超时、重试、选择器都在一处改。踩过的坑早期我把这些参数散在各个脚本里后来站点改版选择器要改 8 个文件从那以后全部收敛到配置。3.4 切换判断的代码化把第 1 节的判断表落成函数自动决定用哪个工具def choose_fetcher(url: str) - str: resp requests.get(url, headersHEADERS, timeout10) soup BeautifulSoup(resp.text, lxml) # 如果源码里能解析出目标节点说明是静态页 if soup.select(article.post): return requests return selenium先花一次请求探测再决定渲染方式能省下大量浏览器开销。这个思路在大批量采集时特别值。4. 验证请求成功率与耗时实测步骤工具选好了怎么证明它真的稳别凭感觉用数据说话。这一节给你一套可复制的验证流程。4.1 成功率统计写一个带重试和计数的采集器import time import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session() - requests.Session: s requests.Session() retry Retry( total3, backoff_factor1.5, status_forcelist[429, 500, 502, 503, 504], allowed_methods[GET], ) s.mount(https://, HTTPAdapter(max_retriesretry)) s.headers.update(HEADERS) return s def batch_fetch(urls: list[str]) - dict: session build_session() ok, fail, total_time 0, 0, 0.0 for url in urls: start time.perf_counter() try: r session.get(url, timeout10) r.raise_for_status() ok 1 except Exception as e: fail 1 print(f[FAIL] {url} - {e}) total_time time.perf_counter() - start n len(urls) return { total: n, success: ok, fail: fail, success_rate: f{ok / n:.2%}, avg_latency: f{total_time / n:.3f}s, }跑 50 个 URL你会得到类似{total: 50, success: 50, fail: 0, success_rate: 100.00%, avg_latency: 0.412s}4.2 对比三种工具耗时同一批页面分别用 requests、Selenium 跑记录平均耗时。我实测下来静态页 requests 平均 0.4sSelenium 平均 3.2s差了近 8 倍。这就是为什么能不用浏览器就不用。import statistics def benchmark(fn, urls): times [] for u in urls: t time.perf_counter() fn(u) times.append(time.perf_counter() - t) return { avg: f{statistics.mean(times):.3f}s, p95: f{sorted(times)[int(len(times)*0.95)]:.3f}s, }4.3 验证模型调用通道采集到的内容要送模型抽取时先验证通道通不通def verify_taotoken() - None: resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 回复OK两个字}], timeout15, ) print(通道正常返回, resp.choices[0].message.content)返回OK就说明 Key、Base URL、模型 ID 三件套都对。如果报错对照下一节排查。注意验证时把timeout设短一点15s避免卡住。生产环境再按需调大。4.4 端到端链路验证把三步串起来跑一遍requests 抓列表 → 解析出详情链接 → 模型抽取字段。记录每段耗时找出瓶颈。通常瓶颈在浏览器渲染和模型调用requests 那一段几乎可以忽略。这套验证流程的价值在于上线前你就知道成功率多少、慢在哪、哪一步会挂而不是等跑崩了才回头查。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来每个都给你原因和修法。5.1 401 Unauthorizedopenai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}原因通常是三种Key 复制时带了空格或换行.env没被load_dotenv()读到Base URL 写错导致请求打到别处。排查顺序import os print(repr(os.getenv(TAOTOKEN_API_KEY))) # 看有没有多余字符 print(os.getenv(TAOTOKEN_BASE_URL)) # 应为 https://taotoken.net/api确认 Base URL 是https://taotoken.net/apiKey 无空格。如果还报 401去控制台重新生成一个 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite5.2 local proxy failed / 连接被拒requests.exceptions.ProxyError: HTTPSConnectionPool ... local proxy failed这是本地网络环境或代理配置导致的。检查环境变量里有没有残留的代理设置echo $HTTP_PROXY echo $HTTPS_PROXY如果有且你不需要它清掉import os os.environ.pop(HTTP_PROXY, None) os.environ.pop(HTTPS_PROXY, None)或者在 requests 里显式禁用session.trust_env False5.3 reading choices 报错AttributeError: NoneType object has no attribute choices这通常出现在模型返回结构异常时resp.choices是 None。原因可能是请求被拦截、返回了错误体但没抛异常。加一层防御resp client.chat.completions.create(...) if not resp.choices: raise RuntimeError(f空响应{resp}) content resp.choices[0].message.content同时检查model字段拼写模型 ID 写错有时不会直接报错而是返回异常结构。5.4 OAuth / 认证相关报错Error: OAuth token expired or invalid如果你用的是需要 OAuth 的工具链比如某些 CLI 编码工具token 过期会报这个。解决方式是重新走一遍授权或者改用 API Key 方式。用 TaoToken 的 API Key 通道可以绕开 OAuth 的过期问题凭证更稳定。5.5 Selenium 相关报错selenium.common.exceptions.NoSuchElementException元素找不到八成是页面还没渲染完。把find_element换成WebDriverWaitWebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, article.post)) )如果是SessionNotCreatedException多半是 Chrome 和驱动版本不匹配用webdriver-manager自动管理即可。5.6 三件套检查清单任何模型调用报错先核对这三样项目正确值Base URLhttps://taotoken.net/apiAPI Key控制台生成无空格Model ID如gpt-4o-mini拼写准确三件套对了90% 的报错都能解决。剩下 10% 看网络和超时。6. 把采集链路接上模型统一 Key 的长期用法前面把三个工具和排障都讲完了最后说说怎么让这套链路长期跑得省心。采集只是第一步真正耗精力的是后续处理正文去噪、字段抽取、内容分类、摘要生成。这些如果每个都单独配一套凭证维护成本会指数级上升。用 TaoToken 统一 Key 之后你只需要维护一份.env所有模型调用都走同一个通道。一个典型的组合用法requests 批量抓列表 → Beautiful Soup 解析出正文 → 模型做字段抽取和摘要 → 结果落库。整条链路里模型调用部分完全解耦换模型、调参数都不影响爬虫逻辑。如果你要验证不同模型在抽取任务上的效果可以直接在模型对话里试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite把一段真实 HTML 贴进去对比不同模型的抽取准确率选定后再写进代码。接入文档在这里参数和错误码都有说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite长期跑采集任务的话Coding Plan 的额度模型更适合高频调用场景比按次计费划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后给一个实用技巧把采集器的重试、超时、模型调用封装成一个Pipeline类每个环节独立可测。这样站点改版时你只改解析层模型换版本时你只改调用层互不干扰。爬虫项目的稳定性从来不是靠某个框架多强而是靠分层清晰、凭证集中、每步可验证。