网络爬虫中的 405 Method Not Allowed:原因与解决方法

405 Method Not Allowed 响应表示服务器找到了您的 URL,但拒绝了您发送的 HTTP 方法。在爬虫场景中,实际情况往往并非如此。有些服务器会用 405 来回应自动化客户端,这就是为什么您的脚本在一个 Chrome 中能正常加载的页面上会失败。
本指南面向正在用 Python 调试爬虫的开发者。它介绍如何在大约两分钟内区分这两种情况,以及针对每种情况该怎么做。
405 Method Not Allowed 错误是什么意思
405 是一种客户端错误。服务器找到了资源并理解了请求,但您使用的方法不在该资源接受的方法列表中。
RFC 9110(于 2022 年 6 月取代了 RFC 7231)要求服务器在每个 405 响应中都附带 允许 头,列出它们支持的方法。几乎没有调试爬虫的人会去读它。而这正是区分真正的方法错误与服务器拒绝的最快方式。
同一状态码会因服务器软件不同而以不同的措辞出现:
- 405 Method Not Allowed
- HTTP Error 405 – Method Not Allowed
- 405 Not Allowed
- The requested method POST is not allowed for the URL
- HTTP verb used to access this page is not allowed
- This page isn’t working – HTTP ERROR 405
它们在协议层面的含义完全相同。
为什么您的爬虫会收到 405,而浏览器却不会
爬虫中出现的 405 错误来自三种情况之一。每种情况需要不同的修复方式,因此靠猜测只会浪费您的时间。
| 原因 | 你看到的现象 | 确认方法 |
|---|---|---|
| 请求方法确实有误 | 在 API 或表单端点上每次都返回 405 | 允许 响应头列出的方法与您所使用的不同 |
| 服务器正在拒绝您的客户端 | 本应接受 GET 的普通页面返回 405 | 同一 URL 在 Chrome 中可以正常加载 |
| CORS 预检请求被拒绝 | 浏览器自动化发出的 OPTIONS 请求返回 405 | 仅在注入跨域请求时出现 |
服务器为何返回 405 而不是 403
在普通页面上,GET 显然是被允许的,因此那里出现的 405 其实与请求方法无关。一些防护层会用 405 而非 403 来回应非浏览器客户端,因为 405 透露的信息更少。403 表明您已被识别并遭到拒绝,而 405 则会让您去白白折腾改写请求方法。
暴露 Python 客户端身份的通常不是 User-Agent 字符串,而是它底下的一切。
- TLS 握手。
requests 库通过 OpenSSL 协商 TLS。它的密码套件列表、扩展顺序和支持的密钥组与 Chrome 的并不一致,而这一组合足够稳定,可以用来生成指纹。JA3 和 JA4 哈希描述的正是这一点。 - 协议版本。 Chrome 与大多数大型网站通信时使用 HTTP/2 或 HTTP/3。
requests 库只支持 HTTP/1.1。这在读取任何一个请求头之前就已将二者区分开来。 - HTTP/2 设置。 确实使用 HTTP/2 的客户端会发送自己的
SETTINGS值和窗口大小,而这些会因库的不同而有所差异。 - 标头顺序。 浏览器发送标头的顺序是固定一致的。
requests 库则按自己的顺序添加自己的默认标头,与任何浏览器都不匹配。
因此,仅仅换上一个 Chrome 的 User-Agent 有时毫无效果。四个信号里您只修好了一个。
当方法确实用错了的时候
这个错误的“真实版本”通常出现在您请求的是从 DevTools 里挖出来的接口,而不是一个页面。搜索接口、分页处理程序和登录表单通常只接受 POST。
打开 Network 标签页,找到失败的请求,查看 Method 一列。如果显示的是 POST,而您的代码发送的是 GET,问题就找到了。前面关于封锁的诊断一概不适用。
如何在两分钟内诊断 405 错误
请按顺序执行以下步骤。只要某一步给出了明确答案,就可以停下。
1. 查看 允许 标头。 这是最快的检查,通常单凭它就能解决问题。
python
import requests
url = "https://example.com/api/search"
response = requests.options(url, timeout=10)
print(response.status_code)
print(response.headers.get("Allow"))如果返回结果中列出了 GET,而您的 GET 请求却被拒绝,那么问题不在方法上。如果只列出了 POST,那问题就在方法上。
2. 在浏览器中打开该 URL。 在 Chrome 中可以打开,在您的脚本中却失败,而且是同一网络:区别就在于您的客户端。
3. 检查失败发生的时间点。 第一个请求就失败,说明问题在于请求的构造方式。成功五十次之后才失败,说明遇到了速率限制。
4. 查看响应标头。 cf-ray, server: cloudflare 或 Akamai 标头,则意味着是防护层而非应用程序本身做出了响应。
5. 等待十五分钟,然后原样再次发送同一个请求。 如果这样能成功,说明您只是触发了临时限制。请降低请求速度,而不要重写任何代码。
| 诊断 | 前往 |
|---|---|
允许 列出了一个您并未使用的方法 | 修复方案 1 |
| 在 Chrome 中能正常加载,但脚本从第一个请求起就失败 | 修复方案 2 |
| 先能正常运行,N 次请求后停止 | 修复方案 3 |
| 等待一段时间后自行恢复 | 修复方案 3,并降低您的请求速率 |
修复 1:发送端点期望的请求方法
一旦 允许 标头告诉了您该发送什么,这一步就很简单。请重现 DevTools 向您展示的那个请求,包括负载和内容类型。
python
import requests
url = "https://example.com/api/search"
payload = {"query": "laptops", "page": 1}
response = requests.post(url, json=payload, timeout=10)
print(response.status_code)重定向常在这里让人栽跟头。如果请求经过了 301 或 302 重定向,某些客户端会在中途把 POST 改成 GET,于是您的方法在您所写的 URL 上是正确的,而在实际到达的 URL 上却是错误的。请打印 response.history 并检查最终请求究竟是什么样,然后再归咎于端点。
修复 2:发送完整的请求
如果端点接受您的方法却拒绝您的客户端,那么问题在于请求本身不完整。浏览器每次导航都会发送十几个标头,而一个裸的 requests.get() 只发送四个。
python
import requests
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1",
}
session = requests.Session()
session.headers.update(headers)
response = session.get("https://example.com/", timeout=10)
print(response.status_code)使用 会话 而不是独立的单次调用。它会在请求之间保留 cookie 并复用连接,就像浏览器那样。每次都新建连接且不携带任何 cookie 的独立请求,在任何带登录功能的网站上都会显得异常。
请使用 Referer 且仅在符合实际的情况下添加。如果请求携带的来源页面并未链接到目标地址,那比完全不发送还要糟糕。
如果换上一整套完整的请求头后仍无变化,那么剩下的就是上文所述的 TLS 和协议差异。 curl_cffi 和 HTTPX 库 使用 http2=True 可以缩小部分差距。而 爬虫浏览器 则能弥补其余差距,但会牺牲速度,因为它是真正的浏览器引擎。
修复 3:将请求分散到更多 IP 地址
如果 405 只在一连串成功请求之后才出现,或者等待一段时间后自行消失,那么触发原因就是来自单一地址的请求量。问题不在请求头,重写请求头也无济于事。
先降速。在请求之间加入延迟,并保持适度的并发。网站公开其速率限制的频率比人们预想的要高,遵守限制比想办法绕过限制成本更低。
然后分散负载。 轮换住宅代理 为每个请求或每个会话分配不同的 IP,这样单地址限制就不再是整个任务的上限。需要保持会话的工作,例如任何登录后的操作,更适合使用粘性会话或静态 IP,因为在会话中途更换地址会触发新的重新验证。
python
proxies = {
"http": "http://YOUR_USERNAME:YOUR_PASSWORD@PROXY_HOST:PROXY_PORT",
"https": "http://YOUR_USERNAME:YOUR_PASSWORD@PROXY_HOST:PROXY_PORT",
}
response = session.get(url, proxies=proxies, timeout=15)
print(response.status_code)IP 质量在这里之所以重要,有一个很实际的原因。在其他地方被标记过的地址会带着这份信誉记录一起到达,因此轮换一个低质量 IP 池可能比使用一个干净 IP 失败得更频繁。NodeMaven 会在 IP 到达您手中之前先对 IP 池进行筛选。
不浪费流量的重试逻辑
大多数爬虫 以同样的方式重试所有非 200 响应。对 405 而言,这是错误的默认做法。真正的方法不匹配会永远以同样的方式失败,而每次尝试都会消耗流量。
在首次失败时分类一次,然后据此采取行动。
python
import time
import requests
def fetch(session, url, proxy_pool, max_attempts=3):
for attempt in range(max_attempts):
proxies = proxy_pool[attempt % len(proxy_pool)]
response = session.get(url, proxies=proxies, timeout=15)
if response.status_code != 405:
return response
allowed = response.headers.get("Allow", "")
if allowed and "GET" not in allowed:
raise ValueError(f"GET not supported here. Allowed: {allowed}")
time.sleep(2 ** attempt)
return None切勿重试已确认的方法不匹配。在两次尝试之间更换 IP,而不是从同一个 IP 重复请求。采用退避策略而不是连续猛发请求,因为临时限制需要的是时间,而不是又一个请求。
405 与 403、404、401 和 415 的区别
这些状态码经常被混淆,因为症状是一样的。您的请求没有通过。
| 代码 | 资源存在 | 请求格式正确 | 方法被允许 | 允许访问 | 在数据采集中的常见含义 |
|---|---|---|---|---|---|
| 400 Bad Request | n/a | 否 | n/a | n/a | 请求载荷格式错误或参数无效 |
| 401 未授权 | 是 | 是 | 是 | 需要身份验证 | 缺少令牌或会话已过期 |
| 403 Forbidden | 是 | 是 | 是 | 否 | 您已被识别并遭到拒绝 |
| 404 未找到 | 否 | 是 | n/a | n/a | URL 错误,或该资源对您隐藏 |
| 405 Method Not Allowed | 是 | 是 | 否 | 是 | 方法错误,或是变相的拒绝 |
| 415 Unsupported Media Type | 是 | 是 | 是 | 是 | 错误 Content-Type 在 POST 请求中 |
同一网站上的 403 和 405 在实践中往往意味着同一件事,只是 403 直接承认了而已。
405 在不同工具中的表现形式
| 工具 | 你看到的现象 | 首先检查 |
|---|---|---|
| requests 库 | response.status_code == 405,不会抛出异常 | 您是否有在检查状态码 |
| HTTPX 库 | 同上,除非您调用 raise_for_status() | 开启 http2=True |
| Scrapy | 响应默认被丢弃,405 未列入 handle_httpstatus_list | DOWNLOAD_DELAY 和 CONCURRENT_REQUESTS_PER_DOMAIN |
| Playwright | response.status() 作用于导航响应 | 页面本身失败还是后台 XHR 失败 |
| curl | 仅返回响应体,除非您传入 -i 或 -v | 运行 curl -X OPTIONS -i 阅读 允许 标头 |
Scrapy 值得一提。它会在您的解析器运行之前丢弃非 2xx 响应,因此一个什么都不返回的爬虫 (spider) 可能正在收到您从未看到的 405。在调试期间,将 405 添加到 handle_httpstatus_list ,响应就会到达您的回调函数。
什么时候应该停止
有时,405 是网站在表明它不欢迎自动化流量,此时正确的做法是不再打扰它。
检查 robots.txt 和服务条款,再决定是否继续投入时间。寻找官方 API,它通常比抓取前端更快也更稳定。数据采集必须遵守法律以及平台自身的条款,本文所有内容都以在允许采集的网站上获取公开数据为前提。




