# Python中的Cloudflare“Just a Moment”：为什么requests会失败

Cloudflare的“Just a moment...”页面是一个JavaScript挑战，而Python requests无法运行JavaScript。以下是如何检测它，为什么它在服务器上比在笔记本电脑上更容易触发，为什么cloudscraper风格的技巧不再足够，以及我们在2026年9月29日测试中通过了什么。

“Just a moment...”是Cloudflare的挑战页面。它要求客户端运行JavaScript并证明它是一个浏览器，然后网站才会响应。Python requests无法运行JavaScript，并且从数据中心服务器发出的请求，其IP和TLS握手已经看起来是自动化的。通过的方法是使用干净IP的真实浏览器，或者运行一个web unlocker。

本指南展示了如何在Python中检测挑战，为什么相同的脚本在你的笔记本电脑上通过而在你的服务器上失败，为什么cloudscraper时代的技巧不再足够，以及我们在2026年9月29日测试中实际通过了什么。

- **这是一个挑战，不是错误。** Cloudflare将被挑战的响应标记为`cf-mitigated: challenge`头。在我们的测试中，它们以HTTP 403到达。
- **头信息无法解决。** 'Enable JavaScript and cookies to continue' 意味着页面需要一个JavaScript引擎。没有User-Agent字符串能提供一个。
- **服务器与笔记本电脑**主要是IP声誉加上TLS指纹。在我们的测试中，仅靠住宅IP无法通过Indeed或Glassdoor。
- **通过的方法：** Web Unlocker通过了Glassdoor（在无头浏览器后，17.2秒）和Indeed（首次尝试，2.6秒）。
- **固定一个会话。** 重用浏览器的清除将第二次Glassdoor请求从14.0秒减少到1.4秒。

## “Just a moment...”页面实际上是什么

Cloudflare称其为插页式挑战页面：在Cloudflare决定访问者是否为自动化时，访客与网站之间的全页面门。其[挑战页面文档](https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/)描述了三种类型。非交互式挑战只需要浏览器运行注入的JavaScript。托管挑战让Cloudflare从请求的信号中选择挑战类型。交互式挑战要求访客做一些事情。当检查通过时，原始请求继续到网站，浏览器获得一个`cf_clearance` cookie，使其可以暂时跳过门。

Python HTTP客户端从未到达那一步。它下载挑战页面，没有引擎来运行脚本，并将门的HTML交给你，而不是你请求的页面。

## 为什么挑战返回403，以及如何检测它

在我们的测试中，每个挑战页面都以状态403到达，这就是为什么`raise_for_status()`抛出`403 Client Error: Forbidden`，人们搜索'cloudflare just a moment 403'。状态本身无法区分挑战和其他拒绝。可靠的信号是一个头信息：Cloudflare[文档说明被挑战的响应带有cf-mitigated: challenge](https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/)，无论你请求的资源是什么。首先检查这个，然后作为备用检查标题：

```python
import re
import requests

URL = "https://www.glassdoor.com/Reviews/Google-Reviews-E9079.htm"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}

def title_of(html: str) -> str:
    m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else ""

def is_cloudflare_challenge(r: requests.Response) -> bool:
    if r.headers.get("cf-mitigated") == "challenge":      # documented by Cloudflare
        return True
    if "just a moment" in title_of(r.text).lower():
        return True
    return "enable javascript and cookies to continue" in r.text[:20000].lower()

r = requests.get(URL, headers=HEADERS, timeout=30)
print(r.status_code, is_cloudflare_challenge(r), title_of(r.text))
# 29 Sep 2026, plain request through a residential proxy (US exit):
# 403, body asking to 'Enable JavaScript and cookies to continue'
```

不要用相同的客户端在紧密循环中重试被挑战的请求。每次重试都会获取相同的门，一些网站会对重复失败以更严格的规则回应。这就是爬虫陷入“Just a moment”循环的原因。

## “Enable JavaScript and cookies to continue”和“your browser does not support JavaScript”

这些字符串是挑战页面的备用文本，显示给不执行其脚本的客户端。当Python requests显示其中一个时，网站并不是在抱怨你的头信息。它是在字面上告诉你，它需要一个JavaScript引擎和一个cookie jar来保存脚本设置的内容。

这正是我们从Indeed和Glassdoor的普通住宅运行中收到的：一个403，带有'Enable JavaScript and cookies to continue'，来自一个干净的家庭IP。添加一个Chrome User-Agent、Accept-Language头或Referer没有任何改变，因为页面并不是在读取它们来决定。它在等待脚本运行。

## 为什么你的爬虫在本地工作，但在服务器上被Cloudflare阻止

这是最常见的抱怨版本：脚本在笔记本电脑上工作，然后一旦在云VM上运行就得到“Just a moment”。这两台机器之间有两件事改变。

- **IP。**在你的笔记本电脑上，脚本从家庭ISP地址离开。在VM上，它从托管提供商的范围离开，机器人防御在读取任何其他内容之前就将其评分为可能是自动化的。我们的法兰克福数据中心服务器被Glassdoor、Indeed、Zillow和Leboncoin等拒绝。
- **TLS指纹。** Python的握手在两台机器上看起来像Python，但在家庭IP上网站可能会容忍它，而在托管IP上则很少会。我们关于[curl_cffi vs requests](https://quantumproxies.io/blog/curl-cffi-vs-requests)的解释显示了库控制的握手有多少。

我们测试的住宅列显示了仅修复IP的限制。Zillow和Leboncoin通过住宅代理的普通请求提供了真实页面。Indeed和Glassdoor仍然以JavaScript挑战回应。你的笔记本电脑可以在更严格的网站上遇到同样的墙。它只是遇到的频率较低。

![同一个 Python requests 脚本在两台机器上运行：在使用家庭宽带 ISP IP 的笔记本电脑上，页面通常能正常加载；在使用托管 IP 的云服务器上，Cloudflare 会返回“Just a moment”挑战。卡片列出了变化的部分，即出口 IP，以及没有变化的部分：Python 的 TLS 握手和缺失的 JavaScript 引擎。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1790701776/blog/generated/qp-unl-g3-diag1.png)
*相同的脚本，两台机器。IP改变了；Python TLS握手和缺失的JavaScript引擎没有改变。*

## 为什么cloudscraper时代的技巧不再有效

[cloudscraper](https://github.com/VeNoMouS/cloudscraper)是为Cloudflare的旧版“I'm Under Attack”页面构建的。该页面主要检查客户端是否可以运行JavaScript计算，因此库获取挑战，在嵌入的解释器中解决脚本，发送答案并保留cookie，所有这些都在`requests`之上。很长一段时间这就足够了，这也是“cloudscraper替代方案”现在成为人们搜索的原因。

自那时以来，发生了几件事情，它们不依赖于任何一个库版本：

- **握手仍然显示Python。**基于requests构建的库通过Python的TLS堆栈连接，因此ClientHello与其User-Agent声称的浏览器不匹配。这种不匹配在任何JavaScript运行之前就可见。
- **挑战不再只是算术。**托管挑战从请求的信号中选择检查，Cloudflare的JavaScript检测读取浏览器环境。评估脚本的解释器不是渲染页面的浏览器。
- **清除与客户端绑定。** Cloudflare的[清除文档](https://developers.cloudflare.com/cloudflare-challenges/concepts/clearance/)表示cf_clearance cookie与其颁发给的访客和设备绑定，以防止跨机器重用。从你的浏览器复制一个到另一台机器上的脚本是设计上会失败的。
- **IP仍然是你的。**解决页面不会改变托管范围的IP，有些网站在提供任何挑战之前就拒绝这些。

截至2026年9月29日，cloudscraper库的最后一次提交是在2025年6月。这并不是对任何特定网站的判决，我们没有为本文对其进行基准测试。这确实意味着你应该针对自己的目标测试它，而不是假设它有效。每种替代方案都解决了部分问题。curl_cffi再现了浏览器TLS握手，但不运行JavaScript。Playwright或其他真实浏览器运行挑战，但仍需要一个干净的IP，并且在大规模操作时较重。解锁器结合了两者，并在两者都不起作用时告诉你。

## 通过的方法：web unlocker路径

QuantumProxies.io Web Unlocker分阶段处理GET。首先，最多三次TLS尝试，每次在不同浏览器指纹下的新鲜住宅出口上。如果每次尝试都被挑战，它会升级到运行挑战的无头浏览器。如果页面在此之后仍被阻止，你会收到一个错误状态，带有`x-qp-unlocker-blocked`和一个阻止类，而不是一个伪装成200的挑战页面。在Python中，它是一个代理设置：

```python
import os
import time
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

user = os.environ["QP_UNLOCK_USER"] + "-country-us"          # unlock-XXXXXXXX-country-us
PROXY = f"http://{user}:{os.environ['QP_UNLOCK_PASSWORD']}@unlock.quantumproxies.io:9000"

s = requests.Session()
s.proxies = {"http": PROXY, "https": PROXY}
s.verify = False                     # or the unlocker CA file from /api/v1/scraper/unlock/ca
s.headers["x-qp-session"] = "glassdoor-01"   # same value on every call: same exit, reusable clearance

for attempt in (1, 2):               # same page twice here; in practice, the next page
    t0 = time.monotonic()
    r = s.get(URL, timeout=120)
    print(attempt, r.status_code, f"{time.monotonic() - t0:.1f}s",
          "rendered:", r.headers.get("x-qp-unlocker-rendered"),
          "clearance:", r.headers.get("x-qp-clearance"),
          "block:", r.headers.get("x-qp-block-class"))
```

`x-qp-*`头由解锁器读取，从不转发到网站。如果你宁愿完全不处理证书，REST端点`POST https://app.quantumproxies.io/api/v1/scraper/unlock`在JSON体中接收URL和你的API密钥，并在响应中返回`blocked`、`blockClass`和`vendor`。我们的[web unlocker API教程](https://quantumproxies.io/blog/web-unlocker-api-tutorial-python)介绍了这两种方法。

## 已测量：通过的两个Cloudflare风格的墙

测量于2026年9月29日。普通请求是带有Chrome User-Agent的curl，从法兰克福数据中心服务器和通过美国出口的住宅代理发出。解锁器使用美国出口的住宅层。时间是一个请求的挂钟时间。

| 网站 | 普通，数据中心服务器 | 普通，住宅代理 | Web Unlocker |
| --- | --- | --- | --- |
| Glassdoor（公司评论） | 403，“安全”页面 | 403，“启用JavaScript和cookie以继续” | 200，2次尝试+无头浏览器，17.2秒 |
| Indeed（工作搜索） | 403，“安全检查”页面 | 403，“启用JavaScript和cookie以继续” | 200，1次尝试，无浏览器，2.6秒 |

Glassdoor是完整路径。两次TLS尝试被挑战，然后无头浏览器运行挑战并返回评论页面。Indeed是有趣的：普通住宅请求收到的挑战在解锁器的第一次尝试中从未出现。一个新鲜住宅出口上的浏览器TLS指纹就足够了，无需浏览器。

[通过Web Unlocker绕过JavaScript挑战](https://quantumproxies.io/web-unlocker)

## 重用清除：为什么第二次请求很快

浏览器升级是慢的部分，你不想在每个页面上为此付费。当无头浏览器通过挑战时，它获得了一个清除。发送相同的`x-qp-session`值，或在代理用户名中保持相同的`-session-<id>`，在后续请求中它们从相同的出口离开，重用该清除而不是再次运行浏览器。`x-qp-clearance`头告诉你每次调用发生了什么：`minted`表示获得了新的清除，`hit`表示重用了存储的清除，`miss`表示没有可重用的。

在2026年9月29日的Glassdoor测试中，使用固定在代理用户名中的会话和两个单独的连接，第一次请求通过浏览器在14.0秒内完成（`minted`），第二次在1.4秒内返回（`hit`）。固定很重要，因为Cloudflare如何绑定清除：它与获得它的访客和设备绑定，并在网站配置的持续时间内保持有效，除非Cloudflare决定会话看起来可疑。改变出口或指纹，你又是一个新访客。

![在同一个固定的 Web Unlocker 会话中向 Glassdoor 发出两个请求：第一个请求在无头浏览器中运行 Cloudflare 挑战，用 14.0 s 生成 clearance 凭证；第二个请求在同一出口上复用该凭证，1.4 s 即返回。数据测于 2026年9月29日。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1790701776/blog/generated/qp-unl-g3-diag2.png)
*第一次请求为浏览器付费；同一固定会话中的后续请求重用清除。*

## 当普通请求在Cloudflare网站上仍然有效时

许多网站仅作为CDN运行在Cloudflare后面，或者具有允许普通流量通过的机器人规则，普通请求在它们上面有效。仅仅因为“在Cloudflare上”并不是使用浏览器的理由。信号是响应。如果没有`cf-mitigated: challenge`头，没有“Just a moment”标题，也没有403，请保持普通请求，并仅升级那些实际被挑战的URL。我们的教程中的plain-first助手正是这样做的。

两个相邻的问题有自己的指南。一个显示“Error 1020 Access denied”的页面是一个拒绝你的防火墙规则，而不是一个挑战，没有多少JavaScript可以修复它；请参阅[是什么触发了Cloudflare错误1020以及如何修复它](https://quantumproxies.io/blog/cloudflare-error-1020-access-denied)。对于Python之外的Cloudflare防御的更广泛图景，请阅读[如何在2026年绕过Cloudflare挑战](https://quantumproxies.io/blog/how-to-bypass-cloudflare-challenges-in-2026-what-actually-matters-now)。如果墙后面的页面是Glassdoor，我们的[Glassdoor薪资和评论爬取指南](https://quantumproxies.io/blog/scrape-glassdoor-salary-reviews)涵盖了数据方面。

## 常见问题

### 为什么我的爬虫在本地工作，但在云中被Cloudflare阻止？

因为IP改变了。在家里，你的脚本从住宅ISP地址离开；在云VM上，它从托管范围离开，机器人防御对其一见面就不信任。Python TLS握手在两者上是相同的，在更严格的网站上，即使从家里也会受到挑战。在我们的测试中，仅靠住宅IP无法通过Indeed或Glassdoor。

### 为什么Cloudflare的“Just a moment”页面返回403？

挑战页面是代替你请求的页面提供的，在我们的测试中，它带有状态403，因此requests会抛出“403 Client Error: Forbidden”。状态并不是特定于挑战。检查cf-mitigated响应头，Cloudflare文档说明在每个挑战页面响应上设置为challenge，或“Just a moment”标题。

### cloudscraper仍然有效吗，有什么好的替代方案？

它可能仍然在设置宽松的网站上有效，因此针对你的目标测试它。它运行在Python的TLS堆栈上，并在解释器中解决脚本，而当前的挑战也会查看握手和浏览器环境。替代方案：curl_cffi用于浏览器握手，真实浏览器用于JavaScript，或同时执行这两者的web unlocker。

### Python requests能否独立通过Cloudflare挑战？

不能。requests不执行JavaScript，因此无法完成依赖于运行页面脚本的挑战。它可以从没有挑战你的Cloudflare网站获取页面。一旦cf-mitigated: challenge出现，你需要一个真实的浏览器，直接或通过运行一个的服务，理想情况下还有一个干净的住宅IP。

### Cloudflare清除持续多长时间？

只要网站配置的时间。Cloudflare称该设置为Challenge Passage，如果Cloudflare决定会话看起来可疑，清除可能会提前结束。cf_clearance cookie与获得它的访客和设备绑定，因此只有在后续请求保持相同的出口和指纹时才有帮助，这就是固定x-qp-session的作用。

简短版本：“Just a moment...”要求一个浏览器，因此requests中的头信息技巧无法满足它。用`cf-mitigated`检测它，保持普通请求用于所有未被挑战的内容，将被挑战的URL通过运行真实浏览器的东西从干净IP发送，并固定一个会话，这样你就只为那个浏览器付费一次，而不是每个页面。

[在住宅出口上试用Web Unlocker](https://quantumproxies.io/web-unlocker/residential)
