如何使用Scrapeless Scraping浏览器抓取Facebook广告库
Specialist in Anti-Bot Strategies
TL;DR:
- Facebook广告库是一个公共平台——查看其中的广告无需登录。 任何人都可以打开
facebook.com/ads/library,搜索广告主或关键词,并阅读某个页面正在运行的每个活跃和非活跃广告,包括广告主名称、广告文案和每个广告的投放日期。 - 该页面完全在客户端渲染,因此普通HTTP返回的是一个空壳。 广告卡由JavaScript在初始响应后绘制,结果网格经过了反机器人检查,因此简单的
requests.get根本看不到任何广告。 - Scrapeless Scraping Browser在云端渲染页面并返回绘制完成的DOM。 通过Chrome DevTools协议连接到美国住宅出口,并使用保温会话返回完全渲染的结果网格,解析就像任何静态页面一样。
- 从稳定的
Library ID:文本锚点发现每个广告,而不是一个哈希CSS类。 每个广告卡都有一个Library ID: <数字>标签;从该锚点上升到它的单卡容器可以抵御因React类名更改而导致的脆弱选择器失效。 - 关键词搜索在首次渲染时返回27个广告卡,向下滚动六次后的结果为104个。 这里的分页是无限滚动:网格在你滚动时附加更多卡片,因此在每次滚动后阅读DOM,直到计数停止增长。
- 免费开始。 新的Scrapeless账户包括免费的Scraping Browser运行时间——在app.scrapeless.com注册。
介绍:阅读任何品牌正在投放的公共广告
Facebook广告库是Meta在Facebook和Instagram上投放广告的公共透明档案。每个活跃广告——对于社会问题、选举和政治广告,甚至每个非活跃广告——都列出了广告主、创意和投放日期。竞争研究人员、品牌安全团队和广告分析师会查看它,以了解某个页面当前向用户展示了什么信息。
困难在于渲染,而非访问。查看公共广告无需账户,但页面在浏览器中会在第一次响应后构建其整个结果网格,而该网格受到自动化流量防护的限制,普通HTTP客户端会立即触发。用requests下载URL时,广告卡根本不在你得到的字节里——它们是后续由JavaScript绘制的,这些代码从未执行。渲染的标记依赖于旋转的、哈希的React类名,因此固定到类字符串的选择器会在下次前端部署时失效。
本指南在Scrapeless Scraping Browser的基础上构建Python中的提取,这是一种云浏览器,可通过美国住宅出口渲染页面并返回完成的DOM。该模式与推动任何爬虫的渲染→发现→提取→分页循环相同,只是有一个 twist:发现锚点在每个广告卡打印的耐用Library ID:文本上,因此解析可以抵御类名更改。在JavaScript中,静态与动态渲染的整体分割包含在Cheerio和Puppeteer演练中。
你可以用它做什么
- 跟踪竞争对手的实时创意。 拉取每个页面正在运行的活跃广告,以了解当前的信息、优惠和落地页面。
- 建立创意趋势数据集。 收集同一类别中多个广告主的广告文案,分析反复出现的语言、窍门和格式。
- 监控活动时间。 阅读活跃广告上的“开始投放”日期及非活跃广告的投放范围,绘制活动何时启动和结束的时间表。
- 审计品牌存在。 确认哪些广告与经过验证的页面相关,并捕捉假冒者在相似名称下投放的广告。
- 为广告智能管道提供数据。 将渲染的结果网格转化为结构化行——广告主、库ID、状态、日期——以便后续分析或模型读取。
- 访问普通HTTP无法达到的网格。 广告库在反机器人检查下以客户端方式渲染,因此将其提升到云浏览器,并保持使用静态页面的相同解析代码。
为什么选择Scrapeless Scraping Browser
Scrapeless Scraping Browser是一种可定制的、反检测的云浏览器,专为网络爬虫和AI代理设计。专门针对广告库,它带来了:
- 云端JavaScript渲染。 结果网格在初始响应后绘制;云浏览器运行页面并返回已包含广告卡的DOM,因此BeautifulSoup可以像解析静态HTML一样解析它。
- 覆盖195多个国家的住宅代理。 广告库根据观看者国家变化其内容,因此固定美国住宅出口可以返回美国访客看到的相同广告。
- 反检测指纹识别。 页面在机器人的检查后设置网格;云浏览器呈现一致的人类浏览器表面,因此网格渲染而不是挑战。
- 一个API密钥用于整个过程。 Python SDK生成一个
browser_ws_endpoint,您可以通过CDP与Playwright连接,并且相同的密钥覆盖运行时。
在免费的计划中获取您的API密钥,网址是app.scrapeless.com。
先决条件
- Python 3.10或更高版本
scrapelessSDK,playwright和beautifulsoup4- Scrapeless账户和API密钥 — 在app.scrapeless.com注册
- 基本的终端使用知识
安装
安装SDK、协议客户端和HTML解析器:
bash
pip install scrapeless playwright beautifulsoup4
playwright install chromium
playwright install chromium 一次性下载本地协议客户端;实际渲染仍在Scrapeless云中进行。scrapeless SDK生成浏览器会话,beautifulsoup4解析返回的DOM。在运行任何内容之前导出您的密钥:export SCRAPELESS_API_KEY=your_api_token_here。
第一步 — 渲染广告库页面并确认广告存在
抓取从干净的渲染开始。广告库的URL通过查询参数进行搜索 — q为关键词,country为观众地理位置,active_status和ad_type为筛选器。连接到云浏览器,首先在公共Facebook主页上预热会话,以便请求携带已建立的浏览器表面,然后加载搜索URL并在编写单个字段选择器之前计算广告卡片的数量。
python
import re
from scrapeless import Scrapeless
from scrapeless.types import ICreateBrowser
from playwright.sync_api import sync_playwright
URL = ("https://www.facebook.com/ads/library/"
"?active_status=all&ad_type=all&country=US&q=nike")
LIB = re.compile(r"Library ID:\s*\d+")
client = Scrapeless() # 从环境中读取SCRAPELESS_API_KEY
session = client.browser.create(ICreateBrowser(proxy_country="US", session_ttl=240))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.browser_ws_endpoint)
ctx = browser.contexts[0] if browser.contexts else browser.new_context()
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://www.facebook.com/", wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(3_000) # 首先在公共主页上预热会话
page.goto(URL, wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(8_000) # 让结果网格在客户端呈现
html = page.content()
browser.close()
print("html字节数:", len(html), "| 第一次渲染的广告卡片数量:", len(LIB.findall(html)))
这将打印一行类似于 html字节数: 1796025 | 第一次渲染的广告卡片数量: 27 — 大约1.8 MB的渲染HTML,包含27个广告卡片在首次绘制时(字节计数在不同创意加载时会有所波动)。页面标题为广告库,Facebook重写URL以添加自己的默认值(search_type=keyword_unordered,media_type=all,一个sort_data块)。wait_until="domcontentloaded"加上固定的延时是有意为之:广告库流媒体分析和个性化请求从不处于空闲状态,因此等待网络空闲将一直停滞直到超时。首先在主页上预热的原因是让网格渲染而不是反机器人的挑战。
第二步 — 从稳定的锚点发现每个广告,然后提取其字段
结果网格的React类名经过哈希处理并在部署之间轮换,因此像div.x1lliihq这样的选择器是一个隐患。持久信号是每个卡片打印的文本:Library ID: <数字>。在该标签上进行锚点发现,爬升到仍然正好包裹一个库ID的最大的祖先 — 该容器就是一个广告卡片 — 然后从文本和内部链接中读取字段。
python
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser") # 第一步的html
def single_card(node):
# 子树中仍然正好持有一个“Library ID:”的最大祖先。
best, n = node.parent, node.parent
while n is not None and len(LIB.findall(n.get_text(" ", strip=True))) == 1:
best, n = n, n.parent
return best
def fields(card):
txt = card.get_text("\n", strip=True)
lib = re.search(r"Library ID:\s*(\d+)", txt)
status = "Active" if re.search(r"\bActive\b", txt) else (
"Inactive" if "Inactive" in txt else None)
started = re.search(r"Started running on ([A-Z][a-z]+ \d{1,2}, \d{4})", txt)
ran = re.search(r"([A-Z][a-z]+ \d{1,2}, \d{4}) - ([A-Z][a-z]+ \d{1,2}, \d{4})", txt)
advertiser = None
for a in card.find_all("a", href=True):
if re.match(r"https://www\.facebook\.com/[^/?#]+/?$", a["href"]) and a.get_text(strip=True):
```python
advertiser = a.get_text(strip=True)
break
return {
"library_id": lib.group(1) if lib else None,
"advertiser": advertiser,
"status": status,
"started_running": started.group(1) if started else None,
"active_range": list(ran.groups()) if ran else None,
}
seen, cards = set(), []
for node in soup.find_all(string=LIB):
card = single_card(node)
if id(card) not in seen:
seen.add(id(card))
cards.append(card)
records = [fields(c) for c in cards]
print("提取的广告记录:", len(records))
for r in records[:4]:
print(r)
这提取了27条记录。前几条打印为真实的广告商和日期 — {'library_id': '1869276447125570', 'advertiser': '耐克', 'status': '活跃', 'started_running': '2026年3月17日', 'active_range': None}。注意两个日期格式:活跃的广告打印为 于 <日期> 开始运行,而不活跃的广告则打印为 <开始> - <结束>的运行范围,因此解析器读取两者并将另一个字段留为空。广告商来自指向裸页面 URL (facebook.com/<page>/) 的第一个链接,这是卡片中页面名称出现的最稳定的位置。
获取你的免费计划 API 密钥: app.scrapeless.com
第三步 — 通过滚动结果网格加载更多广告
广告库不按页码分页 — 它在你滚动时附加更多卡片。可靠的模式是滚动网格,等待新卡片呈现,重新读取 DOM,然后重复直到卡片计数停止增加。在每次滚动后计算 Library ID: 的出现次数可以告诉你何时该搜索的网格耗尽。
python
session = client.browser.create(ICreateBrowser(proxy_country="US", session_ttl=240))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.browser_ws_endpoint)
ctx = browser.contexts[0] if browser.contexts else browser.new_context()
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://www.facebook.com/", wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(3_000)
page.goto(URL, wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(8_000)
def card_count():
return len(LIB.findall(page.content()))
counts = [card_count()]
for _ in range(6): # 网格在你滚动时加载更多卡片
page.mouse.wheel(0, 6_000)
page.wait_for_timeout(2_500)
counts.append(card_count())
browser.close()
print("每次滚动后的卡片计数:", counts)
这打印 每次滚动后的卡片计数: [27, 37, 47, 66, 75, 85, 104] — 网格从第一次呈现的27张卡片增加到六次滚动后的104张。当两个连续的计数相同时,网格已停止为该查询加载,你可以停止滚动。保持滚动步骤适度,停留时间长 enough 以确保新卡片已绘制,否则你在网格追赶之前读取了计数。
第四步 — 写入结构化输出
步骤 2 中的 records 列表已经是一个具有一致键的字典列表,因此写入 CSV 或 JSON 只需几行。提前决定模式 — 每一行上的键都是相同的 — 以便缺失字段变为 None,不会崩溃。
python
import csv
import json
# records 是步骤 2 中构建的字典列表(在步骤 3 最终滚动后重新运行解析以捕获每个加载的卡片)。
fieldnames = ["library_id", "advertiser", "status", "started_running", "active_range"]
with open("facebook_ads.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for row in records:
writer.writerow({**row, "active_range": json.dumps(row["active_range"])})
with open("facebook_ads.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print("写入", len(records), "行到 facebook_ads.csv 和 facebook_ads.json")
这就是整个循环:在云浏览器中渲染网格,从 Library ID: 锚点发现每张卡片,提取广告商和日期,滚动加载更多,并存储。在 URL 中交换 q 和 country 参数,指向广告库服务的任何广告商、关键字或区域。
负责任地处理广告数据
广告库是一个公共透明表面,其中的广告由品牌发布,任何人都可以看到 — 但是记录仍然与可识别的广告商相关联,因此请小心收集它们:
-
保持在公共表面上。 本指南中的所有内容都读取相同的匿名、无登录结果网格,任何访客都可以看到。不要进行身份验证以访问受限视图或提取公共页面未显示的任何内容。
-
收集广告和广告主数据,而不是个人数据。 有用的字段包括广告主页面、创意和投放日期。避免收集评论者的姓名、反应或与广告相关的任何个人信息。
-
尽量简化,保持目标明确。 仅提取分析所需的字段,且只在用例需要时保留这些字段。
-
尊重平台的条款和速率限制。 遵守Meta的服务条款和机器人排除协议,保持请求数量礼貌;限制出口流量和并发请求,而不是过度请求。
对于商业或合规敏感的工作,请审查适用的平台条款,并在建立定期管道之前咨询法律顾问。
您将获得的内容
解析后,每个广告卡片减少为一个具有一致模式的平面记录:
json
[
{
"library_id": "1869276447125570",
"advertiser": "耐克",
"status": "活跃",
"started_running": "2026年3月17日",
"active_range": null
},
{
"library_id": "308819044896583",
"advertiser": "耐克",
"status": "不活跃",
"started_running": null,
"active_range": ["2023年8月15日", "2025年7月24日"]
}
]
// 模式准确反映步骤2解析输出。字段值为示例。
在实践中可以预期的几个事项:
- 广告卡片数量在不同运行之间会有所变化。 同一查询的返回卡片数量可能会有所不同,这取决于哪些广告正在投放以及网格加载的程度;将数量视为快照,而不是固定总数。
- 两种日期形状,一种模式。 活跃广告携带单一“开始投放日期”;不活跃广告携带投放范围。解析器填充现有字段,另一个字段留为
None。 - 关注文本,重新检查漂移。
库 ID:标签比哈希过的React类更为耐用,但Meta仍会定期更改卡片布局——当标记发生变化时,重新检查发现和字段模式。 - 固定出口流量。
proxy_country="US"确保返回的广告与美国观众一致;更改国家代码以匹配所需地区,因为广告库的结果因国家而异。
结论:扩展您的广告库提取管道
读取Facebook广告库归结为四个步骤:在云浏览器中渲染结果网格,从稳定的库 ID: 锚点发现每个广告,提取广告主和投放日期,向下滚动以加载更多,直到数量稳定。即便在正确的HTTP语义下,单纯的HTTP也无法——在反机器人检查后运行客户端网格——干净地上升到无痕抓取浏览器,它渲染页面并返回解析器已经理解的DOM。
从这里开始,以每个生产抓取器扩展的方式进行扩展:锚定选择器到最耐用的挂钩,并在布局发生变化时重新检查它们,固定美国出口以匹配广告目标受众,将缺失字段视为可为空,并保持每个主机的并发请求礼貌。对于JavaScript中的相同渲染分裂,Cheerio和Puppeteer指南在Node.js中阐述了静态与动态决策,SDK和CLI在docs.scrapeless.com上有文档记录。当您准备以大规模运行时,比较定价页面上的运行时选项。
准备构建您的AI驱动数据管道吗?
加入我们的社区,索取免费计划并与构建广告智能管道的开发者联系:Discord · Telegram。
在app.scrapeless.com注册以获得免费的抓取浏览器运行时,并将上述模式适配到您的广告库研究所需的广告主、关键词和地区。
常见问题解答
问:抓取Facebook广告库合法吗?
广告库是一个公开的透明档案,里面的广告可以在没有登录的情况下公开查看,这使其通常比受限数据具有更牢固的法律基础。规则仍然因辖区和Meta的服务条款而异,因此请审查平台条款,收集广告主和广告数据而不是个人信息,并在商业用途上咨询法律顾问。
问:我需要登录才能查看广告库吗?
不。广告库中的公共广告是为匿名访客呈现的,本指南也是针对未登录用户的界面。请不要认证以查看公共页面未显示的内容。
问:我需要代理吗?
是的。广告库的结果根据观众所在国家而有所不同,并通过机器人检查对内容进行限制,因此请通过住宅代理进行路由,并使用proxy_country锁定国家,以便页面返回本地访客所看到的广告。爬虫浏览器会话包括该出口。
问:页面显示挑战或空网格而不是广告——我该如何获得干净的渲染?
这是请求的反机器人检查。通过Scrapeless爬虫浏览器以美国住宅出口进行渲染,首先在同一会话中加载facebook.com以预热会话,然后再导航到广告库URL,以便请求在加载网格时携带已建立的类人浏览器界面。
问:布局更改后我的选择器停止工作——现在该怎么办?
广告库的React类名是哈希且会轮换,因此永远不要固定在上面。基于每个卡片打印的Library ID:文本进行锚点发现,并从文本模式和裸链接中读取字段。当Meta更改卡片布局时,重新检查那些文本模式,而不是追踪类字符串。
问:我该如何翻阅多个广告屏幕?
该网格使用无限滚动,而不是页面编号。向下滚动页面,等待新卡片绘制,重新读取DOM,并重复此过程直到Library ID:计数停止增长——在一次运行中,它从27张卡片增加到104张,经过六次滚动。
问:我可以并行运行多少次搜索?
保持并发适中——每个主机大约三个会话是合理的上限——这样可以保持在礼貌的请求速率内。云浏览器会话比HTTP请求更重,因此请比静态提取更严格地限制它们。
问:我可以在没有AI代理的情况下做到这一点吗?
可以。上面的Python和SDK流程可以独立完成。代理是在其上方的便利层;渲染 → 发现 → 提取 → 滚动循环是您可以直接安排的简单代码。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



