lesson-09

⏱ 预计阅读 7 分钟 更新于 2026/5/14
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)

9.1 架构回顾:谁在代理谁?

在进入代码实战前,我们需要理清流量走向,这非常容易混淆。许多开发者会问:"我的 Node.js 爬虫代码需要配置代理吗?"

答案是:不需要!

在这个架构中,你的核心应用(Node.js 后端)与目标网站之间,隔着我们搭建的 Firecrawl 代理黑盒

graph LR
    Node[Node.js 业务后台
无代理, 真实IP] -->|HTTP POST| FCAPI[本地 Firecrawl:3002] subgraph 代理黑盒区域 FCAPI -->|传递抓取任务| Playwright[Playwright 容器] Playwright -->|通过环境变量 PROXY_SERVER| WARP[宿主机 WARP SOCKS5:40000] end WARP -->|CF 边缘 IP 出海| Target[目标反爬网站] Target -.-> WARP -.-> Playwright -.-> FCAPI -.->|Markdown 内容| Node

核心优势

  1. 业务解耦:你的后台 Node.js 代码完全不需要处理代理逻辑、维护代理池,甚至不需要知道 WARP 的存在。
  2. 安全隔离:如果代理挂了,只影响发往 Firecrawl 的请求,你的 Node.js 连接数据库、调用大模型 API(如 Gemini)依然走本地极速网络。

9.2 在业务代码中调用 Firecrawl

当我们将 Firecrawl 作为基础设施部署好后,在 Node.js 中调用它就变成了一次简单的 HTTP/REST 请求。

我们不建议使用复杂的 SDK,直接使用原生的 fetch 即可获得最灵活的控制权:

// crawler/src/utils/firecrawl-client.ts

export async function scrapeWithFirecrawl(url: string) {
  const FIRECRAWL_API_URL = "http://localhost:3002";
  
  console.log(`🛡️ 正在通过本地代理集群抓取: ${url}`);
  
  try {
    const response = await fetch(`${FIRECRAWL_API_URL}/v1/scrape`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 本地自建无需真实 API Key
        'Authorization': 'Bearer dummy-key' 
      },
      body: JSON.stringify({
        url: url,
        formats: ['markdown', 'html'],
        // 关键参数:给底层的 Playwright 留出渲染时间
        waitFor: 2000, 
        // 绕过常见的无头浏览器检测
        mobile: false, 
      }),
    });

    if (!response.ok) {
      throw new Error(`Firecrawl 响应错误: ${response.status}`);
    }

    const data = await response.json();
    
    if (data.success && data.data) {
      return {
        success: true,
        content: data.data.markdown, // 返回清洗后的纯正文
        html: data.data.html
      };
    }
    
    return { success: false, error: "未获取到数据" };
    
  } catch (error) {
    console.error(`❌ Firecrawl 抓取失败:`, error);
    return { success: false, error: String(error) };
  }
}

9.3 应对极端反爬:高级配置项

针对一些特殊的网站,我们可以通过修改发送给 Firecrawl 的 payload 来加强穿透力:

1. 处理强制弹窗与验证码 (Interact)

如果目标网站打开就是一个 "Accept Cookies" 遮罩层,阻碍了正文提取:

{
  "url": "https://example.com",
  "formats": ["markdown"],
  "actions": [
    { "type": "click", "selector": "#accept-cookies-btn" },
    { "type": "wait", "milliseconds": 1000 }
  ]
}

2. 只抓取特定区域

如果整个网页很庞大,我们只想要文章主体的 Markdown,以节省后续丢给大模型的 Token 成本:

{
  "url": "https://news.ycombinator.com",
  "formats": ["markdown"],
  "includeTags": ["article", "main", ".story-content"],
  "excludeTags": ["nav", "footer", ".ads-banner"]
}

9.4 课后问题

  1. 为什么在 Node.js 中调用大模型 API(如 Gemini)时,不应该让 Node.js 走 WARP 代理?
  2. 如果你的 Firecrawl 容器和 Node.js 应用部署在同一个 Docker Compose 网络中,FIRECRAWL_API_URL 应该写成什么?
  3. waitFor: 2000 这个参数在突破反爬时起到了什么关键作用?