{T}

AI 爬虫方案全解

当 LLM 遇上爬虫:从 AI 辅助提取到自主 Agent 爬取,探索下一代数据采集范式。

概述

2024 年以来,大语言模型(LLM)的爆发深刻改变了数据采集的方式。传统爬虫需要为每个网站编写定位规则(CSS 选择器、XPath),而 AI 爬虫让模型自动理解页面结构和语义,直接提取目标数据——甚至自主规划爬取路径、处理反爬、生成结构化输出。

本章系统梳理当前主流的 AI 爬虫方案,从轻量级的 AI 辅助提取到完全自主的 Agent 爬虫,帮助你根据场景选择合适的方案。

图表渲染中…

方案全景对比

方案代表工具原理学习成本API 成本准确率适用场景
AI 辅助提取LLM API + BeautifulSoup传统爬虫获取 HTML,LLM 解析提取85-95%结构化页面、字段提取
ScrapeGraphAIscrapegraphai图结构管道,LLM 编排全流程80-90%快速原型、多网站通用
Crawl4AIcrawl4ai专注智能抓取+Markdown转换85-95%内容采集、RAG 数据准备
FireCrawlfirecrawl (SaaS)云端爬取+结构化输出极低中-高85-90%无需编码、SaaS 开箱即用
browser-usebrowser-useLLM Agent 操控浏览器70-85%复杂交互、多步操作
自研 AgentClaude/GPT + Playwright自主规划+执行+纠错60-80%极复杂场景、需要推理

1. AI 辅助提取:LLM 替代选择器

最轻量的 AI 爬虫方案——传统爬虫负责请求和获取 HTML,LLM 负责理解和提取数据。无需编写任何 CSS 选择器或 XPath

图表渲染中…

实战:用 LLM 提取商品信息

python
import requests
import json
from bs4 import BeautifulSoup
from anthropic import Anthropic

# 步骤1:获取页面 HTML
response = requests.get(
    'https://example.com/product/123',
    headers={'User-Agent': 'Mozilla/5.0 ...'},
)
soup = BeautifulSoup(response.text, 'lxml')

# 步骤2:提取页面主要文本内容(去除导航/页脚等噪声)
# 关键:给 LLM 过多的 HTML 会让它分心和浪费 token
main_content = soup.get_text(separator='\n', strip=True)

# 如果内容太长,截取关键部分
if len(main_content) > 8000:
    main_content = main_content[:8000]

# 步骤3:调用 LLM 提取结构化数据
client = Anthropic()

message = client.messages.create(
    model='claude-sonnet-4-20250514',
    max_tokens=1024,
    messages=[{
        'role': 'user',
        'content': f"""从以下网页文本中提取商品信息,返回 JSON 格式。

要求:
- name: 商品名称
- price: 价格(数字)
- original_price: 原价(如有,否则 null)
- rating: 评分(数字,如 4.5)
- reviews_count: 评论数
- availability: 是否有货(true/false)
- specs: 规格参数字典

网页内容:
{main_content}

请只返回 JSON,不要其他文字。"""
    }],
)

# 步骤4:解析 LLM 返回的 JSON
result_text = message.content[0].text
# 尝试提取 JSON(LLM 可能返回带 ```json 标记的内容)
if '```json' in result_text:
    result_text = result_text.split('```json')[1].split('```')[0].strip()

product = json.loads(result_text)
print(json.dumps(product, ensure_ascii=False, indent=2))
# {"name": "iPhone 15 Pro Max", "price": 9999, "original_price": 10999, ...}
核心优势
  • 零选择器:不需要为每个网站写 CSS/XPath,LLM 直接理解语义
  • 抗改版:网站改版后选择器会失效,但 LLM 基于语义理解不受影响
  • 一次提取多字段:一个 prompt 提取所有字段,比多个选择器更高效
注意事项
  • Token 成本:每个页面都要发送完整 HTML 给 LLM,成本远高于传统选择器
  • 延迟:LLM API 调用需要 1-5 秒,不适合高吞吐量场景
  • 准确率:约 85-95%,关键业务需要人工校验
  • 最佳实践:先用 BeautifulSoup 清理 HTML,只发送页面主要文本区域给 LLM,减少 token 消耗

2. ScrapeGraphAI:端到端 AI 爬虫框架

ScrapeGraphAI 是第一个专为 LLM 驱动爬虫设计的 Python 框架。它将爬虫流程建模为**图(Graph)**结构,每个节点是一个处理步骤(抓取、解析、LLM 提取),LLM 负责编排整个流程。

图表渲染中…

安装与配置

bash
pip install scrapegraphai

# 需要 LLM API Key(支持 OpenAI / Anthropic / 本地模型)
export OPENAI_API_KEY=sk-...
# 或使用本地模型(免费,但需要 Ollama)
# pip install scrapegraphai[ollama]

实战:一行代码爬取结构化数据

python
import json
from scrapegraphai.graphs import SmartScraperGraph

# 定义数据 Schema(告诉 LLM 你要提取什么)
graph_config = {
    "llm": {
        "model": "openai/gpt-4o-mini",  # 或 "ollama/llama3"
        "api_key": "sk-...",             # 或不设,使用本地模型
    },
}

# 创建爬虫图
scraper = SmartScraperGraph(
    prompt="提取所有商品信息:名称、价格、评分、链接",
    source="https://example.com/products",
    config=graph_config,
)

# 执行爬取
result = scraper.run()
print(json.dumps(result, ensure_ascii=False, indent=2))
# {
#   "products": [
#     {"name": "Product A", "price": "$29.99", "rating": "4.5", "link": "..."},
#     {"name": "Product B", "price": "$49.99", "rating": "4.8", "link": "..."},
#   ]
# }

高级用法:自定义图结构

python
from scrapegraphai.graphs import SmartScraperGraph

# 爬取+搜索组合
graph_config = {
    "llm": {"model": "openai/gpt-4o-mini", "api_key": "sk-..."},
    "verbose": True,
}

scraper = SmartScraperGraph(
    prompt="从页面中提取所有博客文章的标题、摘要、发布日期和作者",
    source="https://blog.example.com",
    config=graph_config,
    schema_provided={  # 可选:提供 Schema 约束输出格式
        "type": "object",
        "properties": {
            "articles": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "title": {"type": "string"},
                        "summary": {"type": "string"},
                        "date": {"type": "string"},
                        "author": {"type": "string"},
                    }
                }
            }
        }
    }
)

result = scraper.run()

3. Crawl4AI:智能爬取 + Markdown 转换

Crawl4AI 专注于智能抓取和内容转换——将网页转换为结构化的 Markdown 文本,为 RAG(检索增强生成)和 AI 应用提供高质量数据。

bash
pip install crawl4ai

核心特性

python
import asyncio
from crawl4ai import AsyncWebCrawler

async def crawl_example():
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(
            url='https://example.com/article',
            # 自动去除导航、广告、页脚等噪声
            word_count_threshold=10,
            # 提取主要内容(类似浏览器阅读模式)
            css_selector='article',
            # 输出 Markdown 格式
            output_format='markdown',
        )

        print(result.markdown)  # 干净的 Markdown 文本
        print(result.cleaned_html)  # 清理后的 HTML

        # 提取结构化数据
        result = await crawler.arun(
            url='https://example.com/products',
            extraction_strategy='llm',  # LLM 辅助提取
            query='提取所有商品的名称和价格',
        )
        print(result.extracted_content)

asyncio.run(crawl_example())

批量爬取与缓存

python
async def batch_crawl(urls: list[str]) -> list[dict]:
    """批量爬取多个 URL"""
    results = []
    async with AsyncWebCrawler() as crawler:
        for url in urls:
            result = await crawler.arun(
                url=url,
                word_count_threshold=10,
                output_format='markdown',
            )
            results.append({
                'url': url,
                'title': result.metadata.get('title', ''),
                'content': result.markdown,
            })
    return results
Crawl4AI 的定位

Crawl4AI 不适合传统爬虫场景(如价格监控、数据采集入库),但在以下场景中无可替代:

  • RAG 数据准备:将网页转为 Markdown 供 LLM 检索
  • 内容聚合:采集文章/新闻,转换为统一格式
  • 知识库构建:批量爬取+清洗+格式化为 AI 可读内容

4. FireCrawl:SaaS 开箱即用

FireCrawl 是一个云端 AI 爬虫服务,无需编写代码,通过 API 调用即可获取结构化数据:

python
# pip install firecrawl-py
from firecrawl import FirecrawlApp

app = FirecrawlApp(api_key='fc-...')

# 爬取单个页面
result = app.scrape_url(
    'https://example.com/product/123',
    params={
        'formats': ['extract'],
        'extract': {
            'schema': {
                'type': 'object',
                'properties': {
                    'name': {'type': 'string'},
                    'price': {'type': 'number'},
                    'description': {'type': 'string'},
                }
            }
        }
    }
)
print(result['extract'])  # 结构化 JSON

# 爬取整个网站
result = app.crawl_url(
    'https://example.com',
    params={
        'limit': 100,
        'scrapeOptions': {
            'formats': ['markdown'],
        }
    }
)
特性说明
优点无需编码、自动处理 JS 渲染、自动处理反爬、结构化输出
缺点付费(500 页/月免费,之后按页计费)、数据经过第三方、不可定制
适用场景快速验证、非敏感数据、小规模采集

5. browser-use:LLM Agent 操控浏览器

browser-use 是最前沿的 AI 爬虫方案——LLM Agent 自主操控浏览器,像人类一样浏览、点击、填写表单、提取数据:

图表渲染中…

安装与基础用法

bash
pip install browser-use
playwright install chromium
python
from browser_use import Agent
from langchain_openai import ChatOpenAI

# 创建 Agent
agent = Agent(
    task="打开京东,搜索'iPhone 15 Pro',获取前3个商品的价格和名称",
    llm=ChatOpenAI(model="gpt-4o"),
)

# 执行任务
result = await agent.run()
print(result)

自定义 Action

python
from browser_use import Agent, Controller
from pydantic import BaseModel

class ProductInfo(BaseModel):
    name: str
    price: float
    url: str

controller = Controller(output_model=ProductInfo)

# 定义自定义操作
@controller.action("将商品信息保存到数据库")
def save_product(product: ProductInfo):
    # 这里可以对接 MongoDB / MySQL
    db.products.insert_one(product.model_dump())
    return f"已保存: {product.name}"

agent = Agent(
    task="爬取某电商网站的手机商品信息",
    llm=ChatOpenAI(model="gpt-4o"),
    controller=controller,
)

result = await agent.run()
browser-use 的局限
  • 成本极高:每步操作都调用 LLM API,一个任务可能消耗 10-50 次 API 调用
  • 速度慢:Agent 需要观察、思考、操作,每个步骤 3-10 秒
  • 不稳定:LLM 可能点错按钮、输入错误内容、陷入循环
  • 适用场景有限:仅适合传统爬虫完全无法处理的极复杂交互场景

6. 自研 AI Agent 爬虫

对于复杂场景,可以使用 Claude/GPT + Playwright 自研 Agent 爬虫。核心思路:LLM 负责决策,Playwright 负责执行

python
import json
from anthropic import Anthropic
from playwright.sync_api import sync_playwright

class AICrawler:
    """AI Agent 爬虫 —— LLM 决策 + Playwright 执行"""

    def __init__(self):
        self.client = Anthropic()
        self.pw = sync_playwright().start()
        self.browser = self.pw.chromium.launch(headless=True)
        self.context = self.browser.new_context(
            user_agent='Mozilla/5.0 ...',
        )
        self.page = self.context.new_page()

    def crawl(self, url: str, goal: str) -> dict:
        """
        爬取目标 URL,根据 goal 提取数据。

        Args:
            url: 目标 URL
            goal: 提取目标描述(自然语言)
        """
        # 步骤1:访问页面
        self.page.goto(url, wait_until='networkidle')

        # 步骤2:获取页面内容
        html = self.page.content()

        # 步骤3:清理 HTML(只保留文本,减少 token 消耗)
        text_content = self.page.evaluate("""
            () => {
                // 去除 script、style 标签
                document.querySelectorAll('script, style, nav, footer, header').forEach(e => e.remove());
                return document.body.innerText;
            }
        """)

        # 步骤4:LLM 提取数据
        response = self.client.messages.create(
            model='claude-sonnet-4-20250514',
            max_tokens=2048,
            messages=[{
                'role': 'user',
                'content': f"""从以下网页内容中提取数据。

目标:{goal}

页面内容(截取前 6000 字符):
{text_content[:6000]}

请返回 JSON 格式的数据,不要包含其他文字。"""
            }],
        )

        result_text = response.content[0].text
        if '```json' in result_text:
            result_text = result_text.split('```json')[1].split('```')[0].strip()

        return json.loads(result_text)

    def close(self):
        self.context.close()
        self.browser.close()
        self.pw.stop()


# 使用
crawler = AICrawler()
data = crawler.crawl(
    url='https://example.com/products',
    goal='提取所有商品的名称、价格、评分和库存状态',
)
print(json.dumps(data, ensure_ascii=False, indent=2))
crawler.close()

AI 爬虫选型决策

图表渲染中…
场景推荐方案理由
简单页面、字段固定传统爬虫(requests + XPath)成本为零,准确率 100%
多网站通用提取AI 辅助提取零选择器,抗改版
快速原型、MVPScrapeGraphAI一行代码完成爬取+提取
RAG 数据准备Crawl4AIMarkdown 输出、内容清洗
无编码能力FireCrawlSaaS 开箱即用
极复杂交互browser-useAgent 自主决策和操作
成本敏感AI 辅助 + 本地模型Ollama 免费,准确率略低

7. AI 反反爬虫:智能对抗升级

传统反反爬虫依赖固定规则(UA 轮换、代理池),而 AI 反反爬虫利用大模型和计算机视觉实现自适应对抗——系统能识别验证码类型、模拟真实用户行为、动态调整策略。

7.1 AI 反反爬虫架构

图表渲染中…

7.2 智能验证码破解

图像验证码:多模态 LLM 直接识别

python
import base64
from anthropic import Anthropic
from playwright.sync_api import sync_playwright

def solve_image_captcha(page, captcha_selector: str) -> str:
    """使用 Claude Vision 识别图像验证码"""

    # 步骤1:截取验证码图片
    captcha_element = page.locator(captcha_selector)
    screenshot_bytes = captcha_element.screenshot()

    # 步骤2:编码为 base64
    img_b64 = base64.b64encode(screenshot_bytes).decode('utf-8')

    # 步骤3:调用多模态 LLM 识别
    client = Anthropic()
    response = client.messages.create(
        model='claude-sonnet-4-20250514',
        max_tokens=100,
        messages=[{
            'role': 'user',
            'content': [
                {
                    'type': 'image',
                    'source': {
                        'type': 'base64',
                        'media_type': 'image/png',
                        'data': img_b64,
                    }
                },
                {
                    'type': 'text',
                    'text': '这是一个验证码图片,请识别其中的字符,只返回字符本身,不要其他文字。'
                }
            ]
        }]
    )

    captcha_text = response.content[0].text.strip()

    # 步骤4:填入验证码
    page.fill('input[name="captcha"]', captcha_text)
    page.click('button[type="submit"]')

    return captcha_text

# 使用
with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://example.com/login')
    code = solve_image_captcha(page, '#captcha-img')
    print(f"识别结果: {code}")

滑块验证码:CV 模型 + 模拟拖拽

python
import random
import math
from playwright.sync_api import sync_playwright

def generate_human_like_drag(distance: int) -> list[dict]:
    """
    生成仿人类的鼠标拖拽轨迹。

    使用贝塞尔曲线 + 随机抖动,避免被行为检测识别为机器人。

    Args:
        distance: 需要拖拽的像素距离

    Returns:
        鼠标移动步骤列表 [{x, y, delay}, ...]
    """
    steps = []
    # 人类拖拽特征:起步慢 → 中间快 → 末尾减速
    total_steps = random.randint(30, 50)

    for i in range(total_steps):
        # 使用缓动函数模拟加速-减速
        progress = i / total_steps
        # ease-out cubic:快速起步,缓慢结束
        eased = 1 - (1 - progress) ** 3

        x = distance * eased
        # 添加纵向随机抖动(人类拖拽不是完全水平的)
        y = random.gauss(0, 1.5)
        # 每步间隔 8-15ms(人类反应速度)
        delay = random.uniform(8, 15)

        steps.append({'x': x, 'y': y, 'delay': delay})

    # 末尾微调(人类通常不会精确到位,需要修正)
    steps.append({'x': distance, 'y': 0, 'delay': random.uniform(50, 100)})

    return steps


def solve_slider_captcha(page, slider_selector: str, distance: int) -> None:
    """使用仿人类轨迹拖拽滑块验证码"""

    slider = page.locator(slider_selector)
    box = slider.bounding_box()

    if not box:
        raise ValueError("滑块元素未找到")

    # 移动到滑块中心
    start_x = box['x'] + box['width'] / 2
    start_y = box['y'] + box['height'] / 2

    page.mouse.move(start_x, start_y)
    page.mouse.down()

    # 按照仿人类轨迹拖拽
    trajectory = generate_human_like_drag(distance)
    for step in trajectory:
        page.mouse.move(
            start_x + step['x'],
            start_y + step['y'],
        )
        page.wait_for_timeout(int(step['delay']))

    page.mouse.up()

点选验证码:目标检测 + 坐标映射

python
import base64
import json
from anthropic import Anthropic
from playwright.sync_api import sync_playwright

def solve_click_captcha(page, captcha_img_selector: str, prompt: str) -> list[dict]:
    """
    使用 LLM 视觉能力解决点选验证码。

    Args:
        page: Playwright 页面对象
        captcha_img_selector: 验证码图片选择器
        prompt: 点击提示(如"请依次点击:猫、狗、鸟")

    Returns:
        点击坐标列表 [{x, y}, ...]
    """
    # 截取验证码图片
    element = page.locator(captcha_img_selector)
    img_bytes = element.screenshot()
    img_b64 = base64.b64encode(img_bytes).decode('utf-8')

    # 获取图片在页面中的位置
    box = element.bounding_box()

    # LLM 识别并返回坐标
    client = Anthropic()
    response = client.messages.create(
        model='claude-sonnet-4-20250514',
        max_tokens=1024,
        messages=[{
            'role': 'user',
            'content': [
                {
                    'type': 'image',
                    'source': {
                        'type': 'base64',
                        'media_type': 'image/png',
                        'data': img_b64,
                    }
                },
                {
                    'type': 'text',
                    'text': f"""这张图片的验证码提示是:{prompt}

请找出需要点击的目标,返回它们的坐标(相对于图片左上角的像素偏移)。

返回 JSON 格式:
{{"clicks": [{{"x": 100, "y": 80, "label": "猫"}}, ...]}}

只返回 JSON,不要其他文字。"""
                }
            ]
        }]
    )

    result_text = response.content[0].text.strip()
    if '```json' in result_text:
        result_text = result_text.split('```json')[1].split('```')[0].strip()

    data = json.loads(result_text)

    # 将相对坐标转为页面绝对坐标并点击
    clicks = []
    for click in data['clicks']:
        abs_x = box['x'] + click['x']
        abs_y = box['y'] + click['y']
        clicks.append({'x': abs_x, 'y': abs_y, 'label': click['label']})

        # 模拟人类点击(添加随机延迟)
        page.mouse.click(abs_x, abs_y, delay=random.randint(50, 150))
        page.wait_for_timeout(random.randint(300, 600))

    return clicks

import random

7.3 AI 模拟人类行为

浏览器指纹伪装

python
from playwright.sync_api import sync_playwright

def create_stealth_browser(pw):
    """创建带反检测能力的浏览器实例"""

    browser = pw.chromium.launch(
        headless=True,
        args=[
            '--disable-blink-features=AutomationControlled',  # 隐藏自动化标志
            '--no-sandbox',
        ]
    )

    context = browser.new_context(
        user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
                   'AppleWebKit/537.36 (KHTML, like Gecko) '
                   'Chrome/125.0.0.0 Safari/537.36',
        viewport={'width': 1920, 'height': 1080},
        locale='zh-CN',
        timezone_id='Asia/Shanghai',
    )

    # 注入反检测脚本
    context.add_init_script("""
        // 1. 隐藏 webdriver 标志
        Object.defineProperty(navigator, 'webdriver', { get: () => undefined });

        // 2. 伪装 Chrome 特征
        window.chrome = { runtime: {}, csi: function(){}, loadTimes: function(){} };

        // 3. 伪装 permissions API
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) =>
            parameters.name === 'notifications'
                ? Promise.resolve({ state: Notification.permission })
                : originalQuery(parameters);

        // 4. 随机化 Canvas 指纹
        const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
        HTMLCanvasElement.prototype.toDataURL = function(type) {
            // 在像素数据中添加微小的随机噪声
            const context = this.getContext('2d');
            if (context) {
                const imageData = context.getImageData(0, 0, 1, 1);
                imageData.data[3] = imageData.data[3] ^ (Math.random() * 2 | 0); // 最低位随机
                context.putImageData(imageData, 0, 0);
            }
            return originalToDataURL.apply(this, arguments);
        };

        // 5. 伪装 WebGL 渲染器信息
        const getParameter = WebGLRenderingContext.prototype.getParameter;
        WebGLRenderingContext.prototype.getParameter = function(parameter) {
            if (parameter === 37445) return 'Intel Inc.';         // UNMASKED_VENDOR
            if (parameter === 37446) return 'Intel Iris OpenGL';  // UNMASKED_RENDERER
            return getParameter.call(this, parameter);
        };

        // 6. 随机化 AudioContext 指纹
        const audioCtx = window.AudioContext || window.webkitAudioContext;
        if (audioCtx) {
            const originalGetFloatFreqData = AnalyserNode.prototype.getFloatFrequencyData;
            AnalyserNode.prototype.getFloatFrequencyData = function(array) {
                originalGetFloatFreqData.call(this, array);
                // 添加微量随机噪声
                for (let i = 0; i < array.length; i++) {
                    array[i] += (Math.random() - 0.5) * 0.001;
                }
            };
        }
    """)

    return browser, context

# 使用
with sync_playwright() as pw:
    browser, context = create_stealth_browser(pw)
    page = context.new_page()
    page.goto('https://example.com')
    # ... 执行爬取操作

打字节奏模拟

python
import random
import time

def human_type(page, selector: str, text: str) -> None:
    """模拟人类打字节奏:随机延迟 + 偶尔退格修正"""

    element = page.locator(selector)
    element.click()

    for i, char in enumerate(text):
        # 基础打字延迟:50-150ms(正常人打字速度)
        delay = random.gauss(100, 30)
        delay = max(40, min(250, delay))  # 钳制到合理范围

        # 模拟偶尔的"打错字 → 退格修正"
        if random.random() < 0.05 and len(text) > 3:  # 5% 概率打错
            # 打一个错误的字符
            wrong_chars = 'abcdefghijklmnopqrstuvwxyz'
            wrong_char = random.choice(wrong_chars)
            page.keyboard.type(wrong_char, delay=0)
            time.sleep(random.uniform(0.1, 0.3))  # 发现错误的延迟
            page.keyboard.press('Backspace')
            time.sleep(random.uniform(0.05, 0.15))

        page.keyboard.type(char, delay=0)
        time.sleep(delay / 1000)

        # 偶尔在标点符号后停顿更长
        if char in ',。!?;:':
            time.sleep(random.uniform(0.2, 0.5))

7.4 AI 驱动的请求调度

python
import time
import random
import logging
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)

@dataclass
class AdaptiveScheduler:
    """
    自适应请求调度器。

    根据服务器响应时间动态调整请求间隔,
    遇到反爬时自动增加延迟,恢复正常后逐步缩短。
    """

    base_delay: float = 1.0          # 基础延迟(秒)
    current_delay: float = 1.0       # 当前延迟
    min_delay: float = 0.5           # 最小延迟
    max_delay: float = 30.0          # 最大延迟
    backoff_factor: float = 1.5      # 退避因子
    recovery_factor: float = 0.9     # 恢复因子
    consecutive_success: int = 0     # 连续成功次数
    consecutive_failure: int = 0     # 连续失败次数

    def on_success(self, response_time: float) -> None:
        """请求成功回调:逐步缩短延迟"""
        self.consecutive_success += 1
        self.consecutive_failure = 0

        # 连续 5 次成功,缩短延迟
        if self.consecutive_success >= 5:
            self.current_delay = max(
                self.min_delay,
                self.current_delay * self.recovery_factor
            )
            self.consecutive_success = 0
            logger.info(f"恢复正常,延迟缩短至 {self.current_delay:.1f}s")

    def on_failure(self, status_code: int) -> float:
        """请求失败回调:指数退避"""
        self.consecutive_failure += 1
        self.consecutive_success = 0

        if status_code == 429:
            # 429 Too Many Requests:大幅增加延迟
            self.current_delay = min(
                self.max_delay,
                self.current_delay * self.backoff_factor * 2
            )
        elif status_code in (403, 503):
            # 403 Forbidden / 503 Service Unavailable
            self.current_delay = min(
                self.max_delay,
                self.current_delay * self.backoff_factor
            )

        logger.warning(f"请求失败({status_code}),延迟增加至 {self.current_delay:.1f}s")
        return self.current_delay

    def wait(self) -> None:
        """等待当前延迟时间(添加随机抖动)"""
        actual_delay = self.current_delay + random.uniform(-0.1, 0.3)
        time.sleep(max(0, actual_delay))

8. AI 智能解析:超越选择器

传统爬虫依赖 CSS 选择器/XPath 定位数据,一旦页面改版就全部失效。AI 智能解析利用多模态大模型,从视觉语义两个维度理解页面,实现"像人类一样看网页提取数据"。

8.1 多模态爬虫架构

图表渲染中…

8.2 视觉模型提取表格/图表

截图 → LLM 提取表格数据

python
import base64
import json
from anthropic import Anthropic
from playwright.sync_api import sync_playwright

def extract_table_from_screenshot(
    url: str,
    table_selector: str = 'table',
) -> list[dict]:
    """
    使用多模态 LLM 从页面截图中提取表格数据。

    适用于:
    - 表格渲染在 Canvas/SVG 中,无法从 HTML 提取
    - 表格结构复杂(合并单元格、嵌套表头)
    - 表格数据来自动态 JS 渲染
    """
    with sync_playwright() as pw:
        browser = pw.chromium.launch(headless=True)
        page = browser.new_page(viewport={'width': 1920, 'height': 1080})
        page.goto(url, wait_until='networkidle')

        # 截取表格区域
        table = page.locator(table_selector)
        screenshot_bytes = table.screenshot()
        browser.close()

    # 发送给多模态 LLM 识别
    img_b64 = base64.b64encode(screenshot_bytes).decode('utf-8')

    client = Anthropic()
    response = client.messages.create(
        model='claude-sonnet-4-20250514',
        max_tokens=4096,
        messages=[{
            'role': 'user',
            'content': [
                {
                    'type': 'image',
                    'source': {
                        'type': 'base64',
                        'media_type': 'image/png',
                        'data': img_b64,
                    }
                },
                {
                    'type': 'text',
                    'text': """请识别这张截图中的表格数据,转换为 JSON 格式。

要求:
1. 表头作为 JSON 的键名
2. 每行数据为一个对象
3. 数字类型保持数值(不要转为字符串)
4. 合并单元格的值填充到所有对应的行

返回格式:
{"headers": ["列1", "列2", ...], "rows": [{...}, ...]}

只返回 JSON,不要其他文字。"""
                }
            ]
        }]
    )

    result_text = response.content[0].text.strip()
    if '```json' in result_text:
        result_text = result_text.split('```json')[1].split('```')[0].strip()

    return json.loads(result_text)

# 使用
data = extract_table_from_screenshot('https://example.com/data-table')
print(json.dumps(data, ensure_ascii=False, indent=2))

图表数据提取

python
import base64
import json
from anthropic import Anthropic

def extract_chart_data(image_path: str, chart_type: str = 'bar') -> dict:
    """
    使用多模态 LLM 从图表截图中估算数据。

    注意:LLM 提取图表数据是**估算**,精度有限。
    对于精确数据,应优先寻找图表的原始数据源(API 接口)。
    """
    with open(image_path, 'rb') as f:
        img_b64 = base64.b64encode(f.read()).decode('utf-8')

    client = Anthropic()
    response = client.messages.create(
        model='claude-sonnet-4-20250514',
        max_tokens=2048,
        messages=[{
            'role': 'user',
            'content': [
                {
                    'type': 'image',
                    'source': {
                        'type': 'base64',
                        'media_type': 'image/png',
                        'data': img_b64,
                    }
                },
                {
                    'type': 'text',
                    'text': f"""这是一个{chart_type}图(柱状图),请估算每个柱子对应的数据值。

返回 JSON 格式:
{{"title": "图表标题", "x_axis_label": "...", "y_axis_label": "...", "data": [{{"label": "...", "value": 123}}, ...]}}

只返回 JSON。"""
                }
            ]
        }]
    )

    result_text = response.content[0].text.strip()
    if '```json' in result_text:
        result_text = result_text.split('```json')[1].split('```')[0].strip()

    return json.loads(result_text)

8.3 PDF 智能解析

python
import json
from anthropic import Anthropic

def extract_pdf_tables(pdf_path: str, api_key: str = None) -> list[dict]:
    """
    使用 PyMuPDF + LLM 联合方案解析 PDF 中的表格。

    流程:
    1. PyMuPDF 提取 PDF 页面文本
    2. LLM 理解上下文并结构化提取
    """
    import fitz  # PyMuPDF

    doc = fitz.open(pdf_path)
    results = []

    for page_num in range(len(doc)):
        page = doc[page_num]

        # 提取文本(保留格式信息)
        text = page.get_text("text")

        if not text.strip():
            continue

        # 如果文本过长,只发送前 8000 字符
        truncated = text[:8000]

        # 调用 LLM 结构化提取
        client = Anthropic() if api_key is None else Anthropic(api_key=api_key)
        response = client.messages.create(
            model='claude-sonnet-4-20250514',
            max_tokens=4096,
            messages=[{
                'role': 'user',
                'content': f"""以下是从 PDF 第 {page_num + 1} 页提取的文本内容。

请识别其中的表格数据,转换为结构化 JSON 格式。
如果没有表格,返回 {{"has_table": false}}。

PDF 内容:
{truncated}

返回格式:
{{"has_table": true, "tables": [{{"headers": [...], "rows": [...]}}]}}"""
            }]
        )

        result_text = response.content[0].text.strip()
        if '```json' in result_text:
            result_text = result_text.split('```json')[1].split('```')[0].strip()

        try:
            page_data = json.loads(result_text)
            page_data['page'] = page_num + 1
            results.append(page_data)
        except json.JSONDecodeError:
            logger.warning(f"第 {page_num + 1} 页 LLM 输出无法解析为 JSON")

    doc.close()
    return results

import logging
logger = logging.getLogger(__name__)
多模态爬虫的适用场景
场景推荐通道原因
普通 HTML 表格文本通道精确、成本低
Canvas/SVG 渲染的图表视觉通道HTML 中无文本数据
复杂排版的 PDFPDF 通道保留页面布局信息
扫描件/图片 PDF视觉通道文本通道无法提取
关键业务数据多通道交叉验证提高准确率

9. 更多 AI 爬虫框架

9.1 Stagehand:Playwright 官方 AI 扩展

Stagehand 是由 Browserbase 开发的 AI 浏览器自动化框架,建立在 Playwright 之上,提供三个核心 AI 原语:actextractobserve

bash
npm install stagehand
# 或 Python 版本
pip install stagehand
python
from stagehand import Stagehand, StagehandConfig

async def stagehand_example():
    config = StagehandConfig(
        env="LOCAL",  # 本地浏览器
        api_key="your-api-key",
        model_name="gpt-4o",
    )

    stagehand = Stagehand(config)
    await stagehand.init()

    page = stagehand.page
    await page.goto("https://example.com/login")

    # act:AI 执行操作(用自然语言描述)
    await page.act("在用户名输入框中输入 admin")
    await page.act("在密码输入框中输入 password123")
    await page.act("点击登录按钮")

    # extract:AI 提取结构化数据
    data = await page.extract({
        "prompt": "提取页面上所有商品信息",
        "schema": {
            "type": "object",
            "properties": {
                "products": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "price": {"type": "number"},
                        }
                    }
                }
            }
        }
    })

    # observe:AI 观察页面,返回可执行的操作列表
    actions = await page.observe("如何找到商品详情页?")

    await stagehand.close()
    return data

与 browser-use 对比

维度Stagehandbrowser-use
底层框架PlaywrightPlaywright
核心理念AI 原语(act/extract/observe)完全自主 Agent
可控性高(每次操作有明确意图)低(Agent 自主决策)
成本中等(按操作调用 LLM)高(Agent 每步都调用 LLM)
稳定性较高(操作明确)较低(可能陷入循环)
适用场景半自动化爬虫、表单填写完全自主探索型任务

9.2 Skyvern:开源 AI Agent 浏览器自动化

Skyvern 是一个开源的 AI Agent 浏览器自动化平台,专注于企业级工作流自动化。

python
# Skyvern 通过 API 调用
import requests

response = requests.post(
    "http://localhost:8000/api/v1/tasks",
    json={
        "url": "https://example.com/form",
        "navigation_goal": "填写注册表单并提交",
        "data_extraction_goal": "提取注册成功后的确认信息",
        "extracted_information_schema": {
            "type": "object",
            "properties": {
                "confirmation_id": {"type": "string"},
                "status": {"type": "string"},
            }
        }
    }
)

task_id = response.json()["id"]
# 轮询任务状态...

9.3 Camoufox:AI 反检测浏览器

Camoufox 是一个基于 Firefox 的反检测浏览器,专为爬虫设计。与 Playwright stealth 插件不同,Camoufox 从浏览器内核层面进行指纹伪装。

python
from camoufox.sync_api import Camoufox

def camoufox_example():
    with Camoufox(headless=True) as browser:
        page = browser.new_page()
        page.goto("https://browserleaks.com/canvas")

        # Camoufox 自动处理:
        # - Canvas 指纹随机化
        # - WebGL 渲染器伪装
        # - AudioContext 指纹伪装
        # - Navigator 属性伪装
        # - 屏幕分辨率和色深随机化

        content = page.content()
        print("指纹检测结果:", content[:200])

# 对比
特性Playwright + stealthCamoufox
底层浏览器ChromiumFirefox
伪装层级JS 注入(可被检测)浏览器内核(更难检测)
Canvas 指纹通过 JS 添加噪声内核级随机化
WebDriver 检测删除 navigator.webdriver从编译层面移除
安装复杂度低(npm/pip)中(需下载专用 Firefox)
社区活跃度

9.4 其他新兴工具速览

工具类型特点适用场景
Jina ReaderSaaS APIURL → Markdown,免费,无需 API Key快速将网页转为 AI 可读文本
Apify AI ActorSaaS 平台预构建 AI 爬虫模板,一键部署非技术用户快速上手
SpiderSaaS API高性能爬取 + AI 提取,Rust 驱动大规模爬取
Crawl4AI开源库Markdown 转换 + LLM 提取,本地运行RAG 数据准备
NoWorker AISaaS可视化 AI 爬虫工作流编辑器无代码 AI 爬虫

10. AI 爬虫生产化部署

10.1 生产架构设计

图表渲染中…

10.2 混合策略:传统 + AI 协同

python
import json
import logging
from dataclasses import dataclass
from typing import Any

logger = logging.getLogger(__name__)

@dataclass
class HybridCrawler:
    """
    混合爬虫:自动在传统爬虫和 AI 爬虫之间选择。

    策略:
    1. 优先尝试传统选择器提取(成本为零)
    2. 选择器提取失败时,回退到 AI 辅助提取
    3. AI 提取结果与传统结果交叉验证
    """

    selectors: dict[str, str]   # CSS 选择器映射
    llm_client: Any = None      # LLM 客户端(延迟初始化)
    confidence_threshold: float = 0.8

    def extract(self, html: str, url: str) -> dict:
        """混合提取策略"""

        # 阶段1:尝试传统选择器提取(成本 ≈ 0)
        traditional_result = self._extract_with_selectors(html)
        if traditional_result and self._validate(traditional_result):
            logger.info(f"[传统提取成功] {url}")
            return {**traditional_result, '_method': 'selector', '_confidence': 1.0}

        # 阶段2:回退到 AI 辅助提取(有 API 成本)
        logger.info(f"[选择器失败,回退 AI] {url}")
        ai_result = self._extract_with_llm(html, url)
        if ai_result:
            return {**ai_result, '_method': 'llm', '_confidence': ai_result.get('_confidence', 0.85)}

        # 阶段3:两种方法都失败
        logger.warning(f"[提取失败] {url}")
        return {'_method': 'failed', '_confidence': 0.0}

    def _extract_with_selectors(self, html: str) -> dict | None:
        """使用 CSS 选择器提取"""
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, 'lxml')

        result = {}
        for field, selector in self.selectors.items():
            element = soup.select_one(selector)
            if element:
                result[field] = element.get_text(strip=True)
            else:
                return None  # 任一字段提取失败,回退到 AI

        return result if result else None

    def _extract_with_llm(self, html: str, url: str) -> dict | None:
        """使用 LLM 辅助提取"""
        if not self.llm_client:
            return None

        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, 'lxml')
        text = soup.get_text(separator='\n', strip=True)[:6000]

        # ... 调用 LLM API 提取 ...
        # 详见第1节"AI 辅助提取"
        return None  # 简化示例

    def _validate(self, result: dict) -> bool:
        """验证提取结果是否合理"""
        if not result:
            return False
        # 检查关键字段是否为空
        for key, value in result.items():
            if not value or (isinstance(value, str) and len(value.strip()) == 0):
                return False
        return True

10.3 成本控制策略

策略实现方式效果
Token 预算每任务设置 Token 上限,超限停止防止单任务成本失控
混合策略简单页面用传统爬虫,复杂页面用 AI减少 80%+ LLM 调用
本地模型Ollama + Llama3/Mistral 替代云端 APIAPI 成本降为零
结果缓存Redis 缓存 URL → 提取结果,TTL 1 小时相同 URL 不重复调用
HTML 预清理BeautifulSoup 删除 <script>/<style>/<nav>减少 50-70% Token
批量合并多个小页面合并为一次 LLM 调用减少请求次数
python
# 本地模型部署方案
# 使用 Ollama 部署本地 LLM(零 API 成本)

# 1. 安装 Ollama
# curl -fsSL https://ollama.com/install.sh | sh

# 2. 拉取模型
# ollama pull llama3        # 8B 参数,~4.7GB
# ollama pull mistral       # 7B 参数,~4.1GB
# ollama pull qwen2:7b      # 中文优化,~4.4GB

# 3. Python 调用
import requests

def extract_with_local_llm(text: str, goal: str) -> dict:
    """使用 Ollama 本地模型提取数据(零 API 成本)"""

    response = requests.post(
        'http://localhost:11434/api/generate',
        json={
            'model': 'qwen2:7b',  # 中文场景推荐
            'prompt': f"""从以下网页内容中提取数据。

目标:{goal}

内容:
{text[:4000]}

请返回 JSON 格式。""",
            'stream': False,
            'options': {
                'temperature': 0,  # 确定性输出
                'num_predict': 1024,
            }
        },
        timeout=60,
    )

    result = response.json()
    return result['response']

10.4 质量保障

python
import json
from dataclasses import dataclass

@dataclass
class ExtractionValidator:
    """AI 爬虫提取结果校验器"""

    schema: dict       # JSON Schema 定义
    required_fields: list[str]

    def validate(self, result: dict) -> tuple[bool, float, list[str]]:
        """
        校验提取结果。

        Returns:
            (是否通过, 置信度 0-1, 问题列表)
        """
        issues = []

        # 1. 检查必需字段
        for field in self.required_fields:
            if field not in result or result[field] is None:
                issues.append(f"缺少必需字段: {field}")

        # 2. 检查类型一致性
        for field, expected_type in self.schema.items():
            if field in result and result[field] is not None:
                actual_type = type(result[field]).__name__
                if actual_type != expected_type:
                    issues.append(f"字段 {field} 类型错误: 期望 {expected_type}, 实际 {actual_type}")

        # 3. 检查值合理性(基于业务规则)
        if 'price' in result and isinstance(result['price'], (int, float)):
            if result['price'] < 0 or result['price'] > 1_000_000:
                issues.append(f"价格不合理: {result['price']}")

        # 4. 计算置信度
        total_checks = len(self.required_fields) + len(self.schema)
        passed_checks = total_checks - len(issues)
        confidence = passed_checks / total_checks if total_checks > 0 else 0.0

        passed = len(issues) == 0 and confidence >= 0.8

        return passed, confidence, issues

    def cross_validate(
        self,
        text_result: dict,
        vision_result: dict,
    ) -> tuple[dict, float]:
        """
        多通道交叉验证:文本通道 vs 视觉通道。

        两个通道提取的结果互相验证,一致的字段置信度更高。
        """
        merged = {}
        total_confidence = 0.0
        field_count = 0

        all_keys = set(text_result.keys()) | set(vision_result.keys())

        for key in all_keys:
            text_val = text_result.get(key)
            vision_val = vision_result.get(key)

            if text_val == vision_val and text_val is not None:
                # 两通道一致:高置信度
                merged[key] = text_val
                total_confidence += 1.0
            elif text_val is not None and vision_val is None:
                # 仅文本通道有值
                merged[key] = text_val
                total_confidence += 0.7
            elif vision_val is not None and text_val is None:
                # 仅视觉通道有值
                merged[key] = vision_val
                total_confidence += 0.7
            else:
                # 两通道不一致:取文本通道(通常更精确)
                merged[key] = text_val
                total_confidence += 0.5

            field_count += 1

        avg_confidence = total_confidence / field_count if field_count else 0
        return merged, avg_confidence

10.5 监控与告警

监控指标告警阈值说明
Token 消耗速率> 100K tokens/min防止成本失控
提取成功率< 80%检查页面是否改版
平均置信度< 0.7LLM 提取质量下降
幻觉率> 5%LLM 编造数据增多
Agent 步骤数> 50 步/任务Agent 陷入循环
传统/AI 比率AI > 50%选择器大面积失效
python
import time
from collections import defaultdict
from dataclasses import dataclass, field

@dataclass
class CrawlMonitor:
    """AI 爬虫监控器"""

    start_time: float = field(default_factory=time.time)
    total_tasks: int = 0
    success_tasks: int = 0
    total_tokens: int = 0
    llm_calls: int = 0
    confidence_scores: list[float] = field(default_factory=list)
    method_counts: dict[str, int] = field(default_factory=lambda: defaultdict(int))

    def record_task(
        self,
        method: str,       # 'selector' / 'llm' / 'agent'
        success: bool,
        confidence: float,
        tokens_used: int = 0,
    ) -> None:
        """记录一次任务结果"""
        self.total_tasks += 1
        if success:
            self.success_tasks += 1
        self.method_counts[method] += 1
        self.confidence_scores.append(confidence)
        self.total_tokens += tokens_used
        if tokens_used > 0:
            self.llm_calls += 1

        # 检查告警条件
        if self.total_tasks >= 10:
            success_rate = self.success_tasks / self.total_tasks
            if success_rate < 0.8:
                self._alert(f"提取成功率过低: {success_rate:.1%}")

            avg_confidence = sum(self.confidence_scores) / len(self.confidence_scores)
            if avg_confidence < 0.7:
                self._alert(f"平均置信度过低: {avg_confidence:.2f}")

            ai_ratio = (self.method_counts.get('llm', 0) + self.method_counts.get('agent', 0)) / self.total_tasks
            if ai_ratio > 0.5:
                self._alert(f"AI 调用占比过高: {ai_ratio:.1%},选择器可能大面积失效")

    def report(self) -> dict:
        """生成监控报告"""
        elapsed = time.time() - self.start_time
        return {
            'elapsed_minutes': round(elapsed / 60, 1),
            'total_tasks': self.total_tasks,
            'success_rate': f"{self.success_tasks / self.total_tasks:.1%}" if self.total_tasks else 'N/A',
            'total_tokens': self.total_tokens,
            'llm_calls': self.llm_calls,
            'avg_confidence': f"{sum(self.confidence_scores) / len(self.confidence_scores):.2f}" if self.confidence_scores else 'N/A',
            'method_distribution': dict(self.method_counts),
        }

    def _alert(self, message: str) -> None:
        """发送告警(实际项目中对接钉钉/企业微信/Slack)"""
        import logging
        logging.getLogger(__name__).warning(f"[AI爬虫告警] {message}")

AI 爬虫成本优化

策略效果实现方式
HTML 预清理减少 50-70% tokenBeautifulSoup 删除 <script>/<style>/<nav>
本地模型替代API 成本降为零Ollama + Llama3/Mistral
批量处理减少请求次数合并多个页面到一次 LLM 调用
缓存 LLM 结果避免重复调用Redis 缓存 URL → 提取结果
混合策略降低 LLM 依赖简单字段用选择器,复杂字段用 LLM
Structured Output减少 prompt 长度使用 JSON Schema 约束输出格式

常见陷阱

陷阱现象原因解决方案
Token 成本爆炸几百个页面花费数十美元发送完整 HTML 给 LLM预清理 HTML,只发送关键文本区域
LLM 幻觉提取出不存在的数据LLM 倾向于"编造"答案使用 Structured Output 约束;交叉验证
提取不稳定同一页面两次提取结果不同LLM 输出具有随机性设置 temperature=0;提供 Schema 约束
网站改版后失效AI 提取结果突然为空页面结构大改,LLM 无法理解增加页面有效性检查;回退到传统选择器
Agent 陷入循环browser-use 反复点击同一按钮LLM 没有记忆已执行的操作设置最大步骤限制;添加操作历史记录
本地模型效果差Ollama 提取准确率仅 60%小模型理解能力不足使用 7B+ 参数模型;优化 prompt;微调

法律与伦理补充

AI 爬虫带来了新的法律和伦理问题:

AI 爬虫的额外风险
  1. 数据发送到第三方:使用 OpenAI/Anthropic API 时,页面内容会发送到云端——可能违反网站 ToS 或数据隐私法规
  2. 自动化程度更高:AI Agent 可以像人类一样操作,但也可能更难被检测——这并不意味着更合法
  3. 大规模数据采集:AI 爬虫降低了技术门槛,可能导致更多人进行大规模无节制爬取
  4. 个人数据风险:LLM 可能无意中提取并传输个人隐私数据(姓名、电话、地址)

最佳实践

  • 使用本地模型(Ollama)处理敏感数据,避免发送到第三方
  • AI 爬虫同样需要遵守 robots.txt 和频率限制
  • 提取个人数据前必须评估合规性
  • 永远不要用 AI 爬虫绕过付费墙或认证系统

术语表

术语英文定义
AI 爬虫AI Crawler使用大语言模型辅助或驱动的网络爬虫
LLMLarge Language Model大语言模型,如 GPT-4、Claude、Llama
多模态 LLMMultimodal LLM能同时处理文本和图像的大语言模型(如 GPT-4V、Claude Vision)
RAGRetrieval-Augmented Generation检索增强生成,LLM 结合外部知识库的技术
AgentAI Agent能够自主规划、执行和纠错的 AI 系统
Structured OutputStructured Output约束 LLM 输出为特定 JSON Schema 的技术
OllamaOllama本地运行 LLM 的开源工具
browser-usebrowser-useLLM Agent 操控浏览器的 Python 框架
ScrapeGraphAIScrapeGraphAI图结构 LLM 驱动的爬虫框架
Crawl4AICrawl4AI智能爬取+Markdown 转换的 Python 库
FireCrawlFireCrawl云端 AI 爬虫 SaaS 服务
StagehandStagehandPlaywright 上的 AI 原语扩展(act/extract/observe)
SkyvernSkyvern开源 AI Agent 浏览器自动化平台
CamoufoxCamoufox基于 Firefox 的反检测浏览器
TokenTokenLLM 处理文本的最小单位(约 4 字符)
幻觉HallucinationLLM 生成不真实或不存在内容的现象
PromptPrompt发送给 LLM 的指令文本
指纹伪装Fingerprint Spoofing修改浏览器指纹特征以避免被识别为自动化程序
交叉验证Cross Validation多种提取方法的结果互相验证以提高准确率
混合策略Hybrid Strategy传统爬虫与 AI 爬虫结合,按需自动切换
置信度Confidence ScoreAI 提取结果的可信程度评分(0-1)
贝塞尔曲线Bézier Curve用于生成平滑鼠标轨迹的数学曲线

延伸阅读

站内链接

外部链接

版本差异(爬虫技术栈 → 当前版本)

本文编写时当前稳定版
requests2.28/2.312.32.x
Scrapy1.x/2.02.11.x(API 稳定)
httpx0.240.28.x
Playwright1.3x1.6x(Python 版)
lxml/BeautifulSoup旧版保持稳定
Python3.8-3.123.14(推荐)

本文讲解的爬虫原理(HTTP、解析、反爬、存储)与核心 API 在最新版本中成立;注意 Python 3.9 及以下已 EOL,新项目使用 3.13/3.14。