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品类+广告创意参考",全程自动化。


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

相关推荐
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年6月5日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
爱吃提升2 小时前
Python 多线程(threading)和 多进程(multiprocessing)核心区别
python
MageGojo2 小时前
基于 API Zero 平台集成 TTS 语音合成服务的技术实践
python·语音合成·tts·restful api·api集成
YsyaaabB2 小时前
LangChain作业二---多语言翻译Prompt
开发语言·python·langchain
HappyAcmen2 小时前
2.PDF长文档完整读取
python·pdf·rag
装不满的克莱因瓶2 小时前
掌握感知器的学习原理
人工智能·python·神经网络·算法·ai·卷积神经网络
py小王子2 小时前
Nature 期刊图复现|Python 实现双轴高维直方图与重叠分布图
python·nature·期刊图复现
小熊Coding2 小时前
从零打造一款回合制 RPG 游戏:基于 Pygame 的《塔影守卫》全解析
python·游戏·计算机专业·pygame·rpg·2d游戏
大貔貅喝啤酒3 小时前
pip 国内镜像源大全【测试 / 自动化开发常备】
运维·自动化·pip·国内镜像源