zhulink logo
自动夜间模式 日间模式 夜间模式
侧栏
0

用 Cloudflare Workers 构建一个带备用方案的轻量级网页元数据爬虫

意外富翁的头像
|
|
|
每天 10 万次免费额度,先直连目标站点,403 就自动去 DuckDuckGo 抓 SEO 信息,平均 200 ms 返回 JSON。代码 120 行,复制-粘贴-部署,全程 5 分钟。 --- 最近在开发一个不一样的网站导航 [Pockoo](https://pockoo.app/),其中有一个功能是相关网站推荐,需要抓取网站的 Title 和 Description 等元数据,然后抛给 AI 做语义识别生成向量。 最开始的方案的直接后端抓取,但是实际运行过程中发现失败率很高,增加了备用补充方案,使用 DuckDuckGo HTML 搜索引擎获取该网站的索引信息,成功率增加了,但是还是不太理想。 后来想到一个“骚”操作: **“把这个功能使用 Cloudflare Workers 实现,网站可以屏蔽我,但总不好屏蔽 Cloudflare 吧?”** 于是把逻辑改成: 1. 先正常抓,成功直接返回 2. 403/超时 → 立刻去 DuckDuckGo 搜 `site:domain`,拿第一条结果的标题 + 摘要 实测成功率从 58% 飙到 96%,成本降到 0——Cloudflare Workers 每天白送 10 万次调用,够了。 --- ## 02 架构 30 秒速览 ``` 浏览器 │ ▼ Cloudflare Worker(全球边缘节点) ┌────────┴────────┐ │1. 直连目标站 │◀── 4 秒超时 │2. 403/超时 │ │3. 搜 DuckDuckGo │ └────────┬────────┘ ▼ 返回 JSON(title & description) ``` - 流式解析:HTMLRewriter 边下载边匹配 `<title>` / `<meta name="description">`,内存忽略不计 - 备用信源:DuckDuckGo 的 `html.duckduckgo.com` 是无 JS 版,返回纯 HTML,解析飞快 - 零运维:Serverless,不写 Dockerfile、不装 Redis,代码一保存就全球上线 --- ## 03 代码与部署 ### ① 打开 Cloudflare 控制台 1. 注册 → 左侧「Workers & Pages」→「Create Worker」 2. 名字随便写,例如 `meta-fet`,点「Deploy」 ### ② 一键替换全部代码 把编辑器里默认内容全删,粘贴下面 120 行,**Save and Deploy** 收工。 ```javascript export default { async fetch(request) { const urlParams = new URL(request.url).searchParams; const targetUrl = urlParams.get('url'); // 基础校验 if (!targetUrl) { return createResponse("", "", "", "Error: Missing url parameter", null, 400); } let lastError = ""; // --- 方案 A: 直接抓取 --- try { const directResult = await fetchDirectly(targetUrl); if (directResult.title) { return createResponse( directResult.title, directResult.description, directResult.keywords, // 增加 keywords "direct", targetUrl ); } lastError = "Direct fetch returned empty title"; } catch (e) { lastError = `Direct fetch failed: ${e.message}`; } // --- 方案 B: DuckDuckGo 搜索备用 --- try { const ddgResult = await fetchFromDDG(targetUrl); if (ddgResult.title) { // 注意:DDG 无法提供原网站的 keywords,故传空字符串 return createResponse( ddgResult.title, ddgResult.description, "", "fallback_ddg", targetUrl ); } lastError += " | DDG fallback returned no results"; } catch (e) { lastError += ` | DDG fallback error: ${e.message}`; } // --- 最终失败方案 --- return createResponse("", "", "", lastError, targetUrl); } }; /** * 统一构造返回对象 (增加 keywords 字段) */ function createResponse(title, description, keywords, source, url, status = 200) { const body = { url: url || "", title: title || "", description: description || "", keywords: keywords || "", // 新增字段 source: source || "" }; return new Response(JSON.stringify(body), { status: status, headers: { "Content-Type": "application/json;charset=UTF-8", "Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600" } }); } /** * 方案 A: 直接请求网站 */ async function fetchDirectly(url) { const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36', }, cf: { cacheEverything: true, cacheTtl: 3600 } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); let data = { title: "", description: "", keywords: "" }; const rewriter = new HTMLRewriter() .on("title", { text(t) { data.title += t.text; } }) .on("meta[name='description']", { element(e) { data.description = e.getAttribute("content"); } }) .on("meta[property='og:description']", { element(e) { if (!data.description) data.description = e.getAttribute("content"); } }) // --- 新增关键词匹配 --- .on("meta[name='keywords']", { element(e) { data.keywords = e.getAttribute("content"); } }) .on("meta[name='Keywords']", { // 兼容大小写不规范的标签 element(e) { if (!data.keywords) data.keywords = e.getAttribute("content"); } }); await rewriter.transform(response).text(); return { title: data.title.trim(), description: data.description?.trim() || "", keywords: data.keywords?.trim() || "" }; } /** * 方案 B: DuckDuckGo 搜索 (逻辑不变,无法提取关键词) */ async function fetchFromDDG(targetUrl) { const domain = new URL(targetUrl).hostname; const searchUrl = `https://html.duckduckgo.com/html/?q=site:${encodeURIComponent(domain)}`; const response = await fetch(searchUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' } }); if (!response.ok) throw new Error("DDG unreachable"); let ddgData = { title: "", description: "" }; let found = false; const rewriter = new HTMLRewriter() .on("a.result__a", { text(t) { if (!found) ddgData.title += t.text; } }) .on("a.result__snippet", { text(t) { if (!found) ddgData.description += t.text; }, onEndTag() { found = true; } }); await rewriter.transform(response).text(); return { title: ddgData.title.trim(), description: ddgData.description.trim() }; } ``` ### ③ 试试好不好用 部署后会得到一个类似 `https://meta-fet.yourname.workers.dev` 的域名,浏览器直接戳: ``` https://meta-fet.yourname.workers.dev/?url=https://zhulink.vip ``` 返回示例: ```json { "url": "https://zhulink.vip", "title": "🔥热议 - 竹林,链接有趣内容,聚合真实想法,和真实的人一起筛内容,不靠算法也能刷到好东西。", "description": "竹林是一个链接优质内容和真实用户讨论的去算法推荐社区,由用户分享推荐优质资讯,聚焦真实评论与用户共鸣,和真实的人一起筛内容,依靠用户共识挑出值得一读的内容,不靠算法也能刷到好东西。", "keywords": "竹林,zhulink,资讯聚合,内容推荐,热点评论,用户精选,新闻互动,每日热榜,去算法推荐", "source": "direct" } ``` 再试一个被盾的站点: ``` https://meta-fet.yourname.workers.dev/?url=https://toutiao.com ``` 大概率能看到 `"source": "ddg"`,说明已经走搜索引擎兜底。 --- ## 04 两个小优化(可选) 1. 防蹭流量 在 Worker 顶部加一行: ```javascript if (request.headers.get('Referer') !== 'https://你的导航站.com') return json({error: 'Forbidden'}, 403); ``` 2. 缓存省额度 对 `extractMeta` 里返回的结果加 `Cache-Control: max-age=86400`,一天内重复请求走 Cloudflare 边缘缓存,不计入 10 万次配额。 缓存机制:Cloudflare Workers 默认支持 cf: { cacheEverything: true }。对于不经常变动的网站标题,建议开启缓存以节省额度和提升速度。

  

🫵 来啊,说点有用的废话!