1461 字
7 分钟
Cloudflare 防护下的合规采集实践:架构与关键代码

核心库速览#

本文示例主要基于以下 Python 库:

  1. requests 用于 HTTP 会话管理、重试策略挂载、流式下载。
  2. urllib3Retry) 用于网络抖动与 429/5xx 场景的指数退避重试。
  3. beautifulsoup4 用于解析 HTML 并提取下载链接。
  4. urllib.robotparser 用于读取并判断 robots.txt 访问许可。
  5. playwright 用于动态页面访问;在挑战页场景仅做识别与人工兜底,不做自动绕过。
  6. camoufox(浏览器方案可选核心) 用于基于 Firefox 内核的自动化会话管理;适合需要完整浏览器上下文的下载流程。
  7. browserforge(与 camoufox 常配套) 用于提供屏幕与浏览器相关配置能力,提升自动化环境的一致性。

安装示例:

Terminal window
pip install requests urllib3 beautifulsoup4 playwright
pip install -U "camoufox[geoip]" browserforge
playwright install
python -m camoufox fetch

背景#

很多站点会使用 Cloudflare 保护下载链路与页面内容。
在工程实践里,真正可长期运行的方案不是“对抗防护”,而是合规访问 + 稳定采集管道

本文给出一套可落地的技术框架,并附上关键代码:

  1. 识别挑战页并停止自动化流程。
  2. 检查 robots.txt 与访问权限。
  3. 限速、重试、缓存与日志审计。
  4. 必要时切换人工处理流程。

常见自动化脚本的技术结构#

典型下载脚本一般包含这些模块:

  1. 页面状态识别:判断是否命中挑战页、登录页、错误页。
  2. 会话管理:复用 cookie/session,减少重复握手成本。
  3. 链接提取:从资源页解析真实下载链接。
  4. 下载落盘:监听下载结果、重命名、冲突避让、失败重试。
  5. 超时控制:统一截止时间,避免无限等待。

这套结构本身是中立的,关键在于用途是否合规。

关键代码一:挑战页识别(仅识别,不绕过)#

from typing import Iterable
CF_MARKERS: Iterable[str] = (
"just a moment",
"cf-challenge",
"enable javascript and cookies",
"__cf_chl_",
"cf-mitigated",
"checking if the site connection is secure",
)
def is_cloudflare_challenge(url: str, title: str, body_html: str) -> bool:
u = (url or "").lower()
t = (title or "").lower()
b = (body_html or "").lower()
if "cdn-cgi/challenge-platform" in u or "__cf_chl_" in u:
return True
if "just a moment" in t:
return True
return any(marker in b for marker in CF_MARKERS)

用途说明:

  1. 用于检测并中止自动采集。
  2. 用于记录日志和告警。
  3. 不用于自动点击挑战控件或规避验证流程。

关键代码二:合规下载脚本(可运行示例)#

下面示例展示了“合规路径”:

  1. 先检查 robots.txt
  2. 采用限速 + 指数退避重试。
  3. 命中挑战页后直接停止任务并提示人工处理。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import random
import time
from pathlib import Path
from urllib.parse import urljoin, urlparse
from urllib.robotparser import RobotFileParser
import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
CF_MARKERS = (
"just a moment",
"cf-challenge",
"enable javascript and cookies",
"__cf_chl_",
"cf-mitigated",
"checking if the site connection is secure",
)
def is_cloudflare_challenge(url: str, title: str, body_html: str) -> bool:
u = (url or "").lower()
t = (title or "").lower()
b = (body_html or "").lower()
if "cdn-cgi/challenge-platform" in u or "__cf_chl_" in u:
return True
if "just a moment" in t:
return True
return any(marker in b for marker in CF_MARKERS)
def build_session() -> requests.Session:
session = requests.Session()
retry = Retry(
total=4,
connect=4,
read=4,
backoff_factor=1.2,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET", "HEAD"),
raise_on_status=False,
)
adapter = HTTPAdapter(max_retries=retry, pool_connections=20, pool_maxsize=20)
session.mount("https://", adapter)
session.mount("http://", adapter)
session.headers.update(
{
"User-Agent": "MyCrawler/1.0 (+contact: admin@example.com)",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
)
return session
def check_robots_allowed(target_url: str, user_agent: str = "*") -> bool:
parsed = urlparse(target_url)
robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
rp = RobotFileParser()
rp.set_url(robots_url)
try:
rp.read()
except Exception:
# robots 无法读取时,按保守策略返回 False 也可以
return False
return rp.can_fetch(user_agent, target_url)
def polite_get(session: requests.Session, url: str, min_delay: float = 1.0, max_delay: float = 2.5) -> requests.Response:
time.sleep(random.uniform(min_delay, max_delay))
return session.get(url, timeout=(10, 45))
def extract_download_link(resource_html: str, resource_url: str) -> str:
soup = BeautifulSoup(resource_html, "html.parser")
a = soup.select_one("a[href*='/download?']")
if not a:
raise RuntimeError("未找到下载链接,页面结构可能已变更。")
href = a.get("href", "").strip()
if not href:
raise RuntimeError("下载链接为空。")
return urljoin(resource_url, href)
def stream_download(session: requests.Session, download_url: str, output_dir: Path) -> Path:
with session.get(download_url, timeout=(10, 60), stream=True) as resp:
resp.raise_for_status()
filename = "download.bin"
cd = resp.headers.get("Content-Disposition", "")
if "filename=" in cd:
filename = cd.split("filename=")[-1].strip().strip('"')
output_dir.mkdir(parents=True, exist_ok=True)
target = output_dir / filename
idx = 1
while target.exists():
target = output_dir / f"{target.stem}_{idx}{target.suffix}"
idx += 1
with target.open("wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
return target
def main() -> int:
parser = argparse.ArgumentParser(description="合规下载示例:挑战页检测 + robots 检查 + 稳定下载")
parser.add_argument("--url", required=True, help="资源页 URL")
parser.add_argument("--out", default="downloads", help="下载目录")
args = parser.parse_args()
url = args.url.strip()
out_dir = Path(args.out).resolve()
if not check_robots_allowed(url):
raise SystemExit("robots.txt 不允许采集,任务终止。")
session = build_session()
res = polite_get(session, url)
res.raise_for_status()
title = ""
try:
soup = BeautifulSoup(res.text, "html.parser")
title = (soup.title.text or "").strip() if soup.title else ""
except Exception:
pass
if is_cloudflare_challenge(res.url, title, res.text):
raise SystemExit("命中 Cloudflare 挑战页,自动流程停止,请走人工授权或官方 API。")
download_url = extract_download_link(res.text, res.url)
file_path = stream_download(session, download_url, out_dir)
print(f"下载完成: {file_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

关键代码三:浏览器自动化的“人工确认模式”#

某些场景必须使用浏览器(动态渲染、登录态页面)。
推荐模式:命中挑战页后停止自动动作,等待人工处理或授权。

from playwright.sync_api import sync_playwright
def is_cf_page(url: str, title: str, html: str) -> bool:
text = f"{url}\n{title}\n{html}".lower()
keys = ["cdn-cgi/challenge-platform", "just a moment", "__cf_chl_", "cf-challenge"]
return any(k in text for k in keys)
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com/resource", wait_until="domcontentloaded", timeout=60000)
title = page.title()
html = page.content()
if is_cf_page(page.url, title, html):
print("检测到挑战页:停止自动流程,请人工完成验证后再继续。")
else:
print("页面可访问,继续执行后续业务逻辑。")
browser.close()

生产环境检查清单#

  1. 是否有官方 API、数据导出、授权渠道。
  2. 是否配置真实身份 User-Agent 与联系方式。
  3. 是否遵守 robots.txt 和服务条款。
  4. 是否设置限速、重试、超时、熔断。
  5. 是否记录审计日志(URL、时间、状态码、重试次数)。
  6. 是否准备人工兜底流程(验证码/登录过期)。

总结#

Cloudflare 场景下,工程目标应从“绕过”转向“合规可持续”:
可识别访问身份 + 可审计流程 + 稳定重试和限速策略 + 人工兜底机制
这种路径更稳定,也更适合长期业务运行。

Cloudflare 防护下的合规采集实践:架构与关键代码
https://blog.dl-am.cn/posts/cloudflare-hegui-caiji/
作者
Code
发布于
2026-02-25
许可协议
CC BY-NC-SA 4.0