// ==UserScript==
// @name Google Search Suggestions Collector
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Collect Google search suggestions
// @author WWW
// @include *://www.google.*/*
// @include *://google.*/*
// @grant GM_setClipboard
// @license MIT
// ==/UserScript==
let MAX_CONCURRENT_REQUESTS = 5; // 最大并发请求数
let REQUEST_DELAY = 100; // 请求间隔(ms)
(function() {
'use strict';
// 在全局作用域内添加状态变量
let isCollecting = false;
let shouldStop = false;
function addStyles() {
const style = document.createElement('style');
style.textContent = `
.suggest-collector-btn {
position: fixed;
right: 200px;
top: 20px;
width: 50px;
height: 50px;
border-radius: 25px;
background: var(--collector-bg, #ffffff);
border: 2px solid var(--collector-border, #e0e0e0);
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
cursor: move;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.suggest-collector-panel {
position: fixed;
width: 300px;
background: var(--collector-bg, #ffffff);
border: 1px solid var(--collector-border, #e0e0e0);
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
z-index: 9999;
display: none;
}
.suggest-collector-panel input {
width: 100%;
padding: 8px;
border: 1px solid var(--collector-border, #e0e0e0);
border-radius: 4px;
margin-bottom: 10px;
background: var(--collector-input-bg, #ffffff);
color: var(--collector-text, #333333);
}
.suggest-collector-panel button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #4CAF50;
color: white;
cursor: pointer;
transition: background 0.3s;
}
.suggest-collector-panel button:hover {
background: #45a049;
}
.suggest-collector-panel textarea {
background: var(--collector-input-bg, #ffffff);
color: var(--collector-text, #333333);
border: 1px solid var(--collector-border, #e0e0e0);
border-radius: 4px;
}
@media (prefers-color-scheme: dark) {
:root {
--collector-bg: #2d2d2d;
--collector-border: #404040;
--collector-text: #e0e0e0;
--collector-input-bg: #3d3d3d;
}
}
.input-mode-selector {
display: flex;
gap: 20px;
margin-bottom: 15px;
padding: 0 10px;
}
.input-mode-selector label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: var(--collector-text, #333333);
min-width: 70px;
}
.input-mode-selector input[type="radio"],
.filter-options input[type="checkbox"] {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.filter-options {
margin-bottom: 15px;
padding: 0 10px;
}
.filter-options label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: var(--collector-text, #333333);
justify-content: flex-end;
}
#singleInput {
padding: 0 10px;
}
.depth-selector {
margin-bottom: 15px;
padding: 0 10px;
display: flex;
align-items: center;
gap: 10px;
}
.depth-selector label {
color: var(--collector-text, #333333);
}
.depth-selector select {
padding: 5px;
border-radius: 4px;
border: 1px solid var(--collector-border, #e0e0e0);
background: var(--collector-input-bg, #ffffff);
color: var(--collector-text, #333333);
cursor: pointer;
}
`;
document.head.appendChild(style);
}
function createUI() {
const btn = document.createElement('div');
btn.className = 'suggest-collector-btn';
btn.innerHTML = '🔍';
document.body.appendChild(btn);
const panel = document.createElement('div');
panel.className = 'suggest-collector-panel';
panel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<div class="input-mode-selector">
<label><input type="radio" name="inputMode" value="single" checked> single</label>
<label><input type="radio" name="inputMode" value="batch"> batch</label>
</div>
<div class="filter-options">
<label><input type="checkbox" id="onlyEnglish"> Only English</label>
</div>
</div>
<div class="depth-selector">
<label>Search Depth:</label>
<select id="searchDepth">
<option value="1">1 letter</option>
<option value="2">2 letters</option>
<option value="3">3 letters</option>
<option value="4">4 letters</option>
<option value="5">5 letters</option>
</select>
</div>
<div class="performance-settings" style="display: flex; gap: 10px; margin-bottom: 15px; padding: 0 10px;">
<div style="flex: 1;">
<label style="display: block; margin-bottom: 5px; color: var(--collector-text);">Max Concurrent:</label>
<input type="number" id="maxConcurrent" value="5" min="1" max="20"
style="width: 100%; padding: 5px; border: 1px solid var(--collector-border);
border-radius: 4px; background: var(--collector-input-bg);
color: var(--collector-text);">
</div>
<div style="flex: 1;">
<label style="display: block; margin-bottom: 5px; color: var(--collector-text);">Delay (ms):</label>
<input type="number" id="requestDelay" value="100" min="0" max="1000" step="50"
style="width: 100%; padding: 5px; border: 1px solid var(--collector-border);
border-radius: 4px; background: var(--collector-input-bg);
color: var(--collector-text);">
</div>
</div>
<div id="singleInput">
<input type="text" id="baseKeyword" placeholder="type keyword">
</div>
<div id="batchInput" style="display: none;">
<textarea id="batchKeywords" placeholder="type keyword in each line" style="width: 100%; height: 100px; margin-bottom: 10px;"></textarea>
</div>
<button id="startCollect">start collect</button>
<div id="estimatedTime" style="margin: 10px 0; color: var(--collector-text);"></div>
<div id="progress" style="display: none; margin-top: 10px;">
<div style="margin-bottom: 8px;">
total progress: <span id="totalProgress">0/0</span>
<div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
<div id="totalProgressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
</div>
</div>
<div style="margin-bottom: 8px;">
current keyword progress: <span id="progressText">0/26</span>
<div style="background: var(--collector-border); height: 20px; border-radius: 10px;">
<div id="progressBar" style="width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s;"></div>
</div>
</div>
<div>collected: <span id="collectedCount">0</span> items</div>
</div>
<div id="result" style="max-height: 300px; overflow-y: auto; margin-top: 10px;"></div>
`;
document.body.appendChild(panel);
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
// 更新面板位置的函数
function updatePanelPosition() {
const btnRect = btn.getBoundingClientRect();
panel.style.right = `${window.innerWidth - (btnRect.right + 20)}px`;
panel.style.top = `${btnRect.bottom + 20}px`;
}
btn.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === btn) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
btn.style.transform = `translate(${currentX}px, ${currentY}px)`;
// 拖动时更新面板位置
updatePanelPosition();
}
}
function dragEnd() {
isDragging = false;
}
btn.addEventListener('click', (e) => {
if (!isDragging) {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
if (panel.style.display === 'block') {
updatePanelPosition();
}
}
});
// 添加事件监听器来实时更新预估时间
function updateEstimatedTime() {
const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value) || 5;
const requestDelay = parseInt(document.getElementById('requestDelay').value) || 100;
const searchDepth = parseInt(document.getElementById('searchDepth').value);
const isBatchMode = document.querySelector('input[name="inputMode"]:checked').value === 'batch';
let keywordCount = 0;
if (isBatchMode) {
const batchText = document.getElementById('batchKeywords').value.trim();
keywordCount = batchText.split('\n').filter(k => k.trim()).length;
} else {
const singleKeyword = document.getElementById('baseKeyword').value.trim();
keywordCount = singleKeyword ? 1 : 0;
}
if (keywordCount === 0) {
document.getElementById('estimatedTime').innerHTML =
'Please enter keyword(s) to see estimated time';
return;
}
const { totalRequests, estimatedSeconds } = calculateEstimatedTime(
keywordCount,
searchDepth,
maxConcurrent,
requestDelay
);
const minutes = Math.floor(estimatedSeconds / 60);
const seconds = estimatedSeconds % 60;
const timeStr = minutes > 0
? `${minutes} min ${seconds} sec`
: `${seconds} sec`;
document.getElementById('estimatedTime').innerHTML =
`Estimated time: ${timeStr}<br>Total requests: ${totalRequests}`;
}
// 添加事件监听器到所有可能影响预估时间的输入元素
document.getElementById('maxConcurrent').addEventListener('input', updateEstimatedTime);
document.getElementById('requestDelay').addEventListener('input', updateEstimatedTime);
document.getElementById('searchDepth').addEventListener('change', updateEstimatedTime);
document.getElementById('baseKeyword').addEventListener('input', updateEstimatedTime);
document.getElementById('batchKeywords').addEventListener('input', updateEstimatedTime);
const radioButtons = panel.querySelectorAll('input[name="inputMode"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', (e) => {
document.getElementById('singleInput').style.display =
e.target.value === 'single' ? 'block' : 'none';
document.getElementById('batchInput').style.display =
e.target.value === 'batch' ? 'block' : 'none';
updateEstimatedTime(); // 添加这行来更新预估时间
});
});
}
async function getSuggestions(keyword, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(`https://suggestqueries.google.com/complete/search?client=chrome&q=${encodeURIComponent(keyword)}`);
const data = await response.json();
return data[1];
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000)); // 失败后等待1秒再重试
}
}
}
function updateProgress(current, total, collectedItems) {
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const collectedCount = document.getElementById('collectedCount');
const progress = document.getElementById('progress');
progress.style.display = 'block';
const percentage = (current / total) * 100;
progressBar.style.width = percentage + '%';
progressText.textContent = `${current}/${total}`;
collectedCount.textContent = collectedItems.size;
}
function generateCombinations(letters, depth) {
if (depth === 1) return letters.map(letter => [letter]);
const combinations = [];
for (let i = 0; i < letters.length; i++) {
const subCombinations = generateCombinations(letters.slice(i + 1), depth - 1);
subCombinations.forEach(subComb => {
combinations.push([letters[i], ...subComb]);
});
}
return combinations;
}
async function asyncPool(concurrency, iterable, iteratorFn) {
const ret = []; // 存储所有的异步任务
const executing = new Set(); // 存储正在执行的异步任务
for (const item of iterable) {
const p = Promise.resolve().then(() => iteratorFn(item, ret)); // 创建异步任务
ret.push(p); // 保存新的异步任务
executing.add(p); // 添加到执行集合
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
if (executing.size >= concurrency) {
await Promise.race(executing); // 等待某个任务完成
}
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY)); // 添加请求间隔
}
return Promise.all(ret);
}
async function collectSuggestions(baseKeyword) {
// 获取用户设置的值
MAX_CONCURRENT_REQUESTS = parseInt(document.getElementById('maxConcurrent').value) || 5;
REQUEST_DELAY = parseInt(document.getElementById('requestDelay').value) || 100;
const result = new Set();
const letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
const resultDiv = document.getElementById('result');
const onlyEnglish = document.getElementById('onlyEnglish').checked;
const searchDepth = parseInt(document.getElementById('searchDepth').value);
const isEnglishOnly = (text) => /^[A-Za-z0-9\s.,!?-]+$/.test(text);
if (shouldStop) {
return Array.from(result);
}
// 收集基础关键词建议
const baseSuggestions = await getSuggestions(baseKeyword);
baseSuggestions.forEach(s => {
if (!onlyEnglish || isEnglishOnly(s)) {
result.add(s);
}
});
// 生成所有可能的字母组合
const allCombinations = [];
for (let depth = 1; depth <= searchDepth; depth++) {
const depthCombinations = generateCombinations(letters, depth);
allCombinations.push(...depthCombinations);
}
// 更新进度条的总数
const totalCombinations = allCombinations.length;
let completedCount = 0;
// 创建查询任务
const searchTasks = allCombinations.map(combination => {
return async () => {
if (shouldStop) return [];
const letterCombination = combination.join('');
const suggestions = await getSuggestions(`${baseKeyword} ${letterCombination}`);
completedCount++;
updateProgress(completedCount, totalCombinations, result);
return suggestions.filter(s => !onlyEnglish || isEnglishOnly(s));
};
});
// 建一个固定的 textarea 元素
resultDiv.innerHTML = `<textarea style="width: 100%; height: 200px;"></textarea>`;
const resultTextarea = resultDiv.querySelector('textarea');
// 使用并发池执行查询
const results = await asyncPool(MAX_CONCURRENT_REQUESTS, searchTasks, async (task) => {
const suggestions = await task();
suggestions.forEach(s => result.add(s));
// 保存当前滚动位置
const scrollTop = resultTextarea.scrollTop;
// 更新内容
resultTextarea.value = Array.from(result).join('\n');
// 恢复滚动位置
resultTextarea.scrollTop = scrollTop;
return suggestions;
});
return Array.from(result);
}
function calculateEstimatedTime(keywordCount, searchDepth, maxConcurrent, requestDelay) {
const letters = 'abcdefghijklmnopqrstuvwxyz';
let totalRequests = 0;
// 计算每个关键词的请求数(基础请求 + 字母组合请求)
for (let depth = 1; depth <= searchDepth; depth++) {
// 计算组合数
let combinations = 1;
for (let i = 0; i < depth; i++) {
combinations *= (letters.length - i);
}
for (let i = depth; i > 0; i--) {
combinations = Math.floor(combinations / i);
}
totalRequests += combinations;
}
totalRequests += 1; // 加上基础关键词的请求
totalRequests *= keywordCount; // 乘以关键词数量
// 计算总时长(毫秒)
const avgResponseTime = 300; // 假设平均响应时间为300ms
const batchCount = Math.ceil(totalRequests / maxConcurrent);
const totalTime = batchCount * (avgResponseTime + requestDelay);
return {
totalRequests,
estimatedSeconds: Math.ceil(totalTime / 1000)
};
}
function init() {
addStyles();
createUI();
const startCollectBtn = document.getElementById('startCollect');
startCollectBtn.addEventListener('click', async () => {
if (isCollecting) {
// 如果正在收集,点击按钮则停止
shouldStop = true;
startCollectBtn.textContent = 'start collect';
startCollectBtn.style.background = '#4CAF50';
isCollecting = false;
return;
}
const isBatchMode = document.querySelector('input[name="inputMode"]:checked').value === 'batch';
let keywords = [];
if (isBatchMode) {
const batchText = document.getElementById('batchKeywords').value.trim();
keywords = batchText.split('\n').filter(k => k.trim());
} else {
const singleKeyword = document.getElementById('baseKeyword').value.trim();
if (singleKeyword) {
keywords = [singleKeyword];
}
}
if (keywords.length === 0) {
alert('Please enter a keyword');
return;
}
// 开始收集
isCollecting = true;
shouldStop = false;
startCollectBtn.textContent = 'stop collect';
startCollectBtn.style.background = '#ff4444';
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = 'Collecting...';
document.getElementById('progress').style.display = 'block';
try {
const allSuggestions = new Set();
const totalKeywords = keywords.length;
for (let i = 0; i < keywords.length; i++) {
if (shouldStop) {
break;
}
const keyword = keywords[i];
document.getElementById('totalProgress').textContent = `${i + 1}/${totalKeywords}`;
document.getElementById('totalProgressBar').style.width = `${((i + 1) / totalKeywords) * 100}%`;
const suggestions = await collectSuggestions(keyword);
suggestions.forEach(s => allSuggestions.add(s));
}
const resultText = Array.from(allSuggestions).join('\n');
resultDiv.innerHTML = `
<textarea style="width: 100%; height: 200px;">${resultText}</textarea>
<button id="copyBtn">Copy to Clipboard</button>
`;
document.getElementById('copyBtn').addEventListener('click', () => {
GM_setClipboard(resultText);
alert('Copied to clipboard!');
});
} catch (error) {
resultDiv.innerHTML = 'Error occurred while collecting: ' + error.message;
} finally {
// 恢复按钮状态
isCollecting = false;
shouldStop = false;
startCollectBtn.textContent = 'start collect';
startCollectBtn.style.background = '#4CAF50';
}
});
}
init();
})();