Google Search Suggestions Collector

Collect Google search suggestions

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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