前言

做爬虫的同学肯定都经历过这样的绝望:
代码写得天衣无缝,逻辑跑得通,但在请求某个网站(尤其是海外站点)时,服务器直接返回 403 Forbidden,或者返回一个写着 "Just a moment..." 的 Cloudflare 质询页面。

这时候,无论你怎么修改 User-Agent,甚至换代理 IP,依然会被拦截。为什么?因为现在的 WAF(Web应用防火墙)已经不只看 HTTP Header 了,它们开始校验 TLS 指纹(JA3/JA4 Fingerprint)

本文将结合我最近的一个实战项目(爬取某电商数据平台),分享如何设计一个**“双模智能下载器”**,既能通过 curl_cffi 完美绕过 Cloudflare 盾,又能保持对普通 CDN 图片的高速下载,同时解决图片防盗链(Referer)问题。


一、 为什么会被 403?揭秘 TLS 指纹识别

1. 什么是 TLS 指纹?

当我们使用 Python 的 requests 或 httpx 库发起 HTTPS 请求时,客户端和服务器在建立加密连接(TLS 握手)的过程中,会发送一个 Client Hello 包。这个包里包含了加密套件列表、TLS 版本、扩展字段顺序等信息。

  • 浏览器(Chrome/Edge)发送的特征是固定的。

  • Python 脚本(requests/urllib)发送的特征也是固定的,但与浏览器完全不同

2. WAF 的逻辑

Cloudflare 等防火墙会检测这个特征。如果它发现请求头里写着 User-Agent: Chrome/120,但 TLS 指纹却是 python-requests 的特征,它就会判定为**“伪造身份的机器人”**,直接拦截。

这就是为什么你只是换个 UA 没用的原因——你的底裤(TLS指纹)暴露了你。


二、 解决方案:curl_cffi

为了解决这个问题,我们需要使用 curl_cffi 库。它是 curl-impersonate 的 Python 绑定,底层修改了 curl 的代码,使其能够完美模拟主流浏览器(Chrome, Safari, Edge)的 TLS 指纹。

安装:

code Bash

downloadcontent_copy

expand_less

pip install curl_cffi

三、 实战:构建“双模智能”图片下载器

在实际项目中,我们面临两个需求:

  1. 高防域名(如 findqc.com):有 Cloudflare 盾,必须用 curl_cffi 模拟指纹。

  2. 普通域名(如 alicdn.com):普通的 CDN 图片,防盗链较弱,但追求速度。

为了兼顾通过率和性能,我们设计了一个**“双客户端”**架构:根据 URL 的域名,动态切换下载引擎。

1. 初始化:准备两把“枪”

我们在类初始化时,同时建立 httpx 客户端(用于普通任务)和 curl_cffi 客户端(用于攻坚任务)。

code Python

downloadcontent_copy

expand_less

import httpx
from curl_cffi.requests import AsyncSession

class ImageDownloader:
    def __init__(self):
        # 1. 普通 HTTP 客户端 (httpx)
        # 优点:轻量、原生异步支持好、速度快
        # 缺点:无法通过强校验的 WAF
        self.httpx_client = httpx.AsyncClient(
            verify=False,
            headers={
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..."
            }
        )

        # 2. 特种作战客户端 (curl_cffi)
        # 优点:完美模拟浏览器 TLS 指纹,通过 Cloudflare
        # 缺点:底层是 C 绑定,开销稍大
        self.cffi_client = AsyncSession(
            impersonate="chrome120",  # 关键:模拟 Chrome 120
            timeout=30,
            headers={
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
                # 预设一些浏览器标准头
                "Sec-Fetch-Dest": "image",
                "Sec-Fetch-Mode": "no-cors",
                "Sec-Fetch-Site": "same-origin",
            }
        )

2. 核心逻辑:智能路由与防盗链处理

下载图片时,我们需要做两件事:

  1. 判断域名:决定用哪个客户端。

  2. 设置 Referer:解决图片防盗链(Hotlink Protection)。很多服务器会检查请求来源,如果 Referer 为空或不对,也会返回 403。

code Python

downloadcontent_copy

expand_less

from urllib.parse import urlparse

async def download_image(self, url: str):
    # 解析域名
    parsed = urlparse(url)
    
    # === 策略判断 ===
    use_cffi = False
    headers = {}
    
    # 针对特定高防域名,开启 CFFI 模式,并设置对应的 Referer
    if "findqc.com" in parsed.netloc:
        use_cffi = True
        headers["Referer"] = "https://findqc.com/"
        
    # 针对普通 CDN 域名,使用普通模式,但也要设置 Referer 防盗链
    elif "alicdn.com" in parsed.netloc:
        headers["Referer"] = "https://www.alibaba.com/"
    
    # === 发送请求 ===
    try:
        if use_cffi:
            # 使用模拟浏览器指纹的客户端
            print(f"正在使用 curl_cffi 下载: {url}")
            response = await self.cffi_client.get(url, headers=headers)
        else:
            # 使用普通客户端
            print(f"正在使用 httpx 下载: {url}")
            response = await self.httpx_client.get(url, headers=headers)
            
        if response.status_code == 200:
            # 保存图片...
            with open("image.jpg", "wb") as f:
                f.write(response.content)
            return True
        else:
            print(f"下载失败: {response.status_code}")
            return False
            
    except Exception as e:
        print(f"异常: {e}")
        return False

四、 关键技术点总结

1. impersonate="chrome120"

这是 curl_cffi 的核心参数。它告诉底层库:“请按照 Chrome 版本 120 的标准,去构造 TLS Client Hello 包”。这包括加密算法的排列顺序、TLS 扩展字段等,让 WAF 认为这就是一个真实的浏览器。

2. User-Agent 与 TLS 指纹的一致性

千万不要犯这个低级错误:用 curl_cffi 模拟了 Chrome 的指纹,却在 Header 里写自己是 Firefox。
这种“精神分裂”的行为会立刻被 WAF 识别。确保 impersonate 的版本和 headers['User-Agent'] 保持一致。

3. Referer 防盗链

图片返回 403 不一定是因为 IP 被封,很有可能是触发了防盗链。

  • 现象:浏览器能看图,代码下载不了。

  • 解决:将 Referer 头设置为图片所在的网页地址,或者该图片的主域名首页(例如 https://www.google.com或目标网站首页)。


五、 完整代码示例

将上述逻辑整合,我们可以得到一个健壮的异步图片下载器类:

code Python

downloadcontent_copy

expand_less

import asyncio
import httpx
from curl_cffi.requests import AsyncSession
from urllib.parse import urlparse
from pathlib import Path

class SmartImageDownloader:
    def __init__(self):
        # 普通客户端
        self.httpx_client = httpx.AsyncClient(
            headers={"User-Agent": "Mozilla/5.0 ..."}
        )
        # 高防客户端
        self.cffi_client = AsyncSession(
            impersonate="chrome120",
            headers={"User-Agent": "Mozilla/5.0 ..."}
        )

    async def close(self):
        await self.httpx_client.aclose()
        self.cffi_client.close()

    async def download(self, url, save_path):
        parsed = urlparse(url)
        
        # 智能路由逻辑
        if "findqc.com" in parsed.netloc:
            client = self.cffi_client
            headers = {"Referer": "https://findqc.com/"}
        else:
            client = self.httpx_client
            headers = {}
            if "alicdn" in parsed.netloc:
                headers["Referer"] = "https://taobao.com/"

        try:
            resp = await client.get(url, headers=headers)
            if resp.status_code == 200:
                with open(save_path, 'wb') as f:
                    f.write(resp.content)
                print(f"下载成功: {save_path}")
            else:
                print(f"下载失败 {resp.status_code}: {url}")
        except Exception as e:
            print(f"下载异常: {e}")

# 使用示例
async def main():
    downloader = SmartImageDownloader()
    # 这是一个高防图片
    await downloader.download("https://findqc.com/api/test.jpg", "test1.jpg")
    # 这是一个普通图片
    await downloader.download("https://img.alicdn.com/test.jpg", "test2.jpg")
    await downloader.close()

if __name__ == "__main__":
    asyncio.run(main())

结语

爬虫与反爬虫是一场永恒的博弈。对于 Cloudflare 这种级别的对手,单纯修改 HTTP 报文已经不够了,必须深入到底层协议栈。通过 curl_cffi 模拟 TLS 指纹 加上 正确的 Referer 策略,我们可以通过 90% 以上的常见 WAF 防护。

希望这篇文章能帮到正在为此头秃的你!如果有问题,欢迎在评论区交流。


本文首发于 CSDN,转载请注明出处。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐