4小时前
|
|
|
每天 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 }。对于不经常变动的网站标题,建议开启缓存以节省额度和提升速度。
🫵 来啊,说点有用的废话!
▲