// ==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">×</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);
})();