Google Search Result Extractor

Automatically extract Google search results (Title, URL) and navigate to the next page.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Google Search Result Extractor
// @name:zh-CN   谷歌搜索结果自动提取器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Automatically extract Google search results (Title, URL) and navigate to the next page.
// @description:zh-CN 自动提取谷歌搜索结果的标题和链接,并消耗自动翻页提取至最后一页。
// @author       YourName
// @match        https://www.google.com/search*
// @match        https://www.google.com.hk/search*
// @match        https://www.google.ad/search*
// @match        https://www.google.ae/search*
// @match        https://www.google.com.tw/search*
// @include      /^https:\/\/www\.google\..*\/search.*/
// @grant        GM_setClipboard
// @grant        GM_notification
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // 1. UI 面板构建
    const createUI = () => {
        const panel = document.createElement('div');
        panel.id = 'gs-extractor-panel';
        panel.style = `
            position: fixed; top: 20px; left: 20px; z-index: 10001;
            background: #ffffff; border: 1px solid #4285f4;
            padding: 15px; border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.15);
            width: 210px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        `;
        panel.innerHTML = `
            <div style="font-weight: bold; color: #4285f4; margin-bottom: 10px; font-size: 16px; border-bottom: 1px solid #eee; padding-bottom: 5px;">🔍 提取助手</div>
            <div style="margin-bottom: 12px; font-size: 13px;">已保存: <b id="gs-count" style="color: #d93025;">0</b> 条数据</div>
            <button id="gs-start" style="width: 100%; padding: 10px; background: #4285f4; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; margin-bottom: 8px; transition: 0.3s;">🚀 开始自动提取</button>
            <button id="gs-copy" style="width: 100%; padding: 8px; background: #34a853; color: white; border: none; border-radius: 6px; cursor: pointer; margin-bottom: 8px;">📋 复制 (Excel格式)</button>
            <button id="gs-clear" style="width: 100%; padding: 6px; background: #fff; color: #666; border: 1px solid #ccc; border-radius: 6px; cursor: pointer; font-size: 12px;">🗑️ 清空缓存</button>
            <div id="gs-loader" style="display:none; color: #ea4335; font-size: 11px; text-align: center; margin-top: 8px; font-style: italic;">正在自动翻页中...</div>
        `;
        document.body.appendChild(panel);
    };

    // 2. 数据处理逻辑
    const getData = () => JSON.parse(localStorage.getItem('gs_ext_data') || '[]');
    const setData = (data) => localStorage.setItem('gs_ext_data', JSON.stringify(data));

    const updateUIStatus = () => {
        const count = getData().length;
        const countEl = document.getElementById('gs-count');
        if (countEl) countEl.innerText = count;
    };

    const extract = () => {
        let currentData = getData();
        const results = document.querySelectorAll('h3');
        let added = 0;

        results.forEach(h3 => {
            const a = h3.closest('a');
            if (a && a.href && !a.href.includes('google.com')) {
                const url = a.href;
                if (!currentData.some(item => item.url === url)) {
                    currentData.push({ title: h3.innerText.replace(/\s+/g, ' '), url: url });
                    added++;
                }
            }
        });

        setData(currentData);
        updateUIStatus();
        return added;
    };

    const runAuto = () => {
        document.getElementById('gs-loader').style.display = 'block';
        document.getElementById('gs-start').innerText = "⏹ 停止提取";
        localStorage.setItem('gs_is_running', 'true');

        extract();

        const nextBtn = document.querySelector('a#pnnext');
        if (nextBtn) {
            // 随机延迟 2.5s - 4.5s 保护账号
            const delay = Math.floor(Math.random() * 2000) + 2500;
            setTimeout(() => {
                if(localStorage.getItem('gs_is_running') === 'true') nextBtn.click();
            }, delay);
        } else {
            stopAuto();
            alert("✅ 提取完毕!已到达最后一页。");
        }
    };

    const stopAuto = () => {
        localStorage.setItem('gs_is_running', 'false');
        const startBtn = document.getElementById('gs-start');
        if (startBtn) {
            startBtn.innerText = "🚀 开始自动提取";
            document.getElementById('gs-loader').style.display = 'none';
        }
    };

    // 3. 事件挂载
    const init = () => {
        createUI();
        updateUIStatus();

        document.getElementById('gs-start').onclick = () => {
            if (localStorage.getItem('gs_is_running') === 'true') {
                stopAuto();
            } else {
                runAuto();
            }
        };

        document.getElementById('gs-copy').onclick = () => {
            const data = getData();
            if (data.length === 0) return alert("没有数据可供复制");
            const output = data.map(i => `${i.title}\t${i.url}`).join('\n');
            
            // 使用 GM_setClipboard 确保兼容性
            navigator.clipboard.writeText(output).then(() => {
                alert("已成功复制到剪贴板!");
            });
        };

        document.getElementById('gs-clear').onclick = () => {
            if (confirm("确定清除所有本地缓存的数据吗?")) {
                localStorage.removeItem('gs_ext_data');
                stopAuto();
                updateUIStatus();
            }
        };

        // 自动续接逻辑
        if (localStorage.getItem('gs_is_running') === 'true') {
            setTimeout(runAuto, 1500);
        }
    };

    // 启动
    if (document.readyState === 'complete') init();
    else window.addEventListener('load', init);

})();