本文是「跨境电商选品工具链」系列第三篇。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 名称是由构建工具自动生成的哈希值(如 x1dr75xp、x8t9es0),每次前端重新部署后都可能改变。
常见的 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(产品市场契合),值得深度参考。
九、注意事项
- Meta Ad Library 不需要登录,可以直接访问,无需处理 Cookie 认证
- 不要并发请求,Meta 对频繁请求会返回验证码
- 截图留档 ,
page.screenshot(full_page=True)可以保存完整页面供人工复核 - 结果存 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品类+广告创意参考",全程自动化。
完整代码见文末,欢迎留言交流。