Greasy Fork is available in English.

Google Ads Transparency Scraper

获取Google广告透明度中心的搜索推荐数据和搜索结果数据

// ==UserScript==
// @name         Google Ads Transparency Scraper
// @namespace    微信:eva-mirror
// @version      1.8
// @description  获取Google广告透明度中心的搜索推荐数据和搜索结果数据
// @author       sheire
// @match        https://adstransparency.google.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      adstransparency.google.com
// ==/UserScript==

(function() {
    'use strict';

    // 存储当前页面的数据
    let currentPageData = [];
    let searchResultsData = [];
    let isPanelOpen = false;
    let currentTab = 'recommendations'; // 'recommendations' 或 'results'
    let isFetching = false; // 是否正在获取数据

    // 添加自定义样式
    GM_addStyle(`
        #fetch-recommendations-btn {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            width: 300px;
            height: 40px;
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
            z-index: 10000;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            display: flex;
            align-items: center;
            justify-content: center;
        }

        #fetch-recommendations-btn:hover {
            background-color: #3367d6;
        }

        #fetch-recommendations-btn .loading-icon {
            margin-right: 8px;
            animation: blink 200ms infinite;
        }

        @keyframes blink {
            0% { opacity: 1; }
            50% { opacity: 0; }
            100% { opacity: 1; }
        }

        #data-panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 80%;
            max-height: 80%;
            background: white;
            border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            z-index: 10001;
            display: none;
            flex-direction: column;
            overflow: hidden;
        }

        #data-panel-header {
            padding: 16px;
            background: #f5f5f5;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #ddd;
        }

        #data-panel-content {
            padding: 16px;
            overflow-y: auto;
            flex-grow: 1;
        }

        #close-panel {
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #757575;
        }

        #copy-data, #copy-selected-data, #copy-data-results, #copy-selected-data-results {
            background: #4285f4;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            margin-bottom: 16px;
            margin-right: 10px;
        }

        #copy-data:hover, #copy-selected-data:hover, #copy-data-results:hover, #copy-selected-data-results:hover {
            background: #3367d6;
        }

        #data-table, #data-table-results {
            width: 100%;
            border-collapse: collapse;
        }

        #data-table th, #data-table td, #data-table-results th, #data-table-results td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }

        #data-table th, #data-table-results th {
            background-color: #f2f2f2;
            position: sticky;
            top: 0;
        }

        #data-table th:first-child, #data-table td:first-child, 
        #data-table-results th:first-child, #data-table-results td:first-child {
            width: 40px;
            text-align: center;
        }

        #data-table a, #data-table-results a {
            color: #4285f4;
            text-decoration: none;
        }

        #data-table a:hover, #data-table-results a:hover {
            text-decoration: underline;
        }

        #overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 10000;
            display: none;
        }

        .tab-container {
            margin-bottom: 16px;
            border-bottom: 1px solid #ddd;
        }

        .tab-button {
            background-color: #f1f1f1;
            border: none;
            outline: none;
            cursor: pointer;
            padding: 10px 20px;
            transition: 0.3s;
            font-size: 16px;
            border-radius: 4px 4px 0 0;
        }

        .tab-button:hover {
            background-color: #ddd;
        }

        .tab-button.active {
            background-color: #4285f4;
            color: white;
        }

        .tab-content {
            display: none;
        }

        .tab-content.active {
            display: block;
        }
    `);

    // 创建按钮
    const button = document.createElement('button');
    button.id = 'fetch-recommendations-btn';
    button.innerHTML = '获取广告主结果';
    document.body.appendChild(button);

    // 创建弹窗和遮罩
    const overlay = document.createElement('div');
    overlay.id = 'overlay';

    const panel = document.createElement('div');
    panel.id = 'data-panel';

    panel.innerHTML = `
        <div id="data-panel-header">
            <h3>广告数据</h3>
            <button id="close-panel">&times;</button>
        </div>
        <div id="data-panel-content">
            <div class="tab-container">
                <button class="tab-button active" data-tab="recommendations">搜索推荐</button>
                <button class="tab-button" data-tab="results">搜索结果</button>
            </div>
            
            <div id="recommendations-tab" class="tab-content active">
                <button id="copy-data">一键复制全部内容</button>
                <button id="copy-selected-data">复制勾选的资料库 id</button>
                <table id="data-table">
                    <thead>
                        <tr>
                            <th><input type="checkbox" id="select-all-header"></th>
                            <th>广告主名</th>
                            <th>资料库 Id or 域名</th>
                            <th>地区</th>
                            <th>广告数</th>
                        </tr>
                    </thead>
                    <tbody></tbody>
                </table>
            </div>
            
            <div id="results-tab" class="tab-content">
                <button id="copy-data-results">一键复制全部内容</button>
                <button id="copy-selected-data-results">复制勾选的资料库 id</button>
                <table id="data-table-results">
                    <thead>
                        <tr>
                            <th><input type="checkbox" id="select-all-header-results"></th>
                            <th>资料库 Id</th>
                            <th>广告主名</th>
                            <th>来源</th>
                        </tr>
                    </thead>
                    <tbody></tbody>
                </table>
            </div>
        </div>
    `;

    document.body.appendChild(overlay);
    document.body.appendChild(panel);

    // 监听按钮点击事件
    button.addEventListener('click', () => {
        showPanel();
    });

    // 关闭弹窗事件
    document.getElementById('close-panel').addEventListener('click', () => {
        hidePanel();
    });

    // 点击遮罩关闭弹窗
    overlay.addEventListener('click', () => {
        hidePanel();
    });

    // Tab切换事件
    document.querySelectorAll('.tab-button').forEach(button => {
        button.addEventListener('click', () => {
            const tabName = button.getAttribute('data-tab');
            switchTab(tabName);
        });
    });

    // 搜索推荐相关事件
    document.getElementById('copy-data').addEventListener('click', () => {
        copyAllDataToClipboard();
    });

    document.getElementById('copy-selected-data').addEventListener('click', () => {
        copySelectedDataToClipboard();
    });

    document.getElementById('select-all-header').addEventListener('change', function() {
        const checkboxes = document.querySelectorAll('#data-table .row-checkbox');
        checkboxes.forEach(checkbox => {
            checkbox.checked = this.checked;
        });
    });

    // 搜索结果相关事件
    document.getElementById('copy-data-results').addEventListener('click', () => {
        copyAllResultsDataToClipboard();
    });

    document.getElementById('copy-selected-data-results').addEventListener('click', () => {
        copySelectedResultsDataToClipboard();
    });

    document.getElementById('select-all-header-results').addEventListener('change', function() {
        const checkboxes = document.querySelectorAll('#data-table-results .row-checkbox');
        checkboxes.forEach(checkbox => {
            checkbox.checked = this.checked;
        });
    });

    // 显示弹窗
    function showPanel() {
        overlay.style.display = 'block';
        panel.style.display = 'flex';
        isPanelOpen = true;
        renderTable();
        renderResultsTable();
    }

    // 隐藏弹窗
    function hidePanel() {
        overlay.style.display = 'none';
        panel.style.display = 'none';
        isPanelOpen = false;
    }

    // Tab切换
    function switchTab(tabName) {
        currentTab = tabName;
        
        // 更新tab按钮状态
        document.querySelectorAll('.tab-button').forEach(button => {
            if (button.getAttribute('data-tab') === tabName) {
                button.classList.add('active');
            } else {
                button.classList.remove('active');
            }
        });
        
        // 显示对应的内容
        document.querySelectorAll('.tab-content').forEach(content => {
            if (content.id === tabName + '-tab') {
                content.classList.add('active');
            } else {
                content.classList.remove('active');
            }
        });
    }

    // 渲染搜索推荐表格数据
    function renderTable() {
        const tbody = document.querySelector('#data-table tbody');
        tbody.innerHTML = '';

        currentPageData.forEach((item, index) => {
            // 检查是否为资料库ID(以AR开头)或域名
            let idCellContent = item['2'] || '';
            if (item['2']) {
                if (item['2'].startsWith('AR')) {
                    // 资料库ID类型
                    idCellContent = `<a href="https://adstransparency.google.com/advertiser/${item['2']}?region=anywhere" target="_blank">${item['2']}</a>`;
                } else if (item['2'].includes('.')) {
                    // 域名类型
                    idCellContent = `<a href="https://adstransparency.google.com/?region=anywhere&domain=${item['2']}" target="_blank">${item['2']}</a>`;
                }
            }

            const row = document.createElement('tr');
            row.innerHTML = `
                <td><input type="checkbox" class="row-checkbox" data-index="${index}"></td>
                <td>${item['1'] || ''}</td>
                <td>${idCellContent}</td>
                <td>${item['3'] || ''}</td>
                <td>${item['4'] || ''}</td>
            `;
            tbody.appendChild(row);
        });

        // 为新添加的复选框添加事件监听器
        document.querySelectorAll('#data-table .row-checkbox').forEach(checkbox => {
            checkbox.addEventListener('change', function() {
                // 检查是否所有行都被选中,以更新全选复选框状态
                const allCheckboxes = document.querySelectorAll('#data-table .row-checkbox');
                const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
                document.getElementById('select-all-header').checked = allChecked;
            });
        });
    }

    // 渲染搜索结果表格数据
    function renderResultsTable() {
        const tbody = document.querySelector('#data-table-results tbody');
        tbody.innerHTML = '';

        searchResultsData.forEach((item, index) => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td><input type="checkbox" class="row-checkbox" data-index="${index}" checked></td>
                <td><a href="https://adstransparency.google.com/advertiser/${item['1']}?region=anywhere" target="_blank">${item['1'] || ''}</a></td>
                <td>${item['12'] || ''}</td>
                <td>${item['14'] || ''}</td>
            `;
            tbody.appendChild(row);
        });

        // 设置表头全选按钮为选中状态
        document.getElementById('select-all-header-results').checked = true;

        // 为新添加的复选框添加事件监听器
        document.querySelectorAll('#data-table-results .row-checkbox').forEach(checkbox => {
            checkbox.addEventListener('change', function() {
                // 检查是否所有行都被选中,以更新全选复选框状态
                const allCheckboxes = document.querySelectorAll('#data-table-results .row-checkbox');
                const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
                document.getElementById('select-all-header-results').checked = allChecked;
            });
        });
    }

    // 复制搜索推荐全部数据到剪贴板
    function copyAllDataToClipboard() {
        let csvContent = "广告主名,资料库 Id or 域名,地区,广告数\n";
        currentPageData.forEach(item => {
            csvContent += `"${item['1'] || ''}","${item['2'] || ''}","${item['3'] || ''}","${item['4'] || ''}"\n`;
        });

        navigator.clipboard.writeText(csvContent).then(() => {
            alert('数据已复制到剪贴板');
        }).catch(err => {
            console.error('复制失败:', err);
            alert('复制失败');
        });
    }

    // 复制搜索推荐选中数据到剪贴板
    function copySelectedDataToClipboard() {
        const selectedCheckboxes = document.querySelectorAll('#data-table .row-checkbox:checked');
        if (selectedCheckboxes.length === 0) {
            alert('请至少选择一项');
            return;
        }

        const selectedIds = Array.from(selectedCheckboxes).map(checkbox => {
            const index = parseInt(checkbox.getAttribute('data-index'));
            return currentPageData[index]['2'] || '';
        }).filter(id => id !== '');

        const clipboardText = selectedIds.join(',');

        navigator.clipboard.writeText(clipboardText).then(() => {
            alert(`已复制 ${selectedIds.length} 个资料库 Id or 域名到剪贴板`);
        }).catch(err => {
            console.error('复制失败:', err);
            alert('复制失败');
        });
    }

    // 复制搜索结果全部数据到剪贴板
    function copyAllResultsDataToClipboard() {
        let csvContent = "资料库 Id,广告主名,来源\n";
        searchResultsData.forEach(item => {
            csvContent += `"${item['1'] || ''}","${item['12'] || ''}","${item['14'] || ''}"\n`;
        });

        navigator.clipboard.writeText(csvContent).then(() => {
            alert('数据已复制到剪贴板');
        }).catch(err => {
            console.error('复制失败:', err);
            alert('复制失败');
        });
    }

    // 复制搜索结果选中数据到剪贴板
    function copySelectedResultsDataToClipboard() {
        const selectedCheckboxes = document.querySelectorAll('#data-table-results .row-checkbox:checked');
        if (selectedCheckboxes.length === 0) {
            alert('请至少选择一项');
            return;
        }

        const selectedIds = Array.from(selectedCheckboxes).map(checkbox => {
            const index = parseInt(checkbox.getAttribute('data-index'));
            return searchResultsData[index]['1'] || '';
        }).filter(id => id !== '');

        const clipboardText = selectedIds.join(',');

        navigator.clipboard.writeText(clipboardText).then(() => {
            alert(`已复制 ${selectedIds.length} 个资料库 Id到剪贴板`);
        }).catch(err => {
            console.error('复制失败:', err);
            alert('复制失败');
        });
    }

    // 更新按钮文本
    function updateButtonText() {
        if (isFetching) {
            button.innerHTML = '<span class="loading-icon">🚗</span> 获取中...';
        } else {
            button.innerHTML = '👌 获取广告主结果';
        }
    }

    // 解析不同类型的数据格式
    function parseDataItem(item) {
        // 类型1: {"1": {"1": "广告主名", "2": "资料库Id", "3": "地区", "4": {"2": {"1": "广告数"}}}}
        if (item['1']) {
            const adCount = item['1']['4'] && item['1']['4']['2'] && item['1']['4']['2']['1'] 
                ? item['1']['4']['2']['1'] : '';
            
            return {
                '1': item['1']['1'] || '',
                '2': item['1']['2'] || '',
                '3': item['1']['3'] || '',
                '4': adCount
            };
        }

        // 类型2: {"2": {"1": "域名"}}
        if (item['2']) {
            return {
                '1': '', // 广告主名未知
                '2': item['2']['1'] || '', // 域名
                '3': '', // 地区未知
                '4': ''  // 广告数未知
            };
        }

        return null;
    }

    // 处理搜索推荐响应数据
    function processResponseData(data) {
        try {
            if (typeof data === 'string') {
                data = JSON.parse(data);
            }

            let parsedItems = [];

            // 处理主要数据结构
            if (data && data['1'] && Array.isArray(data['1'])) {
                data['1'].forEach(item => {
                    const parsed = parseDataItem(item);
                    if (parsed) {
                        parsedItems.push(parsed);
                    }
                });
            }

            // 更新当前页面数据(只保留最后一次请求的数据)
            currentPageData = parsedItems;
            isFetching = false;
            updateButtonText();

            // 如果面板打开中,更新表格
            if (isPanelOpen && currentTab === 'recommendations') {
                renderTable();
            }
        } catch (err) {
            console.error('处理数据出错:', err);
        }
    }

    // 处理搜索结果响应数据
    function processSearchResultsData(data) {
        try {
            if (typeof data === 'string') {
                data = JSON.parse(data);
            }

            let parsedItems = [];

            // 处理主要数据结构
            if (data && data['1'] && Array.isArray(data['1'])) {
                data['1'].forEach(item => {
                    // 提取需要的字段并去重
                    const newItem = {
                        '1': item['1'] || '',   // 资料库 id
                        '12': item['12'] || '', // 广告主名
                        '14': item['14'] || ''  // 来源
                    };
                    
                    // 检查是否已存在相同的资料库 id
                    const exists = parsedItems.some(existingItem => existingItem['1'] === newItem['1']);
                    if (!exists && newItem['1']) {
                        parsedItems.push(newItem);
                    }
                });
            }

            // 更新搜索结果数据
            searchResultsData = parsedItems;
            isFetching = false;
            updateButtonText();

            // 如果面板打开中,更新表格
            if (isPanelOpen && currentTab === 'results') {
                renderResultsTable();
            }
        } catch (err) {
            console.error('处理搜索结果数据出错:', err);
            isFetching = false;
            updateButtonText();
        }
    }

    // 拦截 XMLHttpRequest 请求
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    let lastRecommendationsRequest = null;
    let lastResultsRequest = null;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._url = url;
        return originalOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        if (this._url && this._url.includes('/SearchService/SearchSuggestions')) {
            lastRecommendationsRequest = this;
            isFetching = true;
            updateButtonText();
            this.addEventListener('load', function() {
                try {
                    // 只处理最后一个请求
                    if (this === lastRecommendationsRequest) {
                        processResponseData(this.responseText);
                    }
                } catch (err) {
                    console.error('处理XMLHttpRequest响应失败:', err);
                    isFetching = false;
                    updateButtonText();
                }
            });
        } else if (this._url && this._url.includes('/SearchService/SearchCreatives')) {
            lastResultsRequest = this;
            isFetching = true;
            updateButtonText();
            this.addEventListener('load', function() {
                try {
                    // 只处理最后一个请求
                    if (this === lastResultsRequest) {
                        processSearchResultsData(this.responseText);
                    }
                } catch (err) {
                    console.error('处理搜索结果响应失败:', err);
                    isFetching = false;
                    updateButtonText();
                }
            });
        }
        return originalSend.apply(this, arguments);
    };

})();