Metrics Extractor for Xiaohongshu Note Manager

聪明豆专属!!在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并每隔5秒自动更新。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Metrics Extractor for Xiaohongshu Note Manager
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  聪明豆专属!!在小红书创作者后台的笔记管理页面提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并每隔5秒自动更新。
// @author       zhongjing du
// @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_list 内的 div.icon 元素
        const iconDivs = document.querySelectorAll('div.icon_list div.icon');
        console.log('找到的div.icon数量:', iconDivs.length);
        let numbers = [];

        iconDivs.forEach((div, index) => {
            if (!div.dataset.processed) { // 检查是否已处理
                // 使用 :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}`);
                            // 标记为已处理
                            div.dataset.processed = 'true';
                        } else {
                            console.warn(`div.icon #${index + 1} - 无法解析数字: "${cleanedNumber}"`);
                        }
                    } else {
                        console.warn(`div.icon #${index + 1} - span内未找到数字`);
                    }
                } else {
                    console.warn(`div.icon #${index + 1} - 未找到span元素`);
                }
            } else {
                console.log(`div.icon #${index + 1} - 已处理,跳过。`);
            }
        });

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

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

        // 分类数据,每5个数字对应一个数据项
        for (let i = 0; i < numbers.length; i += metrics.length) {
            const item = {};
            metrics.forEach((metric, idx) => {
                if (i + idx < numbers.length) {
                    item[metric] = numbers[i + idx];
                } else {
                    item[metric] = null;
                }
            });
            categorizedData.push(item);
        }

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

        // 计算总数
        metrics.forEach(metric => {
            totals[metric] = categorizedData.reduce((sum, item) => {
                return sum + (item[metric] || 0);
            }, 0);
        });

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

        // 更新面板
        updateMetricsPanel();

        return true;
    }

    /**
     * 创建指标展示面板
     */
    function createMetricsPanel() {
        console.log('创建指标面板...');
        // 创建样式
        const style = document.createElement('style');
        style.innerHTML = `
        #metrics-extractor-container {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 350px;
            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');
        detailsTable.appendChild(detailsTbody);
        detailsSection.appendChild(detailsTable);

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

        // 创建导出按钮
        const exportButton = document.createElement('button');
        exportButton.textContent = '导出CSV';
        container.appendChild(exportButton);

        // 添加导出按钮点击事件
        exportButton.addEventListener('click', () => {
            let csvContent = '项目,浏览量,评论数,点赞数,收藏数,转发数\n';
            categorizedData.forEach((item, index) => {
                csvContent += `${index + 1},${item['浏览量']},${item['评论数']},${item['点赞数']},${item['收藏数']},${item['转发数']}\n`;
            });

            const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'metrics_data.csv';
            a.click();
            URL.revokeObjectURL(url);
        });

        // 添加按钮点击事件
        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 脚本初始化...');
        // 等待 div.icon_list 内至少有 1 个 div.icon 元素,并且其 span 内有内容
        const elementsLoaded = await waitFor(() => {
            const icons = document.querySelectorAll('div.icon_list div.icon');
            if (icons.length >= 1) { // 根据实际情况调整
                return Array.from(icons).every(div => {
                    const span = div.querySelector(':scope > span');
                    return span && span.innerText.trim() !== '';
                });
            }
            return false;
        }, 30000, 500); // 延长超时时间至30秒

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

        // 创建指标面板
        createMetricsPanel();

        // 提取初始数据
        extractMetrics();

        // 设置定时器,每5秒重新检测并提取数据
        setInterval(() => {
            console.log('定时提取指标数据...');
            extractMetrics();
        }, 5000); // 每5秒执行一次

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

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

})();