先划边界

爬虫逆向最先要做的不是打开开发者工具,而是划清边界。哪些数据是公开可访问的?哪些内容需要登录、付费或授权?有没有 robots、平台规则或接口使用条款?这些问题如果不先想清楚,后面写得越顺,风险越大。

我更愿意把这类工具定位成个人学习、公开信息整理和自己授权内容的解析。不要做登录绕过、验证码绕过、批量压测,也不要把别人的私有数据当成练手素材。

从浏览器行为开始

真正的请求通常藏在页面加载、跳转、异步接口和内联状态里。先观察浏览器怎么打开页面,再看网络面板里的请求顺序、状态码、重定向、响应类型和关键字段。

很多时候,不需要一上来就模拟复杂环境。先把 URL 归一化,跟随短链跳转,拿到最终页面,再判断页面里有没有结构化 JSON、meta 信息或可直接抽取的文本。

Shark-Tools 里我后来会把「请求」和「解析」拆开。下面这段是整理后的安全骨架,保留了超时、域名白名单和可解释错误,去掉了真实站点、cookie 和私有参数:

from urllib.parse import urlparse, urljoin
import requests

ALLOWED_HOSTS = {"example.com", "www.example.com"}

def assert_public_url(url: str) -> str:
    parsed = urlparse(url.strip())
    if parsed.scheme not in {"http", "https"}:
        raise ValueError("只接受 http/https 链接")
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("该域名不在允许列表里")
    return url

def fetch_page(session: requests.Session, url: str) -> str:
    safe_url = assert_public_url(url)
    response = session.get(
        safe_url,
        headers={"User-Agent": "SharkTools-Lab/1.0"},
        timeout=15,
    )
    response.raise_for_status()
    response.encoding = response.apparent_encoding or "utf-8"
    return response.text

def resolve_asset(base_url: str, maybe_relative: str) -> str:
    # 页面里拿到的封面、m3u8、下一页链接经常是相对路径。
    return maybe_relative if maybe_relative.startswith("http") else urljoin(base_url, maybe_relative)

这个骨架的重点不是“模拟得多像浏览器”,而是先把信任边界写进代码。真实项目里不要把用户输入的任意 URL 直接交给请求库,否则 SSRF、内网探测和奇怪协议都会变成隐患。

抽取逻辑要可降级

爬虫最怕把所有希望压在一个选择器上。页面改一个 class,工具就全部失效。更稳的方式是准备多层解析:优先读结构化数据,其次读页面状态,再退回 meta 标签或正文抽取。

每一层失败都应该返回清楚的错误码和原因,而不是只抛一个模糊异常。这样 Agent 调用工具时,才能知道是链接无效、平台改版、网络失败,还是内容确实不可访问。

我会尽量让解析函数返回稳定结构,而不是直接把页面里的原始字段向上传。比如搜索类结果可以先落成一个数据类:

from dataclasses import dataclass

@dataclass
class VideoItem:
    title: str
    cover_url: str
    video_url: str
    author: str
    duration: str
    publish_time: str
    play_count: str
    description: str = ""
    source_id: str = ""

这样上层界面、Agent 工具或素材库都只依赖 VideoItem,底层平台字段怎么变,影响范围会小很多。

速率和缓存

稳定不等于高频请求。个人工具也应该有速率限制、超时、重试上限和缓存。尤其是热榜、网页摘要、视频元信息这类内容,不需要每次打开都重新请求。

如果工具要接进 Agent,就更需要防止模型反复调用同一个接口。后端应该能记录工具调用、限制频率,并把相同输入的结果复用起来。

Shark-Tools 里有些早期脚本把重试次数写得很激进,后来复盘我觉得应该收敛成「少量重试 + 明确失败」:

import time

def retry_get(session, url, *, attempts=3, delay=1.2):
    last_error = None
    for index in range(attempts):
        try:
            response = session.get(url, timeout=15)
            if response.status_code in {401, 403}:
                raise PermissionError("访问被拒绝,停止重试")
            response.raise_for_status()
            return response
        except Exception as exc:
            last_error = exc
            if index < attempts - 1:
                time.sleep(delay * (index + 1))
    raise RuntimeError(f"请求失败:{last_error}")

失败并不可怕,真正可怕的是无限重试、无提示重试和把访问被拒绝误判成网络抖动。

最后是可维护

逆向代码最容易变成一团临时补丁,所以我会尽量把「请求」「解析」「规范化输出」「错误处理」拆开。外部平台会变,代码也会坏;能快速定位坏在哪里,比一次写得很巧更重要。

一套好的爬虫逆向方法论,不是追求绕得多深,而是追求边界清楚、失败可见、维护成本低。