爬虫反爬进阶——IP代理池、请求指纹、字体反爬实战

入门级反爬(User-Agent、IP 封禁、请求频率限制)好解决,但到了进阶阶段,会遇到更多花样。这篇讲三个最实用的反爬对抗技术。

一、IP 代理池------突破封 IP

爬得稍微快一点,网站就封 IP。解决方案是维护一个代理池,IP 被封了自动换。

1. 免费代理获取(思路)

python 复制代码
import requests
from bs4 import BeautifulSoup
import time

def fetch_free_proxies():
    """从免费代理网站采集代理IP(示例)"""
    proxies = []
    url = "https://www.free-proxy-list.com/"
    resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
    soup = BeautifulSoup(resp.text, "html.parser")

    for row in soup.select("table tr")[1:]:  # 跳过表头
        cols = row.select("td")
        if len(cols) >= 2:
            ip = cols[0].text.strip()
            port = cols[1].text.strip()
            proxies.append(f"{ip}:{port}")

    return proxies

需要注意的是: 免费代理质量很差(90% 不可用,速度慢),小规模采集凑合用,大规模采集建议买付费代理。

2. 验证代理是否可用

python 复制代码
import threading

def check_proxy(proxy):
    """测试代理是否可用"""
    try:
        resp = requests.get(
            "http://httpbin.org/ip",
            proxies={"http": proxy, "https": proxy},
            timeout=5
        )
        if resp.status_code == 200:
            print(f"✅ {proxy} 可用 - {resp.text.strip()}")
            return True
    except:
        pass
    return False

# 多线程验证所有代理
def validate_all(proxies):
    valid = []
    threads = []
    lock = threading.Lock()

    def check(p):
        if check_proxy(p):
            with lock:
                valid.append(p)

    for p in proxies:
        t = threading.Thread(target=check, args=(p,))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    return valid

3. 代理池管理器

python 复制代码
import random
import time
import requests

class ProxyPool:
    """简单的代理池管理器"""

    def __init__(self):
        self.proxies = []
        self.blacklist = set()

    def add_proxy(self, proxy):
        if proxy not in self.blacklist:
            self.proxies.append(proxy)

    def get_proxy(self):
        """随机返回一个代理"""
        if not self.proxies:
            return None
        return random.choice(self.proxies)

    def mark_bad(self, proxy):
        """标记代理不可用"""
        if proxy in self.proxies:
            self.proxies.remove(proxy)
            self.blacklist.add(proxy)
        print(f"💀 代理 {proxy} 已移除,剩余 {len(self.proxies)} 个")

    def request_with_retry(self, url, max_retries=5):
        """带代理重试的请求"""
        for i in range(max_retries):
            proxy = self.get_proxy()
            if not proxy:
                print("代理池已空,等待补充...")
                return None

            try:
                resp = requests.get(
                    url,
                    proxies={"http": proxy, "https": proxy},
                    timeout=10,
                    headers={"User-Agent": "Mozilla/5.0"}
                )
                if resp.status_code == 200:
                    return resp
                else:
                    self.mark_bad(proxy)
            except:
                self.mark_bad(proxy)

            time.sleep(0.5)

        print(f"重试 {max_retries} 次全部失败: {url}")
        return None

4. 付费代理推荐

免费代理不稳定,真干活建议买:

  • 快代理:国内最大的代理服务商,动态代理按量计费
  • 芝麻代理:价格便宜,几块钱能用一天
  • 站大爷:支持动态按需提取

一般几十块钱能用几个月,省心很多。

二、请求指纹------伪装浏览器特征

现在的反爬不仅看 IP 和 UA,还会检测TLS 指纹、Headers 顺序、WebDriver 等。

1. 完整的请求头伪装

python 复制代码
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Connection": "keep-alive",
    "Referer": "https://www.google.com/",
    "Sec-Ch-Ua": '"Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
}

2. 随机切换

python 复制代码
import random
from fake_useragent import UserAgent

ua = UserAgent()

class RandomHeaders:
    """随机生成请求头"""

    @staticmethod
    def get_headers():
        user_agents = [
            ua.chrome,
            ua.edge,
            ua.firefox,
        ]

        return {
            "User-Agent": random.choice(user_agents),
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": random.choice([
                "zh-CN,zh;q=0.9,en;q=0.8",
                "en-US,en;q=0.9,zh-CN;q=0.8",
                "zh-CN,zh;q=0.9",
            ]),
            "Referer": random.choice([
                "https://www.baidu.com/",
                "https://www.google.com/",
                "https://www.bing.com/",
            ]),
        }

3. 使用 requests 会话重用

python 复制代码
# 每次都新建会话,容易被识别
# 正确的做法是复用 Session
session = requests.Session()
session.headers.update(RandomHeaders.get_headers())

# 连续请求,保持同一指纹
resp1 = session.get("https://example.com/page1")
resp2 = session.get("https://example.com/page2")

三、字体反爬------最常见的反爬手段

很多网站(大众点评、猫眼电影、58同城)用自定义字体来加密数字,你爬下来看到的是乱码。

原理

复制代码
正常数字:0 1 2 3 4 5 6 7 8 9
加密后:  ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉

网页通过 @font-face 加载一个自定义字体文件(.woff 或 .ttf)
字体文件里把字符映射关系打乱了
所以浏览器能正确显示,但你爬到的文本是乱码

解决方案

python 复制代码
from fontTools.ttLib import TTFont
import re
import requests

def parse_font_anti_spider(html_text, font_url):
    """解析字体反爬"""

    # 1. 下载字体文件
    resp = requests.get(font_url)
    with open("temp.woff", "wb") as f:
        f.write(resp.content)

    # 2. 解析字体映射关系
    font = TTFont("temp.woff")
    cmap = font.getBestCmap()

    # 3. 建立映射关系
    # cmap 返回的是 {unicode编码: 字形名称}
    # 比如 {0xe001: 'one', 0xe002: 'two', ...}
    # 需要根据字形名称找到对应的数字

    unicode_to_digit = {}
    num_map = {
        "one": "1", "two": "2", "three": "3", "four": "4", "five": "5",
        "six": "6", "seven": "7", "eight": "8", "nine": "9", "zero": "0",
    }

    for unicode_val, glyph_name in cmap.items():
        # 字形名称可能类似 "uniE001" 或 "one"
        glyph_name = glyph_name.lower()
        for eng, digit in num_map.items():
            if eng in glyph_name:
                unicode_to_digit[chr(unicode_val)] = digit

    # 4. 替换加密字符
    for enc_char, digit in unicode_to_digit.items():
        html_text = html_text.replace(enc_char, digit)

    return html_text

在线字体反爬(动态加载更麻烦)

很多网站已经升级到了动态字体------每次请求都生成新的字体文件,映射关系每次都不一样:

python 复制代码
# 解决方案思路:
# 1. 每次请求都下载最新的字体文件
# 2. 通过字形轮廓计算来识别数字

# 更简单的方法:用 OCR 识别
# from PIL import Image
# import pytesseract
# 但准确率不如直接解析字体文件

四、Selenium 防检测

用 Selenium 时,网站可以通过 window.navigator.webdriver 检测到你是自动化工具。

1. 隐藏 WebDriver 特征

python 复制代码
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)

driver = webdriver.Chrome(options=options)

# 注入 JS 修改 navigator 属性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
        Object.defineProperty(navigator, 'plugins', {
            get: () => [1, 2, 3, 4, 5]
        });
        Object.defineProperty(navigator, 'languages', {
            get: () => ['zh-CN', 'zh']
        });
    """
})

2. 使用 undetected-chromedriver

有个专门的库解决了大部分检测问题:

bash 复制代码
pip install undetected-chromedriver
python 复制代码
import undetected_chromedriver as uc

driver = uc.Chrome()
driver.get("https://example.com")
# 自动隐藏了 WebDriver 特征,比手动配置稳定得多

五、实际工作流:如何选择反爬策略

反爬级别 特征 应对方法
⭐ 入门级 封 IP、限频率 加延时 + 轮换 UA
⭐⭐ 初级 检测请求头、验证码 Session 复用 + 代理池
⭐⭐⭐ 中级 字体反爬、动态加载 下载字体解析 + 抓接口
⭐⭐⭐⭐ 高级 WebDriver 检测、风控系统 undetected-chromedriver + 行为模拟
⭐⭐⭐⭐⭐ 顶级 滑块验证、人机识别 打码平台 + 行为轨迹模拟

核心原则: 反爬对抗不是越强越好,够用就行。加 3 秒延迟 + 轮换 UA 能解决 80% 的问题,没必要一上来就上分布式代理池。


💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。