Trafilatura + CloakBrowser:全文采集的完整流水线
结合指纹浏览器和正文提取器的全套采集方案——CloakBrowser 渲染 JS 页面 → Trafilatura 提取正文,支持 Cloudflare 保护的文章站。
亿牛云技术团队2026年5月1日2 分钟阅读
问题场景
Trafilatura 只能处理静态 HTML。但大量目标站属于以下类型:
- SPA 应用(Vue/React):内容由 JavaScript 动态渲染,HTML 源代码中只有空壳
- Cloudflare 保护的站点:直接请求返回 Turnstile 挑战页面,而不是真实内容
- 需要登录的站点:未认证时返回重定向或空内容
这导致 Trafilatura 提取结果为空或不完整。
解决方案:CloakBrowser 渲染 → 获取完整 HTML → Trafilatura 提取。前者解决"拿到完整页面"的问题,后者解决"从完整页面中提取正文"的问题。
管线架构
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 亿牛云代理 │ │ CloakBrowser │ │ Trafilatura │
│ 住宅出口 IP │ ──→ │ 指纹伪装+渲染 │ ──→ │ 正文/元数据提取 │
│ geoip 时区匹配 │ │ humanize 行为 │ │ Markdown/JSON │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│
┌──────┴──────┐
│ 存储/输出 │
│ 文件/数据库 │
└─────────────┘基础实现
import trafilatura
from cloakbrowser import launch
from lxml import html
def render_and_extract(url, proxy=None):
"""使用 CloakBrowser 渲染页面,再用 Trafilatura 提取正文"""
# 1. 启动 CloakBrowser
browser = launch(
proxy=proxy,
headless=False,
geoip=True,
humanize=True,
)
page = browser.new_page()
try:
# 2. 访问页面
page.goto(url, wait_until="networkidle")
# 3. 等待内容渲染完成
page.wait_for_timeout(3000)
# 4. 获取完整 HTML
html_content = page.content()
# 5. 用 Trafilatura 提取正文
result = trafilatura.extract(
html_content,
output_format="markdown",
with_metadata=True,
include_tables=True,
)
return result
finally:
browser.close()
# 使用
result = render_and_extract(
"https://example.16yun.cn/article",
proxy="http://user:pass@proxy.16yun.cn:8888",
)
print(result)实际案例:采集 Cloudflare 保护的文章站
import trafilatura
import json
from cloakbrowser import launch_persistent_context
def scrape_cloudflare_protected_site():
"""采集 Cloudflare 保护的技术博客"""
# 使用持久化上下文——首次访问通过 Turnstile 后,后续复用
ctx = launch_persistent_context(
"./profiles/tech-blog",
headless=False,
proxy="http://user:pass@proxy.16yun.cn:8888",
geoip=True,
humanize=True,
)
page = ctx.new_page()
# 访问首页——可能触发 Turnstile
page.goto("https://example.16yun.cn", wait_until="networkidle")
# 首次访问可能需要手动处理 Turnstile(CloakBrowser 自动通过)
page.wait_for_timeout(5000)
# 获取文章链接列表
article_links = page.eval_on_selector_all(
"a[href*='/blog/']",
"elements => elements.map(el => el.href)"
)
print(f"发现 {len(article_links)} 篇文章")
# 逐篇提取
for i, article_url in enumerate(article_links[:10]):
print(f"[{i+1}/{len(article_links[:10])}] {article_url}")
page.goto(article_url, wait_until="networkidle")
page.wait_for_timeout(2000)
html_content = page.content()
result = trafilatura.extract(
html_content,
output_format="json",
with_metadata=True,
include_tables=True,
)
if result:
data = json.loads(result)
data["url"] = article_url
filename = f"articles/article-{i+1}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f" SAVED:: {data.get('title', 'untitled')}")
ctx.close()
scrape_cloudflare_protected_site()集成到批量管线
将渲染 + 提取封装为可复用的管线段:
import trafilatura
import json
from cloakbrowser import launch
from concurrent.futures import ThreadPoolExecutor, as_completed
import os
class RenderingExtractor:
"""渲染 + 提取管线"""
def __init__(self, proxy, profile_dir="./profiles"):
self.proxy = proxy
self.profile_dir = profile_dir
def extract(self, url, profile_name="default"):
ctx = launch_persistent_context(
os.path.join(self.profile_dir, profile_name),
headless=False,
proxy=self.proxy,
geoip=True,
humanize=True,
)
page = ctx.new_page()
try:
page.goto(url, wait_until="networkidle")
page.wait_for_timeout(3000)
html_content = page.content()
result = trafilatura.extract(
html_content,
output_format="json",
with_metadata=True,
include_tables=True,
)
return json.loads(result) if result else None
finally:
ctx.close()
# 使用
extractor = RenderingExtractor(
proxy="http://user:pass@proxy.16yun.cn:8888"
)
urls = [
"https://example.16yun.cn/article-1",
"https://example.16yun.cn/article-2",
]
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {
executor.submit(extractor.extract, url, f"site-{i}"): url
for i, url in enumerate(urls)
}
for future in as_completed(futures):
result = future.result()
if result:
print(f"OK: {result.get('title')}")各产品在全链路中的角色
| 组件 | 角色 | 亿牛云产品配合 |
|---|---|---|
| 代理 IP | 提供住宅出口,不被 CDN 拦截 | 爬虫代理 / API 代理 / 独享代理 |
| CloakBrowser | 指纹伪装 + JS 渲染 + 绕过反爬 | geoip=True 自动匹配代理 IP 的时区 |
| Trafilatura | 从完整 HTML 中提取正文和元数据 | 不直接关联,但结果质量依赖前两步 |
性能注意事项
大页面渲染时间长
SPA 页面可能需要 5-10 秒才能完整渲染。设置合理的等待策略:
# 等待特定元素出现,而非固定时间
page.wait_for_selector("article", timeout=15000)
# 或等待网络空闲
page.goto(url, wait_until="networkidle")并发限制
每个 CloakBrowser 实例占用 300-500MB 内存。并发数建议:
| 服务器配置 | 推荐并发数 |
|---|---|
| 4 核 8GB | 3-5 |
| 8 核 16GB | 8-12 |
| 16 核 32GB | 16-25 |
总结
| 需求 | 方案 | 解决的问题 |
|---|---|---|
| 静态 HTML 页面 | Trafilatura 直接提取 | 最快,最轻量 |
| JS 渲染页面 | CloakBrowser + Trafilatura | 获取完整渲染内容后提取 |
| Cloudflare 保护 | CloakBrowser(C++ patch) + 住宅代理 | 绕过指纹检测 + IP 检测 |
| 大规模采集 | 批量管线 + SEO 代理池 + 缓存 | 可扩展、可容错 |
三者协同后的效果:
目标站看到的是:住宅 IP + 真实 Chrome 指纹 + 人类行为 → 正常展示内容
采集端得到的是:完整 HTML → Trafilatura 提取出干净的正文和元数据
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。