幕客网课程抓取工具(动态分页+按钮触发+Loading)

手动触发抓取课程信息,支持动态分页、图片提取与 loading 效果

目前為 2025-06-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name         幕客网课程抓取工具(动态分页+按钮触发+Loading)
// @namespace    http://tampermonkey.net/
// @version      2.31
// @description  手动触发抓取课程信息,支持动态分页、图片提取与 loading 效果
// @match        https://coding.imooc.com/*
// @grant        none
// @license      none
// ==/UserScript==

(function () {
    'use strict';

    const allCourses = [];

    // 显示 loading 遮罩
    function showLoading() {
        const loading = document.createElement('div');
        loading.id = 'imooc-loading-overlay';
        loading.style.cssText = `
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0, 0, 0, 0.4);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9998;
            font-size: 20px;
            color: white;
            font-weight: bold;
        `;
        loading.innerHTML = '⏳ 正在抓取课程数据,请稍候...';
        document.body.appendChild(loading);
    }

    // 移除 loading 遮罩
    function hideLoading() {
        const loading = document.getElementById('imooc-loading-overlay');
        if (loading) {
            document.body.removeChild(loading);
        }
    }

    // 获取最大页码
    function getTotalPages() {
        const pageLinks = document.querySelectorAll('.page a[href*="page="]');
        const pages = Array.from(pageLinks)
            .map(a => {
                const match = a.href.match(/page=(\d+)/);
                return match ? parseInt(match[1]) : null;
            })
            .filter(n => n !== null);
        return pages.length ? Math.max(...pages) : 1;
    }

    // 获取基础 URL 和分页参数前缀
    function getBaseUrlAndParam() {
        const url = new URL(window.location.href);
        const base = url.origin + url.pathname;
        const searchParams = new URLSearchParams(url.search);
        searchParams.delete('page'); // 去除分页参数
        const paramStr = searchParams.toString();
        return {
            baseUrl: base,
            paramPrefix: paramStr ? `${paramStr}&` : ''
        };
    }

    // 解析页面 HTML,提取课程数据
    function parseHTML(htmlText) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(htmlText, 'text/html');
        const cards = doc.querySelectorAll('li.course-card');
        const pageCourses = [];

        cards.forEach(card => {
            const name = card.getAttribute('data-name');
            const price = card.getAttribute('data-price') + '元';

            const numbersText = card.querySelector('p.one .numbers')?.innerText || '';
            const [level, applyRaw] = numbersText.split('·').map(s => s.trim());
            const apply = applyRaw?.replace('人报名', '') || '';

            const imgDiv = card.querySelector('.img');
            let image = '';
            if (imgDiv) {
                const style = imgDiv.getAttribute('style') || '';
                const match = style.match(/url\((.*?)\)/);
                if (match && match[1]) {
                    image = match[1].startsWith('//') ? 'https:' + match[1] : match[1];
                }
            }

            pageCourses.push({
                name,
                level,
                apply,
                price,
                image
            });
        });

        return pageCourses;
    }

    // 主抓取逻辑
    async function fetchAllPages() {
        allCourses.length = 0;
        showLoading();

        const totalPages = getTotalPages();
        const { baseUrl, paramPrefix } = getBaseUrlAndParam();

        for (let page = 1; page <= totalPages; page++) {
            const url = `${baseUrl}?${paramPrefix}page=${page}`;
            console.log(`📄 抓取第 ${page} 页:${url}`);

            try {
                const res = await fetch(url, { credentials: 'include' });
                const html = await res.text();
                const courses = parseHTML(html);
                allCourses.push(...courses);
            } catch (err) {
                console.error(`❌ 第 ${page} 页请求失败:`, err);
                break;
            }
        }

        console.log('✅ 所有课程抓取完毕,共:', allCourses.length, '条');
        console.table(allCourses);

        // ✅ 替换为你的本地或线上接口地址
        fetch('http://192.168.31.45/data', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(allCourses)
        })
        .then(res => res.json())
        .then(data => console.log('✅ 已发送后端,返回:', data))
        .catch(err => console.error('❌ 发送失败:', err))
        .finally(() => hideLoading());
    }

    // 页面右上角添加按钮
    function addGrabButton() {
        const btn = document.createElement('button');
        btn.textContent = '📦 抓取当前课程列表';
        btn.style.cssText = `
            position: fixed;
            top: 100px;
            right: 20px;
            z-index: 9999;
            background-color: #f60;
            color: white;
            padding: 10px 16px;
            font-size: 14px;
            border: none;
            border-radius: 6px;
            box-shadow: 0 0 6px rgba(0,0,0,0.2);
            cursor: pointer;
        `;
        btn.onclick = fetchAllPages;
        document.body.appendChild(btn);
    }

    window.addEventListener('load', () => {
        setTimeout(addGrabButton, 1000);
    });
})();