Metrics Extractor for Xiaohongshu Note Manager

在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数)。

目前為 2024-12-13 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Metrics Extractor for Xiaohongshu Note Manager
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数)。
// @author       您的名字
// @match        https://creator.xiaohongshu.com/new/note-manager*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 防止脚本多次运行
    if (document.getElementById('metrics-extractor-container')) {
        console.log('Metrics Extractor 已经运行,退出脚本。');
        return;
    }

    const metrics = ['浏览量', '评论数', '点赞数', '收藏数', '转发数'];
    let categorizedData = [];
    let totals = {
        '浏览量': 0,
        '评论数': 0,
        '点赞数': 0,
        '收藏数': 0,
        '转发数': 0
    };

    /**
     * 提取指标数据
     * @returns {boolean} 是否成功提取数据
     */
    function extractMetrics() {
        console.log('开始提取指标数据...');

        // 使用更精确的选择器,选择div.icon的直接子span
        const iconDivs = document.querySelectorAll('div.icon, div[class*="icon"]');
        console.log('找到的div.icon数量:', iconDivs.length);
        const numbers = [];

        iconDivs.forEach((div, index) => {
            // 使用 :scope > span 选择器,确保选择div.icon的直接子span
            const span = div.querySelector(':scope > span');
            if (span) {
                // 使用 innerText 代替 textContent
                const numberText = span.innerText.trim();
                console.log(`div.icon #${index + 1} - 提取到的span文本: "${numberText}"`);
                
                // 检查是否有数字
                const digitMatch = numberText.match(/\d+/g);
                if (digitMatch) {
                    // 将所有找到的数字部分连接起来
                    const cleanedNumber = digitMatch.join('');
                    console.log(`div.icon #${index + 1} - 清理后的数字字符串: "${cleanedNumber}"`);
                    
                    const number = parseInt(cleanedNumber, 10);
                    if (!isNaN(number)) {
                        numbers.push(number);
                        console.log(`div.icon #${index + 1} - 解析后的数字: ${number}`);
                    } else {
                        console.warn(`div.icon #${index + 1} - 无法解析数字: "${cleanedNumber}"`);
                    }
                } else {
                    console.warn(`div.icon #${index + 1} - span内未找到数字`);
                }
            } else {
                console.warn(`div.icon #${index + 1} - 未找到span元素`);
            }
        });

        console.log('提取到的数字数组:', numbers);

        if (numbers.length === 0) {
            console.warn('未提取到任何数字。');
            return false;
        }

        if (numbers.length % 5 !== 0) {
            console.warn('提取的数字数量不是5的倍数,可能存在数据缺失或多余。');
        }

        categorizedData = [];
        for (let i = 0; i < numbers.length; i += 5) {
            const item = {};
            metrics.forEach((metric, index) => {
                if (i + index < numbers.length) {
                    item[metric] = numbers[i + index];
                } else {
                    item[metric] = null;
                }
            });
            categorizedData.push(item);
        }

        console.log('分类后的数据:', categorizedData);

        // 计算总数
        totals = {
            '浏览量': 0,
            '评论数': 0,
            '点赞数': 0,
            '收藏数': 0,
            '转发数': 0
        };
        categorizedData.forEach(item => {
            metrics.forEach(metric => {
                const value = item[metric];
                if (typeof value === 'number') {
                    totals[metric] += value;
                }
            });
        });

        console.log('各指标总数:', totals);

        // 创建或更新面板
        if (document.getElementById('metrics-extractor-container')) {
            updateMetricsPanel();
        } else {
            createMetricsPanel();
        }

        return true;
    }

    /**
     * 创建指标展示面板
     */
    function createMetricsPanel() {
        console.log('创建指标面板...');
        // 创建样式
        const style = document.createElement('style');
        style.innerHTML = `
        #metrics-extractor-container {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 320px;
            background: rgba(255, 255, 255, 0.95);
            border: 1px solid #ccc;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            padding: 15px;
            z-index: 10000;
            font-family: Arial, sans-serif;
            max-height: 90vh;
            overflow-y: auto;
        }
        #metrics-extractor-container h2 {
            text-align: center;
            margin-top: 0;
            font-size: 18px;
        }
        #metrics-extractor-container table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 15px;
        }
        #metrics-extractor-container th, #metrics-extractor-container td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: center;
            font-size: 14px;
        }
        #metrics-extractor-container th {
            background-color: #f2f2f2;
        }
        #metrics-extractor-container button {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-bottom: 10px;
        }
        #metrics-extractor-container button:hover {
            background-color: #45a049;
        }
        #metrics-extractor-details {
            display: none;
            max-height: 300px;
            overflow-y: auto;
        }
        `;
        document.head.appendChild(style);

        // 创建容器
        const container = document.createElement('div');
        container.id = 'metrics-extractor-container';

        // 创建标题
        const title = document.createElement('h2');
        title.textContent = '指标总数';
        container.appendChild(title);

        // 创建总数表格
        const totalsTable = document.createElement('table');
        const totalsThead = document.createElement('thead');
        const totalsHeaderRow = document.createElement('tr');
        const metricHeader = document.createElement('th');
        metricHeader.textContent = '指标';
        const totalHeader = document.createElement('th');
        totalHeader.textContent = '总数';
        totalsHeaderRow.appendChild(metricHeader);
        totalsHeaderRow.appendChild(totalHeader);
        totalsThead.appendChild(totalsHeaderRow);
        totalsTable.appendChild(totalsThead);

        const totalsTbody = document.createElement('tbody');
        for (const [metric, total] of Object.entries(totals)) {
            const row = document.createElement('tr');
            const metricCell = document.createElement('td');
            metricCell.textContent = metric;
            const totalCell = document.createElement('td');
            totalCell.textContent = total;
            row.appendChild(metricCell);
            row.appendChild(totalCell);
            totalsTbody.appendChild(row);
        }
        totalsTable.appendChild(totalsTbody);
        container.appendChild(totalsTable);

        // 创建按钮
        const toggleButton = document.createElement('button');
        toggleButton.id = 'metrics-extractor-toggle';
        toggleButton.textContent = '显示详细数据';
        container.appendChild(toggleButton);

        // 创建详细数据部分
        const detailsSection = document.createElement('div');
        detailsSection.id = 'metrics-extractor-details';

        const detailsTitle = document.createElement('h2');
        detailsTitle.textContent = '详细数据';
        detailsSection.appendChild(detailsTitle);

        const detailsTable = document.createElement('table');
        const detailsThead = document.createElement('thead');
        const detailsHeaderRow = document.createElement('tr');
        const projectHeader = document.createElement('th');
        projectHeader.textContent = '项目';
        detailsHeaderRow.appendChild(projectHeader);
        metrics.forEach(metric => {
            const th = document.createElement('th');
            th.textContent = metric;
            detailsHeaderRow.appendChild(th);
        });
        detailsThead.appendChild(detailsHeaderRow);
        detailsTable.appendChild(detailsThead);

        const detailsTbody = document.createElement('tbody');
        categorizedData.forEach((item, index) => {
            const row = document.createElement('tr');
            const projectCell = document.createElement('td');
            projectCell.textContent = index + 1;
            row.appendChild(projectCell);
            metrics.forEach(metric => {
                const cell = document.createElement('td');
                cell.textContent = item[metric] !== null ? item[metric] : '-';
                row.appendChild(cell);
            });
            detailsTbody.appendChild(row);
        });
        detailsTable.appendChild(detailsTbody);
        detailsSection.appendChild(detailsTable);

        container.appendChild(detailsSection);
        document.body.appendChild(container);

        // 添加按钮点击事件
        toggleButton.addEventListener('click', () => {
            if (detailsSection.style.display === 'none' || detailsSection.style.display === '') {
                detailsSection.style.display = 'block';
                toggleButton.textContent = '隐藏详细数据';
            } else {
                detailsSection.style.display = 'none';
                toggleButton.textContent = '显示详细数据';
            }
        });

        console.log('指标面板已创建。');
    }

    /**
     * 更新指标展示面板的数据
     */
    function updateMetricsPanel() {
        console.log('更新指标面板数据...');
        const totalsTableBody = document.querySelector('#metrics-extractor-container table tbody');
        const detailsTableBody = document.querySelector('#metrics-extractor-details table tbody');

        // 更新总数表格
        totalsTableBody.innerHTML = '';
        for (const [metric, total] of Object.entries(totals)) {
            const row = document.createElement('tr');
            const metricCell = document.createElement('td');
            metricCell.textContent = metric;
            const totalCell = document.createElement('td');
            totalCell.textContent = total;
            row.appendChild(metricCell);
            row.appendChild(totalCell);
            totalsTableBody.appendChild(row);
        }

        // 更新详细数据表格
        detailsTableBody.innerHTML = '';
        categorizedData.forEach((item, index) => {
            const row = document.createElement('tr');
            const projectCell = document.createElement('td');
            projectCell.textContent = index + 1;
            row.appendChild(projectCell);
            metrics.forEach(metric => {
                const cell = document.createElement('td');
                cell.textContent = item[metric] !== null ? item[metric] : '-';
                row.appendChild(cell);
            });
            detailsTableBody.appendChild(row);
        });

        console.log('指标面板数据已更新。');
    }

    /**
     * 等待指定的条件满足
     * @param {Function} conditionFunction - 返回布尔值的函数,表示条件是否满足
     * @param {number} timeout - 最大等待时间(毫秒)
     * @param {number} interval - 检查间隔时间(毫秒)
     * @returns {Promise<boolean>} 条件是否在超时前满足
     */
    function waitFor(conditionFunction, timeout = 30000, interval = 500) { // 延长超时时间至30秒
        return new Promise((resolve) => {
            const startTime = Date.now();
            const checkCondition = () => {
                if (conditionFunction()) {
                    resolve(true);
                } else if (Date.now() - startTime >= timeout) {
                    resolve(false);
                } else {
                    setTimeout(checkCondition, interval);
                }
            };
            checkCondition();
        });
    }

    /**
     * 初始化脚本
     */
    async function init() {
        console.log('Metrics Extractor 脚本初始化...');
        // 使用更精确的选择器,等待至少2个div.icon元素存在且其span内有内容
        const elementsLoaded = await waitFor(() => {
            const icons = document.querySelectorAll('div.icon, div[class*="icon"]');
            if (icons.length >= 2) {
                return Array.from(icons).every(div => {
                    const span = div.querySelector(':scope > span');
                    return span && span.innerText.trim() !== '';
                });
            }
            return false;
        }, 30000, 500); // 延长超时时间

        if (!elementsLoaded) {
            console.warn('等待目标元素超时,未找到足够的div.icon元素或span内无内容。');
            return;
        }

        const extractionSuccess = extractMetrics();
        if (!extractionSuccess) {
            console.warn('提取指标数据失败。');
            return;
        }

        console.log('Metrics Extractor 脚本运行完毕。');
    }

    // 运行初始化函数
    init();

})();