您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collect Google search suggestions
// ==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(); })();