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 核 8GB3-5
8 核 16GB8-12
16 核 32GB16-25

总结

需求方案解决的问题
静态 HTML 页面Trafilatura 直接提取最快,最轻量
JS 渲染页面CloakBrowser + Trafilatura获取完整渲染内容后提取
Cloudflare 保护CloakBrowser(C++ patch) + 住宅代理绕过指纹检测 + IP 检测
大规模采集批量管线 + SEO 代理池 + 缓存可扩展、可容错

三者协同后的效果

目标站看到的是:住宅 IP + 真实 Chrome 指纹 + 人类行为 → 正常展示内容

采集端得到的是:完整 HTML → Trafilatura 提取出干净的正文和元数据

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。