Use Cases (更新: 2026/6/2)

用 Claude Code 安全实现 Web Scraping:Fetch、Playwright 与审计日志

用 Claude Code 安全实现 Web Scraping:robots.txt、Fetch、Playwright、CSV 与审计日志。

用 Claude Code 安全实现 Web Scraping:Fetch、Playwright 与审计日志

先定义边界,再让 Claude Code 写代码

Web scraping 指的是用程序读取网页上的信息。它可以用于监控自己的网站、整理公开文档、做少量市场调研,也可以用于内容质量检查。但公开网页不等于可以任意收集。Claude Code 能很快生成代码,所以更要在生成之前写清楚边界:只处理公开数据,遵守网站条款和 robots.txt,限制请求频率,除非有合法依据否则不收集个人信息,并保留审计记录。

适合初学者的顺序是:先找官方 API、RSS、sitemap、CSV 导出或其他结构化入口。它们通常比 HTML 更稳定,使用规则也更清楚。只有在任务合理、数据量很小、没有更好来源时,才考虑读取 HTML。

本文把 Claude Code 当作实现助手,而不是绕过限制的工具。这里不讨论登录绕过、CAPTCHA 绕过、反爬保护绕过、批量抓取邮箱、抓取受限数据等做法。如果流程涉及个人信息、销售触达或合规要求,先确认使用目的、合法依据、隐私政策、保留时间、退订方式和当地规则。

设计时请以官方资料为准:robots.txt 协议看 RFC 9309,搜索抓取解释看 Google robots.txt 文档,标准请求能力看 MDN Fetch API,浏览器自动化隔离看 Playwright Browser contexts

推荐流程

flowchart TD
  A["明确一个用途"] --> B["先查 API、RSS、sitemap"]
  B --> C["检查条款与 robots.txt"]
  C --> D{"静态 HTML 能解决吗?"}
  D -->|可以| E["用 Fetch 一页一页获取"]
  D -->|不行且已获许可| F["用 Playwright 检查渲染后的 DOM"]
  E --> G["CSV 保存 URL 与时间"]
  F --> G
  G --> H["人工抽样复核后再使用"]

这样写需求,Claude Code 才不会自由发挥。不要说“把这个站点都抓下来”,而是说“只访问这个允许的 origin,每次请求至少间隔 2 秒,被 robots.txt 阻止就停止,CSV 必须包含 sourceUrlfetchedAt”。边界越具体,越不容易生成脆弱选择器、激进循环或不该保存的数据。

Fetch 与 Playwright 怎么选

如果需要的信息已经在 HTML 响应里,就用 fetch。静态文档、博客文章、公开价格页、状态页、从 sitemap 得到的 URL 检查,通常都适合 Fetch。它只是发 HTTP 请求并读取文本,依赖少,也容易审计。

如果内容必须由浏览器执行 JavaScript 后才出现,才考虑 Playwright。适合的场景是自己的网站本地预览、staging 环境、得到许可的内部 QA。浏览器自动化会加载脚本、Cookie、localStorage 和前端状态,风险更高,所以要用独立 browser context,避免不同任务的登录状态混在一起。

给 Claude Code 的指令也应如此:先做 Fetch 版本,证明静态 HTML 不够时再添加 Playwright。代码审查时重点看是否有固定 sleep 到处乱放、是否误用了登录状态、选择器是否依赖样式 class、是否缺少限速、是否没有记录来源 URL 和时间。

四个实用场景

第一个场景是监控自己的网站。比如检查培训页、产品页、表单、文章、canonical URL、标题和 CTA 文案是否异常。因为网站是自己的,可以自己维护 robots.txt、稳定选择器和运行频率。这个流程可以和 AI 内容运营自动化 以及 内容漏斗审计 连接起来:脚本发现变化,人再决定是否修改页面。

第二个场景是收集公开文档 URL。团队可能需要整理官方文档、内部 handbook、公开知识库的页面列表。很多时候不需要保存全文,只保存 URL、标题、检查时间和状态即可。这样既能支持搜索和编辑计划,也不会复制过多内容。

第三个场景是人工复核的竞品公开价格页检查。公开价格页可以做低频、少量监控,用来发现套餐名称、公开活动或页面结构变化。但输出不能直接当作事实。价格可能受地区、税费、币种和活动条件影响。CSV 必须保留 source URL 和时间,再由人工复核后才能写进销售资料或比较文章。

第四个场景是线索研究,但要有严格护栏。可以少量收集公司名、官网、行业和公开联系页面;不要盲目抓取个人邮箱并直接导入外呼系统。如果后续要发邮件,必须处理退订、发件人身份、抑制名单和人工确认。只有在收集步骤合法且最小化之后,才把它和 Claude Code 邮件自动化 组合。

常见失败

最常见的问题是不看 robots.txt 和条款。robots.txt 不是完整的法律许可,但它是网站公开表达的机器可读边界,应当尊重。服务条款还可能限制自动访问、再利用或商业监控。

第二个问题是看到邮箱就保存。公开页面上的个人信息仍然可能是个人信息。不需要就不要收集;确实需要时,也要记录目的、依据、保留时间、访问权限、删除流程和退订方式。

没有速率限制也很危险。几百个请求瞬间打过去,在对方眼里可能像攻击。小批量、低频率、清楚的 User-Agent、有限重试和失败即停止,是最低要求。

脆弱选择器会让数据悄悄变错。.card > div:nth-child(2) 今天可用,明天设计改版就失效。优先用语义 HTML、time[datetime]main h1 或自己控制的 data 属性。关键字段取不到时,不要默默输出空值,要让任务失败并写日志。

绕过保护不是功能。Claude Code 如果开始建议 CAPTCHA 绕过、登录墙抓取、轮换身份、规避限速,就应该停止并换成被允许的数据来源。

最后,不要默认保存敏感内容。原始 HTML、登录后数据、token、客户信息和个人记录,都不应该随便进入 CSV。安全运营的整体思路可以继续读 Claude Code 安全最佳实践

可复制运行的 Fetch 示例

下面的脚本适用于 Node 18 或更新版本。它只抓取一个允许的页面,保守检查 robots.txt,等待请求间隔,提取标题、H1、描述和链接数量,并写出带 source URL 与时间戳的 CSV,同时生成 JSON 审计文件。

// scrape-allowed-page.mjs
import { writeFile } from "node:fs/promises";

const USER_AGENT = "ClaudeCodeLabAuditBot/1.0 (+https://example.com/bot-info)";
const BOT_TOKEN = "ClaudeCodeLabAuditBot";
const targetUrl = new URL(process.env.SCRAPE_URL ?? "https://example.com/");
const allowedOrigins = (process.env.ALLOWED_ORIGINS ?? "https://example.com")
  .split(",")
  .map((value) => new URL(value.trim()).origin);
const delayMs = Number.parseInt(process.env.REQUEST_DELAY_MS ?? "2000", 10);

if (!allowedOrigins.includes(targetUrl.origin)) {
  throw new Error(`Blocked by allowlist: ${targetUrl.origin}`);
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchText(url, accept) {
  await sleep(delayMs);
  return fetch(url, {
    headers: {
      "user-agent": USER_AGENT,
      accept,
    },
  });
}

async function loadRobots(origin) {
  const robotsUrl = new URL("/robots.txt", origin);
  const response = await fetchText(robotsUrl, "text/plain");
  if (response.status === 404) {
    return { url: robotsUrl.toString(), status: response.status, text: null };
  }
  if (!response.ok) {
    throw new Error(`robots.txt check failed: HTTP ${response.status}`);
  }
  return {
    url: robotsUrl.toString(),
    status: response.status,
    text: await response.text(),
  };
}

function parseRobots(text) {
  const groups = [];
  let agents = [];
  let rules = [];

  function commit() {
    if (agents.length > 0) {
      groups.push({ agents, rules });
    }
    agents = [];
    rules = [];
  }

  for (const rawLine of text.split(/\r?\n/)) {
    const cleaned = rawLine.split("#")[0].trim();
    if (!cleaned) continue;
    const separator = cleaned.indexOf(":");
    if (separator === -1) continue;

    const field = cleaned.slice(0, separator).trim().toLowerCase();
    const value = cleaned.slice(separator + 1).trim();

    if (field === "user-agent") {
      if (rules.length > 0) commit();
      agents.push(value.toLowerCase());
      continue;
    }

    if ((field === "allow" || field === "disallow") && agents.length > 0) {
      rules.push({ type: field, path: value });
    }
  }

  commit();
  return groups;
}

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function pathMatches(pattern, path) {
  if (!pattern) return false;
  const exact = pattern.endsWith("$");
  const normalized = exact ? pattern.slice(0, -1) : pattern;
  const source = `^${escapeRegExp(normalized).replace(/\\\*/g, ".*")}${exact ? "$" : ""}`;
  return new RegExp(source).test(path);
}

function isAllowedByRobots(robotsText, url) {
  if (robotsText === null) {
    return process.env.ALLOW_WITHOUT_ROBOTS === "true";
  }

  const groups = parseRobots(robotsText);
  const bot = BOT_TOKEN.toLowerCase();
  const exactGroups = groups.filter((group) =>
    group.agents.some((agent) => agent !== "*" && bot.includes(agent)),
  );
  const fallbackGroups = groups.filter((group) => group.agents.includes("*"));
  const selectedGroups = exactGroups.length > 0 ? exactGroups : fallbackGroups;
  const rules = selectedGroups.flatMap((group) => group.rules);
  const targetPath = `${url.pathname}${url.search}`;
  let winner = null;

  for (const rule of rules) {
    if (!pathMatches(rule.path, targetPath)) continue;
    const length = rule.path.replace(/[*$]/g, "").length;
    if (!winner || length > winner.length || (length === winner.length && rule.type === "allow")) {
      winner = { type: rule.type, length };
    }
  }

  return winner ? winner.type === "allow" : true;
}

function normalizeText(value) {
  return value
    .replace(/<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<style[\s\S]*?<\/style>/gi, " ")
    .replace(/<[^>]*>/g, " ")
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/\s+/g, " ")
    .trim();
}

function firstMatch(html, pattern) {
  const match = html.match(pattern);
  return match ? normalizeText(match[1]) : "";
}

function extractPageSummary(html) {
  const metaMatch =
    html.match(/<meta\s+[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i) ??
    html.match(/<meta\s+[^>]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i);

  return {
    title: firstMatch(html, /<title[^>]*>([\s\S]*?)<\/title>/i),
    h1: firstMatch(html, /<h1[^>]*>([\s\S]*?)<\/h1>/i),
    metaDescription: metaMatch ? normalizeText(metaMatch[1]) : "",
    linkCount: [...html.matchAll(/<a\s+[^>]*href=["'][^"']+["']/gi)].length,
  };
}

function csvEscape(value) {
  const text = String(value ?? "");
  return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}

const robots = await loadRobots(targetUrl.origin);
if (!isAllowedByRobots(robots.text, targetUrl)) {
  throw new Error(`Blocked by robots.txt: ${targetUrl.toString()}`);
}

const response = await fetchText(targetUrl, "text/html");
if (!response.ok) {
  throw new Error(`Page fetch failed: HTTP ${response.status}`);
}

const html = await response.text();
const fetchedAt = new Date().toISOString();
const row = {
  sourceUrl: targetUrl.toString(),
  fetchedAt,
  ...extractPageSummary(html),
};
const headers = ["sourceUrl", "fetchedAt", "title", "h1", "metaDescription", "linkCount"];
const csv = [headers.join(","), headers.map((header) => csvEscape(row[header])).join(",")].join("\n");

await writeFile("scrape-output.csv", `${csv}\n`, "utf8");
await writeFile(
  "scrape-audit.json",
  JSON.stringify(
    {
      checkedAt: fetchedAt,
      userAgent: USER_AGENT,
      robotsUrl: robots.url,
      robotsStatus: robots.status,
      allowedOrigins,
      sourceUrl: row.sourceUrl,
    },
    null,
    2,
  ),
  "utf8",
);

console.log(`Saved scrape-output.csv for ${row.sourceUrl}`);

PowerShell 示例:$env:SCRAPE_URL="https://your-domain.example/page"; $env:ALLOWED_ORIGINS="https://your-domain.example"; node scrape-allowed-page.mjs。输出只有一行 CSV 和一个 JSON 审计文件,正因为简单,后续复核才容易。

只对自有页面使用 Playwright

下面的 Playwright 示例用于自己的网站或本地预览,不用于绕过外部网站保护。它检查渲染后的选择器是否存在,并写出审计文件。

// check-own-site-selectors.mjs
import { writeFile } from "node:fs/promises";
import { chromium } from "playwright";

const target = process.env.LOCAL_PREVIEW_URL ?? "http://127.0.0.1:4321/blog/claude-code-web-scraping/";
const allowedPrefixes = [
  "http://127.0.0.1:",
  "http://localhost:",
  "https://claudecodelab.com/",
];

if (!allowedPrefixes.some((prefix) => target.startsWith(prefix))) {
  throw new Error(`Playwright check is limited to owned or local pages: ${target}`);
}

const browser = await chromium.launch();
const context = await browser.newContext({
  userAgent: "ClaudeCodeLabAuditBot/1.0 local-preview-check",
});
const page = await context.newPage();

await page.goto(target, { waitUntil: "domcontentloaded" });

const checks = [
  { name: "article title", selector: "main h1, article h1" },
  { name: "updated date", selector: "time, [data-updated-date]" },
  { name: "main article", selector: "main article, article" },
];
const results = [];

for (const check of checks) {
  const locator = page.locator(check.selector);
  const count = await locator.count();
  const firstText = count > 0 ? ((await locator.first().textContent()) ?? "").trim().slice(0, 120) : "";
  results.push({ ...check, count, firstText });
}

await writeFile(
  "selector-audit.json",
  JSON.stringify({ target, checkedAt: new Date().toISOString(), results }, null, 2),
  "utf8",
);

await context.close();
await browser.close();

const missing = results.filter((result) => result.count === 0);
if (missing.length > 0) {
  throw new Error(`Missing selectors: ${missing.map((result) => result.name).join(", ")}`);
}

console.log(`Saved selector-audit.json for ${target}`);

Playwright context 很重要,因为它隔离 Cookie、localStorage 和权限。不要把真实登录会话随便交给自动化脚本,除非任务已经被批准,并且数据允许被处理。

给 Claude Code 的提示词

可以这样要求 Claude Code:

添加一个只处理 allowlist origin 的单页抓取脚本。先在 README 记录是否存在官方 API、RSS 或 sitemap。抓取 HTML 前检查 robots.txt。CSV 必须保存 sourceUrlfetchedAt。不要抓取邮箱、个人姓名、登录后数据或秘密信息。不要绕过 CAPTCHA、登录墙、bot 防护或限速。添加请求间隔,被阻止时停止,并展示 JavaScript 文件的 node --check 结果。

这段提示词的目的,是让 Claude Code 成为可审计的实现者,而不是自动决定合规边界的人。执行前仍然要人工检查 diff、目标 URL、保存字段、请求频率、删除流程和样本输出。

运营检查表

  • 先确认 API、RSS、sitemap 或导出功能。
  • 确认页面公开,且用途不违反条款。
  • 尊重 robots.txt,并把检查结果写入审计日志。
  • 使用 origin allowlist 和小批量请求。
  • 添加延迟、有限重试和失败停止。
  • User-Agent 说明用途或联系页面。
  • 保存 source URL、抓取时间、方法和 robots 状态。
  • 优先使用语义选择器或自有 data 属性。
  • 默认不保存个人信息、秘密、登录数据或原始 HTML。
  • 商业使用前进行人工抽样复核。

如果 CSV 会被电子表格打开,还要注意 CSV injection。来自网页的字符串都应视为不可信输入。把抓取流程接入安全审查、内容运营和外联控制,而不是静默写入 CRM。

培训与咨询

代码本身不难,真正难的是决定不收集什么、如何证明收集被允许、如何复核变化、如何删除过期输出。ClaudeCodeLab 可以通过 Claude Code 培训与咨询 帮团队把这些规则落到 CLAUDE.md、Playwright 检查、CSV 审计日志和人工审批流程里。

个人使用时,从一页、一次、公开数据开始。不要先扩大数量,而要先完善审计、失败停止和人工复核。

总结

用 Claude Code 做安全 Web scraping,起点不是选择器,而是边界。优先 API 和 sitemap;静态公开页面用 Fetch;只有自有或已获许可的动态页面才用 Playwright。每次都记录 URL、时间、robots 状态和 User-Agent。

需要避免的事项同样重要:不忽略条款,不盲目抓取邮箱,不绕过限速,不让选择器静默失败,不绕过保护,不保存敏感数据。Claude Code 可以加速实现,但只有当人能说明目的、来源、时间和删除路径时,这个流程才适合投入实际运营。

Masa 按本文流程实际试过后发现,把第一版限制为一个页面、一行 CSV、一个 JSON 审计文件,复核会轻松很多。sourceUrlfetchedAt 对检查价格页和自有站点内容特别有用。相反,先抓很多页面的原型更难追踪选择器失败和策略遗漏,最后往往要重写。

#Claude Code #web scraping #Fetch API #Playwright #robots.txt
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。