Greasy Fork

Greasy Fork is available in English.

小红书点赞数高亮和动态过滤

动态高亮笔记和按点赞数过滤内容,点击选择显示大于特定点赞数的内容,不选中的变模糊,支持一键下载所有浏览过的内容

// ==UserScript==
// @name         小红书点赞数高亮和动态过滤
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  动态高亮笔记和按点赞数过滤内容,点击选择显示大于特定点赞数的内容,不选中的变模糊,支持一键下载所有浏览过的内容
// @author       Your name
// @match        https://www.xiaohongshu.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 存储所有已加载的笔记数据,使用Map以支持高效去重
    const allLoadedNotes = new Map();

    // 将带"万"的数字转换为实际数值
    function parseCount(str) {
        if (typeof str !== 'string') return 0;
        if (str.includes('万')) {
            return parseFloat(str.replace('万', '')) * 10000;
        }
        return parseInt(str) || 0;
    }

    // 定义颜色阈值和对应的颜色以及发光效果
    const colorRules = [
        { threshold: 10000, color: '#FF4500', glow: '0 0 15px #FF4500, 0 0 30px #FF4500' },
        { threshold: 5000, color: '#FF6347', glow: '0 0 15px #FF6347, 0 0 30px #FF6347' },
        { threshold: 1000, color: '#FFD700', glow: '0 0 15px #FFD700, 0 0 30px #FFD700' },
        { threshold: 500, color: '#32CD32', glow: '0 0 15px #32CD32, 0 0 30px #32CD32' },
        { threshold: 100, color: '#00BFFF', glow: '0 0 15px #00BFFF, 0 0 30px #00BFFF' },
        { threshold: 0, color: '#00CED1', glow: '0 0 15px #00CED1, 0 0 30px #00CED1' }
    ];

    // 当前选中的点赞数阈值
    let currentThreshold = 0;

    // 添加样式
    function addGlobalStyle() {
        const styleElement = document.createElement('style');
        styleElement.innerHTML = `
            .xhs-note-filtered {
                filter: blur(3px);
                opacity: 0.5;
                transition: all 0.3s ease;
                pointer-events: none;
            }

            .xhs-note-active {
                filter: none;
                opacity: 1;
                transition: all 0.3s ease;
                pointer-events: auto;
            }

            .xhs-filter-container {
                position: fixed;
                top: 70px;
                right: 20px;
                z-index: 9999;
                background-color: white;
                border-radius: 8px;
                box-shadow: 0 0 8px rgba(0,0,0,0.3);
                padding: 10px;
                font-size: 14px;
                display: flex;
                flex-direction: column;
                gap: 5px;
            }

            .xhs-download-button {
                position: fixed;
                top: 20px;
                right: 20px;
                z-index: 9999;
                background-color: #ff2442;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 10px 15px;
                font-size: 14px;
                cursor: pointer;
                box-shadow: 0 0 8px rgba(0,0,0,0.3);
                transition: all 0.2s;
            }

            .xhs-download-button:hover {
                background-color: #e61e3c;
                box-shadow: 0 0 12px rgba(0,0,0,0.4);
            }

            .xhs-counter {
                position: fixed;
                top: 20px;
                right: 180px;
                z-index: 9999;
                background-color: rgba(0, 0, 0, 0.7);
                color: white;
                border-radius: 4px;
                padding: 10px 15px;
                font-size: 14px;
                pointer-events: none;
            }
        `;
        document.head.appendChild(styleElement);
    }

    function applyFilter() {
        const items = document.querySelectorAll('.note-item');

        items.forEach(item => {
            const countElement = item.querySelector('.count');
            const likes = parseCount(countElement?.textContent || '0');

            // 不隐藏元素,只应用模糊效果
            if (likes >= currentThreshold) {
                item.classList.add('xhs-note-active');
                item.classList.remove('xhs-note-filtered');
            } else {
                item.classList.add('xhs-note-filtered');
                item.classList.remove('xhs-note-active');
            }
        });
    }

    // 添加或更新已加载笔记计数器
    function updateNotesCounter() {
        let counter = document.querySelector('.xhs-counter');
        if (!counter) {
            counter = document.createElement('div');
            counter.className = 'xhs-counter';
            document.body.appendChild(counter);
        }

        counter.textContent = `已捕获 ${allLoadedNotes.size} 条笔记`;
    }

    // 创建过滤控件
    function createFilterControl() {
        // 检查是否已存在过滤器
        if (document.querySelector('.xhs-filter-container')) {
            return;
        }

        const filterContainer = document.createElement('div');
        filterContainer.className = 'xhs-filter-container';

        const filters = [
            { label: '显示全部 (0+)', value: 0 },
            { label: '100+ 点赞', value: 100 },
            { label: '500+ 点赞', value: 500 },
            { label: '1000+ 点赞', value: 1000 },
            { label: '5000+ 点赞', value: 5000 },
            { label: '10000+ 点赞', value: 10000 }
        ];

        const title = document.createElement('div');
        title.textContent = '按点赞数过滤';
        title.style.fontWeight = 'bold';
        title.style.marginBottom = '5px';
        title.style.textAlign = 'center';
        filterContainer.appendChild(title);

        filters.forEach(filter => {
            const button = document.createElement('button');
            button.textContent = filter.label;
            button.style.cssText = `
                padding: 5px 10px;
                border: 1px solid #ccc;
                border-radius: 4px;
                background-color: ${filter.value === currentThreshold ? '#ff2442' : 'white'};
                color: ${filter.value === currentThreshold ? 'white' : 'black'};
                cursor: pointer;
                transition: all 0.2s;
                margin-bottom: 3px;
                width: 100%;
            `;

            button.onclick = () => {
                currentThreshold = filter.value;
                // 更新所有按钮的样式
                filterContainer.querySelectorAll('button').forEach(btn => {
                    const btnValue = parseInt(btn.dataset.value);
                    btn.style.backgroundColor = btnValue === currentThreshold ? '#ff2442' : 'white';
                    btn.style.color = btnValue === currentThreshold ? 'white' : 'black';
                });
                applyFilter();
            };

            button.dataset.value = filter.value;
            filterContainer.appendChild(button);
        });

        document.body.appendChild(filterContainer);
        console.log("过滤控件已创建");
    }

    // 捕获笔记信息并存储到集合中
    function captureNoteData(noteElement) {
        try {
            // 提取用户名
            let username = '未知用户';
            const nameElement = noteElement.querySelector('.name, .author-name');
            if (nameElement) {
                username = nameElement.textContent.trim();
            }

            // 提取点赞数
            let likes = '0';
            const countElement = noteElement.querySelector('.count, .like-count');
            if (countElement) {
                likes = countElement.textContent.trim();
            }

            // 提取描述/标题
            let description = '无描述';
            const titleElement = noteElement.querySelector('.title span, .desc, .description');
            if (titleElement) {
                description = titleElement.textContent.trim();
            }

            // 提取笔记ID (替代笔记链接)
            let noteId = '';
            // 从隐藏链接中提取ID
            const hiddenLinkElement = noteElement.querySelector('a[href^="/explore/"][style*="display: none"], a[href^="/search_result/"][style*="display: none"]');
            if (hiddenLinkElement && hiddenLinkElement.getAttribute('href')) {
                const href = hiddenLinkElement.getAttribute('href');
                noteId = href.split('/')[2]?.split('?')[0] || '';
            }

            // 如果没找到隐藏链接,尝试可见链接
            if (!noteId) {
                const visibleLinkElement = noteElement.querySelector('a.cover[href^="/search_result/"], a.cover[href^="/explore/"]');
                if (visibleLinkElement && visibleLinkElement.getAttribute('href')) {
                    const href = visibleLinkElement.getAttribute('href');
                    noteId = href.split('/')[2]?.split('?')[0] || '';
                }
            }

            // 提取用户资料链接
            let userProfileLink = '';
            const authorElement = noteElement.querySelector('a.author[href^="/user/profile/"]');
            if (authorElement && authorElement.getAttribute('href')) {
                const href = authorElement.getAttribute('href');
                const userId = href.split('/')[3]?.split('?')[0];
                if (userId) {
                    userProfileLink = `https://www.xiaohongshu.com/user/profile/${userId}`;
                }
            }

            // 提取封面图链接
            let coverImageUrl = '';
            const imageElement = noteElement.querySelector('img[data-xhs-img]');
            if (imageElement && imageElement.getAttribute('src')) {
                coverImageUrl = imageElement.getAttribute('src');
            }

            // 判断是否为视频
            const isVideo = !!noteElement.querySelector('.play-icon');

            if (description !== '无描述' || username !== '未知用户') {
                // 存储数据时,用noteId替代noteLink
                allLoadedNotes.set(noteId, {
                    username,
                    likes,
                    description,
                    noteId,  // 使用ID代替链接
                    userProfileLink,
                    coverImageUrl,
                    isVideo: isVideo ? '是' : '否',
                    likesCount: parseCount(likes)
                });
            }
        } catch (error) {
            console.error('捕获笔记数据时出错:', error);
        }
    }


    // 扫描页面上的所有笔记
    function scanAllNotes() {
        // 尝试多种可能的笔记元素选择器
        const possibleNoteSelectors = [
            '.note-item',
            '.feed-item',
            '[data-v-a264b01a]',
            '.feed-card',
            '.note',
            '.explore-feed-card',
            '.card'
        ];

        let notesFound = false;

        for (const selector of possibleNoteSelectors) {
            const notes = document.querySelectorAll(selector);
            if (notes.length > 0) {
                notes.forEach(note => captureNoteData(note));
                notesFound = true;
                // 继续尝试其他选择器,以确保捕获所有可能的笔记
            }
        }

        if (notesFound) {
            updateNotesCounter();
        }
    }

    // 高亮点赞数
    function highlightLikes() {
        const items = document.querySelectorAll('.note-item');

        items.forEach(item => {
            const countElement = item.querySelector('.count');
            if (!countElement) return;

            const likes = parseCount(countElement.textContent || '0');

            // 高亮处理
            for (let rule of colorRules) {
                if (likes >= rule.threshold) {
                    item.style.boxShadow = rule.glow;
                    item.style.border = `2px solid ${rule.color}`;
                    item.style.borderRadius = '10px';
                    item.style.transition = 'box-shadow 0.3s ease-in-out, border 0.3s ease-in-out';

                    // 增强显示点赞数
                    countElement.style.fontWeight = 'bold';
                    countElement.style.color = rule.color;
                    break;
                }
            }
        });
    }

    // 创建下载按钮
    function createDownloadButton() {
        // 检查是否已存在下载按钮
        if (document.querySelector('.xhs-download-button')) {
            return;
        }

        const downloadButton = document.createElement('button');
        downloadButton.className = 'xhs-download-button';
        downloadButton.textContent = '下载所有笔记';
        downloadButton.addEventListener('click', downloadCapturedNotes);
        document.body.appendChild(downloadButton);
    }

    // 下载已捕获的所有笔记
    function downloadCapturedNotes() {
        if (allLoadedNotes.size === 0) {
            alert('还没有捕获到任何笔记数据。请滚动页面以加载更多内容。');
            return;
        }

        let notesToExport = Array.from(allLoadedNotes.values());

        // 应用当前的过滤条件(如果有)
        if (currentThreshold > 0) {
            notesToExport = notesToExport.filter(note => note.likesCount >= currentThreshold);

            if (notesToExport.length === 0) {
                alert(`没有找到符合当前过滤条件(${currentThreshold}+ 点赞)的笔记。`);
                return;
            }
        }

        // 按点赞数从高到低排序
        notesToExport.sort((a, b) => b.likesCount - a.likesCount);

        // 在downloadCapturedNotes函数中修改CSV头和内容处理部分
        let csvContent = '用户名,点赞数(原始),点赞数(数值),描述,笔记ID,用户主页,是否视频,封面图链接\n';
        notesToExport.forEach(note => {
            // 处理CSV中的特殊字符,确保不会破坏CSV格式
            const username = `"${note.username.replace(/"/g, '""')}"`;
            const description = `"${note.description.replace(/"/g, '""')}"`;

            // 添加两列点赞数:原始文本形式和数值形式
            csvContent += `${username},${note.likes},${note.likesCount},${description},${note.noteId},${note.userProfileLink},${note.isVideo},${note.coverImageUrl}\n`;
        });

        // 添加UTF-8 BOM以支持中文
        const BOM = '\uFEFF';
        const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8' });
        const url = URL.createObjectURL(blob);

        // 创建下载链接并触发下载
        const link = document.createElement('a');
        link.setAttribute('href', url);
        link.setAttribute('download', `小红书内容_${currentThreshold}+点赞_${notesToExport.length}条_${new Date().toLocaleDateString().replace(/\//g, '-')}.csv`);
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);

        // 释放URL对象
        setTimeout(() => {
            URL.revokeObjectURL(url);
        }, 100);

        console.log(`已下载 ${notesToExport.length} 条笔记数据`);
    }

    // 初始化
    function init() {
        console.log("小红书点赞数高亮和动态过滤脚本初始化中...");
        // 添加全局样式
        addGlobalStyle();

        // 添加一些延迟以确保页面元素已加载
        setTimeout(() => {
            createFilterControl();
            createDownloadButton();
            updateNotesCounter();
            highlightLikes();
            applyFilter();

            // 初始扫描
            scanAllNotes();
        }, 1000);

        // 监听页面滚动,用于捕获动态加载的笔记
        let scrollTimeout;
        window.addEventListener('scroll', () => {
            // 使用节流技术减少处理频率
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(() => {
                scanAllNotes();
            }, 300);
        });

        // 观察DOM变化以处理动态内容
        const observer = new MutationObserver((mutations) => {
            // 性能优化:检查是否有相关元素变化
            let hasRelevantChanges = false;
            let newNotes = [];

            for (const mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1) {
                            // 收集所有可能的笔记元素
                            if (node.classList &&
                                (node.classList.contains('note-item') ||
                                 node.classList.contains('feed-item'))) {
                                newNotes.push(node);
                                hasRelevantChanges = true;
                            } else {
                                // 检查子元素
                                const childNotes = node.querySelectorAll('.note-item, .feed-item, [data-v-a264b01a]');
                                if (childNotes.length > 0) {
                                    newNotes.push(...childNotes);
                                    hasRelevantChanges = true;
                                }
                            }
                        }
                    }
                }
            }

            if (hasRelevantChanges) {
                highlightLikes();
                applyFilter();

                // 捕获新加载的笔记数据
                newNotes.forEach(note => captureNoteData(note));

                // 如果找到了新笔记,更新计数器
                if (newNotes.length > 0) {
                    updateNotesCounter();
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 定期扫描页面,确保不会漏掉任何笔记
        setInterval(scanAllNotes, 5000);
    }

    // 等待页面加载完毕
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();