Playwright实战:抓取Meta Ad Library动态页面的三级降级策略

本文是「跨境电商选品工具链」系列第三篇。Meta Ad Library(Facebook广告资料库)是目前唯一公开的、覆盖亿级广告的创意素材数据库。本文详细介绍如何用 Playwright 稳定抓取广告数据,以及应对 Meta 不稳定 CSS 结构的三级降级方案。


一、为什么要抓 Meta Ad Library?

通过 Google Trends 筛选出上升趋势的长尾词之后,下一步问题是:这个品类的广告应该怎么打?

Meta Ad Library 是一个合法的公开数据库(Meta 为满足广告透明度监管要求建立),可以搜索任意关键词,查看当前所有活跃广告的:

  • 广告主名称
  • 广告文案(headline + body copy)
  • 投放起始日期
  • CTA 按钮类型
  • 广告格式(图片/视频/轮播)

一条广告跑了 12 个月还在活跃,说明它在持续盈利。这类"长跑广告"是创意参考的最高价值来源。

访问地址: https://www.facebook.com/ads/library/


二、技术挑战:Meta 的动态 CSS 结构

Meta Ad Library 是一个 React 应用,CSS class 名称是由构建工具自动生成的哈希值(如 x1dr75xpx8t9es0),每次前端重新部署后都可能改变

常见的 CSS 选择器方案会这样翻车:

python 复制代码
# ❌ 不稳定 ------ 这个 class 可能在下次部署后消失
cards = page.query_selector_all('div[class*="x1dr75xp"]')

# 实际返回: ['Meta Ad Library(导航栏)', '美国(国家筛选器)', 
#             'Filters(UI按钮)', '一条真实广告', 'System status(页脚)']
# 5个元素中只有1个是广告卡片,其他都是UI框架元素

解决思路: 不依赖 CSS class,改用语义特征定位广告卡片------所有广告卡片都包含"Started running on"文字(投放起始日期),以此为锚点做 DOM 遍历。


三、三级降级策略架构

复制代码
Level 1: JS DOM TreeWalker(主方法)
   ↓ 失败时
Level 2: CSS 多选择器轮询(备用)
   ↓ 失败时
Level 3: 全页文本正则分割(兜底)

四、Level 1:JS TreeWalker ------ 最稳定的方案

核心思路:TreeWalker 遍历所有文本节点,找到包含 "Started running on" 的节点,然后向上爬父节点,找到足够大的容器(高度>250px,宽度>350px)作为广告卡片。

python 复制代码
js_ads = page.evaluate("""
() => {
    const results = [];

    // 遍历所有文本节点
    const walker = document.createTreeWalker(
        document.body,
        NodeFilter.SHOW_TEXT,
        null
    );

    const dateNodes = [];
    let node;
    while ((node = walker.nextNode())) {
        if (node.textContent.includes('Started running on'))
            dateNodes.push(node);
    }

    // 从每个日期节点向上找广告卡片容器
    for (const dn of dateNodes) {
        let card = dn.parentElement;
        for (let i = 0; i < 25; i++) {
            if (!card || !card.parentElement) break;
            const r = card.getBoundingClientRect();
            // 高度>250、宽度>350 的容器即为广告卡片
            if (r.height > 250 && r.width > 350) break;
            card = card.parentElement;
        }
        if (card) results.push(card.innerText || card.textContent);
    }

    // 去重(同一张卡可能有多个日期节点)
    const seen = new Set();
    return results.filter(t => {
        const key = t.substring(0, 80);
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    });
}
""")

优点: 完全不依赖 CSS class,只依赖"广告都有投放日期"这个业务不变量,极其稳定。


五、从卡片文本解析结构化数据

拿到每张卡片的纯文本后,需要提取关键字段:

python 复制代码
import re

def parse_card(text: str, index: int) -> dict | None:
    lines = [l.strip() for l in text.split('\n') if l.strip()]
    if len(lines) < 3:
        return None

    ad = {"index": index}

    # 1. 广告主名:找"Started running on"之前的最后一个短行
    date_idx = next((i for i, l in enumerate(lines)
                     if "Started running on" in l), -1)
    candidates = [l for l in lines[:max(date_idx, 1)] if 2 < len(l) < 80]
    ad["advertiser"] = candidates[-1] if candidates else lines[0]

    # 2. 投放起始日期
    m = re.search(r'Started running on (.+?\d{4})', text)
    if m:
        ad["started_running"] = m.group(1).strip()

    # 3. 广告文案:过滤掉 UI 文字,取长度>30的行
    EXCLUDE = {"Started running on", "Active", "See ad details",
               "See More", "Inactive", "About this ad"}
    body = [l for l in lines
            if len(l) > 30 and not any(e in l for e in EXCLUDE)]
    ad["ad_copy"]      = body[0] if body else ""
    ad["ad_copy_full"] = " | ".join(body[:5])

    # 4. CTA 按钮
    CTA_KEYWORDS = ["Shop Now", "Learn More", "Get Offer",
                    "Buy Now", "Order Now", "Subscribe"]
    ad["cta"] = next((l for l in lines
                      if any(k in l for k in CTA_KEYWORDS)), "")

    # 5. 媒体格式
    tl = text.lower()
    ad["format"] = ("Video" if "video" in tl
                    else "Carousel" if "carousel" in tl
                    else "Image")
    return ad

六、Level 3:全页文本兜底方案

当 DOM 方案完全失效(极少数情况),用正则分割整页文本:

python 复制代码
def parse_full_text(page_text: str, max_ads: int = 20) -> list[dict]:
    ads = []
    # 按"Started running on"分割,每段对应一条广告
    segments = re.split(r'(?=Started running on )', page_text)
    date_pat  = re.compile(r'Started running on (.+?)(?:\n|$)')

    prev_tail = ""
    for seg in segments:
        if not seg.strip().startswith("Started running on"):
            prev_tail = seg
            continue

        m = date_pat.match(seg)
        if not m:
            prev_tail = seg
            continue

        date_str   = m.group(1).strip()[:40]
        rest_lines = [l.strip() for l in seg[m.end():].split('\n') if l.strip()]

        # 广告主:从上一段末尾找短行
        prev_lines = [l.strip() for l in prev_tail.split('\n') if l.strip()]
        advertiser = next((l for l in reversed(prev_lines[-6:])
                           if 2 < len(l) < 80), "")

        body_lines = [l for l in rest_lines[:20]
                      if len(l) > 25 and "Started running" not in l]
        CTA_KWS = ["Shop Now", "Learn More", "Buy Now", "Get Offer"]
        cta     = next((l for l in rest_lines[:12]
                        if any(k in l for k in CTA_KWS)), "")

        ads.append({
            "index":           len(ads) + 1,
            "advertiser":      advertiser,
            "started_running": date_str,
            "ad_copy":         body_lines[0] if body_lines else "",
            "ad_copy_full":    " | ".join(body_lines[:5]),
            "cta":             cta,
        })
        prev_tail = seg
        if len(ads) >= max_ads:
            break

    return ads

七、完整抓取流程

python 复制代码
from playwright.sync_api import sync_playwright
import time

BASE_URL = (
    "https://www.facebook.com/ads/library/"
    "?active_status=active&ad_type=all&country=US"
    "&media_type=all&search_type=keyword_unordered&q={query}"
)

def scrape(keyword: str, max_ads: int = 20) -> list[dict]:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False, args=["--lang=en-US"])
        page = browser.new_context(locale="en-US").new_page()

        # 导航到搜索页
        url = BASE_URL.format(query=keyword.replace(" ", "+"))
        page.goto(url, timeout=30000)
        time.sleep(3)

        # 关闭 Cookie 弹窗
        for btn in ["Allow all cookies", "接受所有 Cookie"]:
            try:
                page.click(f'button:has-text("{btn}")', timeout=2000)
                break
            except:
                pass

        # 滚动加载更多广告(每次滚动约加载 3-4 条)
        for i in range(8):
            page.keyboard.press("End")
            time.sleep(2.5)

        # Level 1: JS DOM 方案
        js_cards = page.evaluate(JS_TREEWALK)  # 见上文
        if js_cards and len(js_cards) >= 1:
            ads = [parse_card(t, i+1) for i, t in enumerate(js_cards[:max_ads])]
            ads = [a for a in ads if a]
            if ads:
                browser.close()
                return ads

        # Level 3: 全页文本兜底
        page_text = page.inner_text("body")
        browser.close()
        return parse_full_text(page_text, max_ads)

八、实战抓取结果示例

"ergonomic lumbar support cushion" 为例,抓取结果(节选):

广告主 投放起始 运行时长 核心文案 CTA
Nordic Comforts 3 Jun 2025 12个月+ 🛑 Tired of back pain from sitting all day? Shop Now
Nordic Comforts 3 Jun 2025 12个月+ Breaking News: Your Chair Might Be Killing You! Shop Now
Celinva 3 Apr 2026 2个月 Say goodbye to uncomfortable car seats. Shop Now

关键洞察: Nordic Comforts 用两套文案轮跑了超过12个月,说明这两套创意都已经找到 PMF(产品市场契合),值得深度参考。


九、注意事项

  1. Meta Ad Library 不需要登录,可以直接访问,无需处理 Cookie 认证
  2. 不要并发请求,Meta 对频繁请求会返回验证码
  3. 截图留档page.screenshot(full_page=True) 可以保存完整页面供人工复核
  4. 结果存 JSON ,广告文案包含大量 emoji,用 ensure_ascii=False 正确序列化

十、小结

方案 优点 局限
JS TreeWalker 不依赖CSS class,最稳定 需要页面已加载 JS
全页文本正则 最简单,无 DOM 依赖 广告主名不易提取

完整脚本支持 CLI 调用:

bash 复制代码
python3 meta_adlib_scraper.py "lumbar cushion" "air purifier desk" --max 20
python3 meta_adlib_scraper.py --file winning_keywords.txt --headless

下一篇是本系列的收官篇:将 Google Trends 筛词和 Meta 广告素材抓取整合成一条完整的选品研究流水线,从"100个候选词"到"Top5品类+广告创意参考",全程自动化。


完整代码见文末,欢迎留言交流。

相关推荐
青 春 记 忆6 小时前
Dify Docker Compose 通用无损升级指南:从备份、双版本预演到切换与回滚
运维·人工智能·python·docker·容器
GlueNa2SiO36 小时前
03-Flask模板引擎Jinja2详解
笔记·python·flask
临沂GEO8 小时前
GEO搜索优化科普|正规地理位置流量运营入门指南
大数据·人工智能·python·流量运营
大模型码小白8 小时前
Spring AI Tool 实现自然语言操作 MySQL 数据库详解
服务器·开发语言·数据库·人工智能·python·mysql·spring
深蓝电商API9 小时前
常见反爬策略全解析
爬虫·反爬策略
荷蒲9 小时前
【小白量化Qbuddy】利用AI学习中文Python
人工智能·python·学习
数据狐(Datafox)11 小时前
京东商品列表API技术解析与落地应用(含标准 JSON 示例)
java·大数据·前端·人工智能·python·数据分析·json
量化吞吐机11 小时前
2026年下半年手工交易规则走向量化表达,产品该先接住哪一步
人工智能·python
志栋智能12 小时前
迈向真正的超自动化巡检
运维·自动化
Metaphor69212 小时前
使用 Python 读取和删除 Excel 文件属性
python·excel