Skip to content

Instantly share code, notes, and snippets.

@gotexis
Last active March 9, 2026 15:12
Show Gist options
  • Select an option

  • Save gotexis/8908926ca9b9a86fe3d6b4a9b830694f to your computer and use it in GitHub Desktop.

Select an option

Save gotexis/8908926ca9b9a86fe3d6b4a9b830694f to your computer and use it in GitHub Desktop.
Meta Facebook Ad Report Extractor
// ==UserScript==
// @name FB Ad Expert Extractor (Row Linked)
// @namespace http://tampermonkey.net/
// @version 2.5
// @description 保留 v1.2 的媒体提取能力,并利用 aria-haspopup 关联表格行数据
// @author Gemini
// @match *://*.facebook.com/adsmanager/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
window.savedArray = window.savedArray || [];
const processedFingerprints = new Set();
// 用于存放当前鼠标悬停行的临时数据
let activeRowData = null;
console.log('%c 🔗 缝合器已就绪: 正在监听预览触发并关联行数据', 'color: #00ff00; font-weight: bold;');
// --- 1. 监听触发器 (aria-haspopup) 捕捉行数据 ---
document.addEventListener('mouseover', (e) => {
const trigger = e.target.closest('[aria-haspopup="true"]');
if (trigger) {
const rowContainer = trigger.closest('div[role="presentation"]');
if (rowContainer) {
// 抓取行内的所有指标
const cells = Array.from(rowContainer.querySelectorAll('._4lg0'));
activeRowData = {
adNameInRow: rowContainer.querySelector('._4-b4')?.innerText || "Unknown",
rowMetrics: cells.map(c => c.innerText.trim()).filter(t => t.length > 0),
lockedAt: Date.now()
};
// 在控制台打印,确认行数据已被锁定
console.log(`%c 📌 已锁定行: ${activeRowData.adNameInRow}`, 'color: #777');
}
}
}, true);
// --- 2. 增强版素材提取 (基于 v1.2 逻辑) ---
function extractAdData() {
const container = document.querySelector('[data-testid="ad-preview-mobile-feed-standard"]');
if (!container) return;
// 提取基本信息
const pageName = container.querySelector('.x1xlr1w8')?.innerText || '';
const adMessage = container.querySelector('[data-ad-preview="message"]')?.innerText || '';
const headline = container.querySelector('[data-ad-preview="headline"]')?.innerText || '';
const ctaText = container.querySelector('[data-ad-preview="cta"]')?.innerText || '';
let mediaItems = [];
// --- v1.2 的媒体识别逻辑 ---
const carouselCards = container.querySelectorAll('[data-ad-preview="carousel-card"]');
if (carouselCards.length > 0) {
carouselCards.forEach(card => {
const img = card.querySelector('img');
if (img) mediaItems.push({ type: 'image', url: img.src });
});
} else {
const videoElement = container.querySelector('video');
const videoCover = container.querySelector('img[data-ad-preview="video-cover"]');
if (videoElement && videoElement.src) {
mediaItems.push({
type: 'video',
url: videoElement.src,
cover: videoCover?.src
});
} else {
const singleImg = container.querySelector('img[data-ad-preview="image"]');
if (singleImg) {
mediaItems.push({ type: 'image', url: singleImg.src });
}
}
}
// --- 缝合逻辑 ---
const firstMediaUrl = mediaItems[0]?.url || '';
// 增加行名称作为指纹的一部分,防止不同行但素材相同的情况
const rowName = activeRowData ? activeRowData.adNameInRow : 'NoRow';
const fingerprint = `${rowName}_${pageName}_${adMessage.slice(0, 20)}_${firstMediaUrl.slice(-30)}`;
if (processedFingerprints.has(fingerprint) || mediaItems.length === 0) return;
// 合并数据
const finalAdObject = {
capturedAt: new Date().toLocaleTimeString(),
// 来自表格行的数据
rowInfo: activeRowData ? {
adName: activeRowData.adNameInRow,
metrics: activeRowData.rowMetrics
} : "Row Data Not Captured",
// 来自预览窗的数据
previewInfo: {
pageName,
adMessage,
headline,
ctaText,
mediaItems
},
fingerprint: fingerprint
};
window.savedArray.push(finalAdObject);
processedFingerprints.add(fingerprint);
const typeLabel = mediaItems[0]?.type === 'video' ? '📹 视频' : '🖼️ 图片';
console.log(`%c ✅ 关联抓取成功 [${typeLabel}]`, 'color: #42b983; font-weight: bold;', finalAdObject);
// 每次抓取成功后,可以考虑不立刻清空 activeRowData,因为有时预览窗加载比 hover 慢
}
// 维持 v1.2 的轮询机制
setInterval(extractAdData, 1000);
// 下载功能
window.downloadAds = function() {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(window.savedArray, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", `fb_stitched_ads_${Date.now()}.json`);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment