1461 字
7 分钟
Cloudflare 防护下的合规采集实践:架构与关键代码
核心库速览
本文示例主要基于以下 Python 库:
requests用于 HTTP 会话管理、重试策略挂载、流式下载。urllib3(Retry) 用于网络抖动与 429/5xx 场景的指数退避重试。beautifulsoup4用于解析 HTML 并提取下载链接。urllib.robotparser用于读取并判断robots.txt访问许可。playwright用于动态页面访问;在挑战页场景仅做识别与人工兜底,不做自动绕过。camoufox(浏览器方案可选核心) 用于基于 Firefox 内核的自动化会话管理;适合需要完整浏览器上下文的下载流程。browserforge(与camoufox常配套) 用于提供屏幕与浏览器相关配置能力,提升自动化环境的一致性。
安装示例:
pip install requests urllib3 beautifulsoup4 playwrightpip install -U "camoufox[geoip]" browserforgeplaywright installpython -m camoufox fetch背景
很多站点会使用 Cloudflare 保护下载链路与页面内容。
在工程实践里,真正可长期运行的方案不是“对抗防护”,而是合规访问 + 稳定采集管道。
本文给出一套可落地的技术框架,并附上关键代码:
- 识别挑战页并停止自动化流程。
- 检查
robots.txt与访问权限。 - 限速、重试、缓存与日志审计。
- 必要时切换人工处理流程。
常见自动化脚本的技术结构
典型下载脚本一般包含这些模块:
- 页面状态识别:判断是否命中挑战页、登录页、错误页。
- 会话管理:复用 cookie/session,减少重复握手成本。
- 链接提取:从资源页解析真实下载链接。
- 下载落盘:监听下载结果、重命名、冲突避让、失败重试。
- 超时控制:统一截止时间,避免无限等待。
这套结构本身是中立的,关键在于用途是否合规。
关键代码一:挑战页识别(仅识别,不绕过)
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)用途说明:
- 用于检测并中止自动采集。
- 用于记录日志和告警。
- 不用于自动点击挑战控件或规避验证流程。
关键代码二:合规下载脚本(可运行示例)
下面示例展示了“合规路径”:
- 先检查
robots.txt。 - 采用限速 + 指数退避重试。
- 命中挑战页后直接停止任务并提示人工处理。
#!/usr/bin/env python3# -*- coding: utf-8 -*-
import argparseimport randomimport timefrom pathlib import Pathfrom urllib.parse import urljoin, urlparsefrom urllib.robotparser import RobotFileParser
import requestsfrom bs4 import BeautifulSoupfrom requests.adapters import HTTPAdapterfrom 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()生产环境检查清单
- 是否有官方 API、数据导出、授权渠道。
- 是否配置真实身份
User-Agent与联系方式。 - 是否遵守
robots.txt和服务条款。 - 是否设置限速、重试、超时、熔断。
- 是否记录审计日志(URL、时间、状态码、重试次数)。
- 是否准备人工兜底流程(验证码/登录过期)。
总结
Cloudflare 场景下,工程目标应从“绕过”转向“合规可持续”:
可识别访问身份 + 可审计流程 + 稳定重试和限速策略 + 人工兜底机制。
这种路径更稳定,也更适合长期业务运行。
Cloudflare 防护下的合规采集实践:架构与关键代码
https://blog.dl-am.cn/posts/cloudflare-hegui-caiji/