背景:AI 抓取的痛点
当你让 AI Agent 去抓取网页内容时,通常会遇到这些问题:
- HTML 噪音太多 - 导航栏、广告、侧边栏、脚本、样式…
- Token 消耗巨大 - 2,000 字的正文可能需要 15,000+ tokens 的 HTML
- 解析困难 - AI 需要从复杂 HTML 中提取有用信息
- 成本高 - 按 token 付费的模型下,这直接意味着钱
Cloudflare Markdown for Agents 就是为了解决这个问题而生的。
什么是 Cloudflare Markdown for Agents?
这是 Cloudflare 在 2026 年 2 月推出的功能。当 AI Agent 抓取启用了此功能的网站时,Cloudflare 会自动将 HTML 转换为 Markdown 返回。
效果有多显著?
根据 Cloudflare 官方数据:
- 一篇博客文章在 HTML 格式下约 16,180 tokens
- 转换为 Markdown 后仅 3,150 tokens
- 节省约 80% 的 token 消耗
工作原理
当 AI Agent 发送 HTTP 请求时,在请求头中添加:
Accept: text/markdown
如果网站启用了 Cloudflare Markdown for Agents,Cloudflare 会在边缘节点实时将 HTML 转换为 Markdown,然后返回给 AI Agent。
返回的内容:
- ✅ 自动去除 HTML 标签、CSS、JavaScript
- ✅ 保留内容的语义结构(标题、列表、链接等)
- ✅ AI 更容易解析,减少噪声干扰
- ✅ 大幅减少 token 消耗
实战:如何让 AI Agent 抓取 Markdown 格式
无论目标网站是否启用了 Cloudflare Markdown for Agents,你都可以通过以下方法优化抓取效果。
方法 1:请求 Markdown 格式(如果网站支持)
最简单的做法是在 HTTP 请求头中声明接受 Markdown 格式:
import requests
headers = {
'Accept': 'text/markdown, text/html;q=0.8'
}
response = requests.get('https://example.com/article/', headers=headers)
# 检查返回的内容类型
if 'markdown' in response.headers.get('Content-Type', ''):
print("✅ 获取到 Markdown 格式")
content = response.text
else:
print("ℹ️ 返回 HTML,需要转换")
content = html_to_markdown(response.text)
判断网站是否支持:
- 如果返回的
Content-Type包含text/markdown,说明支持 - 目前支持此功能的网站还不多,但会逐渐增加
方法 2:尝试 Markdown 版本 URL
一些网站会主动提供 Markdown 版本,通常的 URL 模式:
https://example.com/posts/article-title/index.md
https://example.com/posts/article-title.md
https://example.com/api/content/article-title?format=md
抓取策略:
- 先尝试
.md或/index.md后缀的 URL - 如果不存在,回退到普通 HTML 抓取
- 将 HTML 转换为 Markdown
方法 3:使用 Smart Fetch 工具
我编写了一个完整的工具,自动完成上述流程:
smart_fetch.py 核心功能:
- 优先请求 Markdown 格式
- 自动检测返回类型
- 如果返回 HTML,自动转换为 Markdown
- 提取正文内容,去除导航和广告
完整源码:
#!/usr/bin/env python3
"""
Smart Fetch - 智能网页抓取工具
支持 Cloudflare Markdown for Agents
自动检测并处理 Markdown/HTML 响应
"""
import sys
import urllib.request
import urllib.error
from html.parser import HTMLParser
import re
class HTMLToMarkdown(HTMLParser):
"""HTML 转 Markdown 转换器"""
def __init__(self):
super().__init__()
self.result = []
self.in_script = False
self.in_style = False
self.skip_tags = {'script', 'style', 'nav', 'header', 'footer', 'aside'}
def handle_starttag(self, tag, attrs):
if tag in ('script', 'style'):
self.in_script = tag == 'script'
self.in_style = tag == 'style'
elif tag in self.skip_tags:
pass
elif tag == 'h1':
self.result.append('\n# ')
elif tag == 'h2':
self.result.append('\n## ')
elif tag == 'h3':
self.result.append('\n### ')
elif tag == 'h4':
self.result.append('\n#### ')
elif tag == 'p':
self.result.append('\n')
elif tag == 'br':
self.result.append('\n')
elif tag == 'a':
attrs_dict = dict(attrs)
if 'href' in attrs_dict:
self.result.append(f'[{attrs_dict.get("title", "") or attrs_dict.get("href", "")}](')
elif tag == 'img':
attrs_dict = dict(attrs)
alt = attrs_dict.get('alt', '')
src = attrs_dict.get('src', '')
if src:
self.result.append(f'')
elif tag in ('ul', 'ol'):
self.result.append('\n')
elif tag == 'li':
self.result.append('- ')
elif tag in ('strong', 'b'):
self.result.append('**')
elif tag in ('em', 'i'):
self.result.append('*')
elif tag == 'code':
self.result.append('`')
elif tag == 'pre':
self.result.append('\n```\n')
def handle_endtag(self, tag):
if tag == 'script':
self.in_script = False
elif tag == 'style':
self.in_style = False
elif tag in self.skip_tags:
pass
elif tag in ('h1', 'h2', 'h3', 'h4', 'p', 'li'):
self.result.append('\n')
elif tag == 'a':
self.result.append(')')
elif tag in ('strong', 'b'):
self.result.append('**')
elif tag in ('em', 'i'):
self.result.append('*')
elif tag == 'code':
self.result.append('`')
elif tag == 'pre':
self.result.append('\n```\n')
def handle_data(self, data):
if self.in_script or self.in_style:
return
text = data.strip()
if text:
self.result.append(text)
def get_markdown(self):
return ''.join(self.result)
def smart_fetch(url, max_chars=5000):
"""智能抓取网页内容"""
# 构建请求头 - 优先请求 Markdown
headers = {
'User-Agent': 'Mozilla/5.0 (compatible; AI-Agent/1.0; +https://www.d5n.xyz)',
'Accept': 'text/markdown, text/plain;q=0.9, text/html;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'identity',
'Connection': 'keep-alive',
}
try:
req = urllib.request.Request(url, headers=headers, method='GET')
with urllib.request.urlopen(req, timeout=30) as response:
content_type = response.headers.get('Content-Type', '').lower()
raw_data = response.read()
try:
content = raw_data.decode('utf-8')
except UnicodeDecodeError:
try:
content = raw_data.decode('gbk')
except:
content = raw_data.decode('utf-8', errors='ignore')
# 检查是否返回了 Markdown
if 'markdown' in content_type:
print(f"✅ 获取到 Markdown 格式 (Content-Type: {content_type})", file=sys.stderr)
return content[:max_chars]
# 如果是纯文本
if 'text/plain' in content_type:
return content[:max_chars]
# 如果是 HTML,转换为 Markdown
print(f"🔄 返回 HTML,转换为 Markdown", file=sys.stderr)
converter = HTMLToMarkdown()
# 提取 body 内容
body_match = re.search(r'<body[^>]*>(.*?)</body>', content, re.DOTALL | re.IGNORECASE)
if body_match:
body_content = body_match.group(1)
else:
body_content = content
converter.feed(body_content)
markdown = converter.get_markdown()
markdown = re.sub(r'\n{3,}', '\n\n', markdown)
return markdown[:max_chars]
except Exception as e:
return f"❌ Error: {str(e)}"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 smart_fetch.py <URL> [max_chars]")
sys.exit(1)
url = sys.argv[1]
max_chars = int(sys.argv[2]) if len(sys.argv) > 2 else 5000
print(smart_fetch(url, max_chars))
使用示例:
# 抓取网页,自动处理 Markdown/HTML
python3 smart_fetch.py "https://example.com/article/"
# 限制返回字符数
python3 smart_fetch.py "https://example.com/article/" 3000
进阶:搜索 + 抓取一体化
在实际应用中,通常需要先搜索,再抓取详细内容。我将 SearXNG 搜索和 Smart Fetch 组合成了一个完整的工具链。
search_and_fetch.py 完整源码:
#!/usr/bin/env python3
"""
SearXNG + Smart Fetch 组合工具
先搜索,再智能抓取详细内容
"""
import sys
import urllib.request
import urllib.error
import urllib.parse
import json
import subprocess
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SEARXNG_URL = "http://localhost:8888"
def searxng_search(query, num_results=5):
"""使用 SearXNG 搜索"""
try:
url = f"{SEARXNG_URL}/search?q={urllib.parse.quote(query)}&format=json"
req = urllib.request.Request(url, headers={
'User-Agent': 'Mozilla/5.0 (compatible; AI-Agent/1.0)'
})
with urllib.request.urlopen(req, timeout=30) as response:
data = json.loads(response.read().decode('utf-8'))
return data.get('results', [])[:num_results]
except Exception as e:
print(f"❌ 搜索失败: {e}", file=sys.stderr)
return []
def smart_fetch(url, max_chars=3000):
"""调用 smart_fetch.py 抓取内容"""
try:
result = subprocess.run(
['python3', os.path.join(SCRIPT_DIR, 'smart_fetch.py'), url, str(max_chars)],
capture_output=True,
text=True,
timeout=30
)
return result.stdout
except Exception as e:
return f"❌ 抓取失败: {e}"
def main():
if len(sys.argv) < 2:
print("""Usage: python3 search_and_fetch.py "搜索关键词" [结果数量] [brief|full]
Options:
结果数量 - 搜索返回的结果数 (默认: 5)
抓取深度 - brief(摘要) | full(全文) (默认: brief)
Examples:
python3 search_and_fetch.py "OpenClaw 教程"
python3 search_and_fetch.py "AI 新闻" 3 full
""")
sys.exit(1)
query = sys.argv[1]
num_results = int(sys.argv[2]) if len(sys.argv) > 2 else 5
fetch_depth = sys.argv[3] if len(sys.argv) > 3 else 'brief'
print(f"🔍 搜索: {query}\n")
# 1. 搜索
results = searxng_search(query, num_results)
if not results:
print("未找到结果")
sys.exit(1)
# 2. 抓取详情
for i, result in enumerate(results, 1):
title = result.get('title', '无标题')
url = result.get('url', '')
content = result.get('content', '')
print(f"\n{'='*60}")
print(f"{i}. {title}")
print(f" URL: {url}")
print(f"{'='*60}\n")
if content:
print(f"📄 摘要: {content[:200]}...")
if fetch_depth == 'full' and url:
print(f"\n🔄 正在抓取详细内容...")
detail = smart_fetch(url, 3000)
print(f"\n📄 详细内容:\n{detail[:1500]}...")
print()
if __name__ == "__main__":
main()
使用方式:
# 仅搜索(返回摘要)
./search-and-fetch.sh "OpenClaw 教程" 5 brief
# 搜索 + 抓取全文
./search-and-fetch.sh "AI 新闻" 3 full
关于 SearXNG 搜索的搭建,可以参考我之前写的文章:
实际效果对比
测试场景:抓取一篇技术博客
| 方式 | Content-Type | Token 数量 | 效果 |
|---|---|---|---|
| 普通 HTML | text/html | ~5,000 | 包含导航、样式等噪声 |
| Markdown 格式 | text/markdown | ~1,000 | 仅保留正文内容 |
| 节省 | - | ~80% | ✅ 显著优化 |
对 AI Agent 的好处
- 成本降低 - Token 消耗减少 60-80%
- 处理更快 - 需要解析的内容更少
- 准确性提升 - 减少 HTML 噪声干扰
- 上下文更长 - 同样的上下文窗口可以容纳更多内容
附:如何让网站支持 Markdown 格式
如果你想让自己的网站也支持 Markdown for Agents,以下是实现方法。
以 Hugo 为例
在 hugo.toml 中配置:
[outputs]
page = ["HTML", "Markdown"]
[outputFormats.Markdown]
mediatype = "text/markdown"
baseName = "index"
isPlainText = true
创建 layouts/_default/single.md 模板:
---
title: "{{ .Title }}"
date: {{ .Date }}
---
{{ .RawContent }}
构建后,每篇文章会同时生成 index.html 和 index.md。
其他平台的思路
- WordPress:使用插件生成 Markdown 版本
- Next.js/Gatsby:在构建时生成
.md文件 - Docusaurus/VitePress:本身 Markdown 源文件,直接提供访问
- 自建系统:发布时同时写入 HTML 和 Markdown
总结
核心要点
- 请求头是关键 - 使用
Accept: text/markdown请求 Markdown 格式 - 尝试 Markdown URL - 部分网站提供
/index.md格式的直接访问 - 自动转换兜底 - 使用 Smart Fetch 工具自动处理 HTML→Markdown 转换
- 组合工具提效 - 搜索+抓取一体化,完整工作流
适用场景
- ✅ AI 助手实时问答(需要抓取外部资料)
- ✅ 内容聚合和分析(批量处理文章)
- ✅ 自动化监控(定期检查更新)
- ✅ 研究辅助(快速获取干净内容)
参考资源
本文示例代码可在 GitHub 获取,欢迎尝试和反馈。