Google Ads Transparency Scraper

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

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

(function() {
    'use strict';

    // 存储拦截到的数据
    let interceptedData = [];
    let isPanelOpen = 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);
        }

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

        #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 {
            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 {
            background: #3367d6;
        }

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

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

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

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

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

        .select-all-container {
            margin-bottom: 10px;
        }

        .select-all-container label {
            font-weight: bold;
            cursor: pointer;
        }
    `);

    // 创建按钮
    const button = document.createElement('button');
    button.id = 'fetch-recommendations-btn';
    button.textContent = '获取搜索推荐 (0)';
    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">
            <button id="copy-data">一键复制全部内容</button>
            <button id="copy-selected-data">复制勾选</button>
            <div class="select-all-container">
                <label>
                    <input type="checkbox" id="select-all-checkbox"> 全选
                </label>
            </div>
            <table id="data-table">
                <thead>
                    <tr>
                        <th><input type="checkbox" id="select-all-header"></th>
                        <th>广告主名</th>
                        <th>资料库 Id or 域名</th>
                        <th>地区</th>
                    </tr>
                </thead>
                <tbody></tbody>
            </table>
        </div>
    `;

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

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

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

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

    // 复制数据事件
    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('.row-checkbox');
        checkboxes.forEach(checkbox => {
            checkbox.checked = this.checked;
        });
    });

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

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

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

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

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

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

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

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

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

        const selectedIds = Array.from(selectedCheckboxes).map(checkbox => {
            const index = parseInt(checkbox.getAttribute('data-index'));
            return interceptedData[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 updateButtonText() {
        button.textContent = `获取搜索推荐 (${interceptedData.length})`;
    }

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

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

        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);
                    }
                });
            }

            // 处理其他可能的数据结构
            if (data && data['2'] && Array.isArray(data['2'])) {
                data['2'].forEach(item => {
                    const parsed = parseDataItem(item);
                    if (parsed) {
                        parsedItems.push(parsed);
                    }
                });
            }

            // 如果直接是对象数组
            if (data && Array.isArray(data)) {
                data.forEach(item => {
                    const parsed = parseDataItem(item);
                    if (parsed) {
                        parsedItems.push(parsed);
                    }
                });
            }

            // 添加新解析的数据
            let newDataCount = 0;
            parsedItems.forEach(parsedItem => {
                // 检查是否已存在相同数据
                const exists = interceptedData.some(existing =>
                    existing['2'] === parsedItem['2'] && existing['2'] !== ''
                );

                if (!exists) {
                    interceptedData.push(parsedItem);
                    newDataCount++;
                }
            });

            if (parsedItems.length > 0) {
                updateButtonText();

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

    // 方法1: 拦截 fetch 请求
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        const [resource, options] = args;

        // 检查是否为 SearchSuggestions 请求
        if (typeof resource === 'string' && resource.includes('/SearchService/SearchSuggestions')) {
            return originalFetch(...args).then(response => {
                // 克隆响应以便可以读取内容
                const clonedResponse = response.clone();

                clonedResponse.text().then(text => {
                    processResponseData(text);
                }).catch(err => {
                    console.error('读取fetch响应失败:', err);
                });

                return response;
            }).catch(err => {
                console.error('fetch请求失败:', err);
                throw err;
            });
        }

        return originalFetch(...args);
    };

    // 方法2: 拦截 XMLHttpRequest 请求
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        if (url && url.includes('/SearchService/SearchSuggestions')) {
            this.addEventListener('load', function() {
                try {
                    processResponseData(this.responseText);
                } catch (err) {
                    console.error('处理XMLHttpRequest响应失败:', err);
                }
            });
        }

        originalOpen.apply(this, arguments);
    };

    // 方法3: 定期检查页面数据(备用方案)
    setInterval(() => {
        try {
            // 尝试从页面中查找相关数据
            const scripts = document.querySelectorAll('script');
            scripts.forEach(script => {
                if (script.textContent && script.textContent.includes('SearchSuggestions')) {
                    // 尝试从script标签中提取JSON数据
                    const matches = script.textContent.match(/\{[^{]*?"[12]".*?\}/gs);
                    if (matches) {
                        matches.forEach(match => {
                            try {
                                const data = JSON.parse(match);
                                if (data['1'] || data['2']) {
                                    processResponseData(data);
                                }
                            } catch (e) {
                                // 忽略解析错误
                            }
                        });
                    }
                }
            });
        } catch (e) {
            // 忽略错误
        }
    }, 3000);

})();