您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在任何页面上通过一个可交互的、可拖动的界面,输入多个CSS选择器,预览提取的数据,并将匹配到的内容导出为CSV表格文件。
// ==UserScript== // @name 网页列表数据提取器 (增强版 - 带预览功能) // @namespace http://tampermonkey.net/ // @version 1.1.1 // @description 在任何页面上通过一个可交互的、可拖动的界面,输入多个CSS选择器,预览提取的数据,并将匹配到的内容导出为CSV表格文件。 // @author Kamjin3086 // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // 1. 注册一个菜单命令,用来打开提取器界面 GM_registerMenuCommand('打开内容提取器', toggleExtractorPanel); // 2. 为界面添加CSS样式 GM_addStyle(` #gm-extractor-panel { position: fixed; top: 60px; right: 15px; width: 450px; background-color: #ffffff; border: 2px solid #4A90E2; border-radius: 10px; z-index: 99999; box-shadow: 0 6px 20px rgba(0,0,0,0.15); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: none; flex-direction: column; max-height: 90vh; overflow: hidden; } #gm-extractor-panel .extractor-header { padding: 12px 16px; background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); color: white; font-weight: 600; font-size: 15px; border-top-left-radius: 8px; border-top-right-radius: 8px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } #gm-extractor-panel .header-buttons { display: flex; align-items: center; gap: 8px; } #gm-extractor-panel .header-btn { background-color: rgba(255, 255, 255, 0.2); border: none; color: white; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background-color 0.2s ease; } #gm-extractor-panel .header-btn:hover { background-color: rgba(255, 255, 255, 0.3); } #gm-extractor-panel .extractor-body { padding: 12px 16px; flex: 1; overflow-y: auto; background-color: #fafafa; } #gm-extractor-panel details { border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 12px; background-color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } #gm-extractor-panel summary { padding: 10px 12px; font-weight: 600; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); cursor: pointer; outline: none; border-radius: 6px; transition: all 0.2s ease; font-size: 14px; } #gm-extractor-panel summary:hover { background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); } #gm-extractor-panel .instructions { padding: 16px; font-size: 14px; line-height: 1.6; background: white; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } #gm-extractor-panel .instructions ul { padding-left: 20px; margin: 0; } #gm-extractor-panel .instructions code { background-color: #f1f3f4; padding: 3px 6px; border-radius: 4px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; color: #d63384; } #gm-extractor-panel .instructions li { margin-bottom: 10px; } #gm-extractor-panel .instructions .pro-tip { border-top: 1px dashed #dee2e6; margin-top: 15px; padding-top: 15px; font-size: 13px; color: #6c757d; background-color: #f8f9fa; padding: 12px; border-radius: 6px; } #gm-extractor-panel .selector-optimize { margin-top: 8px; padding: 10px 12px; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; font-size: 12px; color: #856404; } #gm-extractor-panel .optimize-btn { background-color: #17a2b8; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 11px; margin-left: 8px; } #gm-extractor-panel .optimize-btn:hover { background-color: #138496; } #gm-extractor-panel .optimize-options { margin-top: 8px; padding: 8px; background-color: #f8f9fa; border-radius: 4px; border: 1px solid #dee2e6; } #gm-extractor-panel .optimize-option { display: inline-block; margin: 2px 4px 2px 0; padding: 4px 8px; background-color: #e9ecef; border: 1px solid #ced4da; border-radius: 3px; cursor: pointer; font-size: 11px; transition: all 0.2s ease; } #gm-extractor-panel .optimize-option:hover { background-color: #17a2b8; color: white; } #gm-extractor-panel .optimize-option.selected { background-color: #007bff; color: white; } #gm-extractor-panel .validation-result { margin-top: 8px; padding: 6px 10px; border-radius: 4px; font-size: 12px; } #gm-extractor-panel .validation-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } #gm-extractor-panel .validation-warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } #gm-extractor-panel .validation-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } #gm-extractor-panel .selector-row { margin-bottom: 10px; padding: 10px; background-color: white; border-radius: 6px; border: 1px solid #e0e0e0; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } #gm-extractor-panel .selector-input-row { display: flex; align-items: center; gap: 8px; } #gm-extractor-panel .selector-input { flex-grow: 1; padding: 8px 10px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 13px; transition: border-color 0.2s ease; background-color: white; } #gm-extractor-panel .selector-input:focus { outline: none; border-color: #4A90E2; box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1); } #gm-extractor-panel .add-btn { padding: 6px 8px; cursor: pointer; background-color: #28a745; color: white; border: none; border-radius: 4px; font-weight: 600; transition: background-color 0.2s ease; min-width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 16px; } #gm-extractor-panel .add-btn:hover { background-color: #218838; } #gm-extractor-panel .remove-btn { padding: 6px 8px; cursor: pointer; background-color: #dc3545; color: white; border: none; border-radius: 4px; font-weight: 600; transition: background-color 0.2s ease; min-width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 16px; } #gm-extractor-panel .remove-btn:hover { background-color: #c82333; } #gm-extractor-panel .preview-section { margin-top: 12px; padding: 12px; background-color: white; border: 1px solid #e0e0e0; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } #gm-extractor-panel .preview-title { font-weight: 600; margin-bottom: 8px; color: #495057; display: flex; align-items: center; gap: 6px; font-size: 14px; } #gm-extractor-panel .preview-table { width: 100%; border-collapse: collapse; font-size: 12px; background-color: white; } #gm-extractor-panel .preview-table th { background-color: #f8f9fa; padding: 6px 8px; text-align: left; font-weight: 600; border: 1px solid #dee2e6; color: #495057; font-size: 11px; } #gm-extractor-panel .preview-table td { padding: 6px 8px; border: 1px solid #dee2e6; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; } #gm-extractor-panel .preview-table tr:nth-child(even) { background-color: #f8f9fa; } #gm-extractor-panel .preview-info { margin-top: 10px; padding: 8px 12px; background-color: #e3f2fd; border-radius: 4px; font-size: 12px; color: #1976d2; } #gm-extractor-panel .extractor-footer { padding: 12px 16px; border-top: 1px solid #e0e0e0; background-color: white; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; } #gm-extractor-panel .footer-actions { display: flex; gap: 8px; justify-content: center; } #gm-extractor-panel button { padding: 8px 14px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 13px; transition: all 0.2s ease; min-width: 90px; } #gm-extractor-panel .footer-actions button { min-width: 100px; padding: 10px 16px; } #gm-extractor-panel #add-selector-btn { background-color: #28a745; color: white; } #gm-extractor-panel #add-selector-btn:hover { background-color: #218838; transform: translateY(-1px); } #gm-extractor-panel #preview-btn { background-color: #ffc107; color: #212529; } #gm-extractor-panel #preview-btn:hover { background-color: #e0a800; transform: translateY(-1px); } #gm-extractor-panel #export-csv-btn { background-color: #007bff; color: white; } #gm-extractor-panel #export-csv-btn:hover { background-color: #0056b3; transform: translateY(-1px); } #gm-extractor-panel #close-panel-btn { cursor: pointer; font-size: 24px; font-weight: bold; opacity: 0.8; transition: opacity 0.2s ease; padding: 0; min-width: auto; background: none; color: white; } #gm-extractor-panel #close-panel-btn:hover { opacity: 1; } .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } .status-success { background-color: #28a745; } .status-warning { background-color: #ffc107; } .status-error { background-color: #dc3545; } #gm-extractor-panel .accumulation-controls { margin-bottom: 12px; padding: 10px; background-color: #e3f2fd; border: 1px solid #bbdefb; border-radius: 6px; display: flex; align-items: center; gap: 10px; } #gm-extractor-panel .accumulation-toggle { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #1976d2; } #gm-extractor-panel .toggle-switch { position: relative; width: 40px; height: 20px; background-color: #ccc; border-radius: 20px; cursor: pointer; transition: background-color 0.3s; } #gm-extractor-panel .toggle-switch.active { background-color: #4caf50; } #gm-extractor-panel .toggle-slider { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background-color: white; border-radius: 50%; transition: transform 0.3s; } #gm-extractor-panel .toggle-switch.active .toggle-slider { transform: translateX(20px); } #gm-extractor-panel .accumulation-info { flex: 1; font-size: 12px; color: #666; } #gm-extractor-panel .clear-btn { background-color: #ff9800; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 600; } #gm-extractor-panel .clear-btn:hover { background-color: #f57c00; } #gm-extractor-panel .group-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: none; justify-content: center; align-items: center; z-index: 10000; } #gm-extractor-panel .group-modal.open { display: flex; } #gm-extractor-panel .group-modal-content { background-color: white; border-radius: 8px; padding: 20px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } #gm-extractor-panel .group-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e0e0e0; } #gm-extractor-panel .group-modal-title { font-size: 18px; font-weight: 600; color: #333; } #gm-extractor-panel .group-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } #gm-extractor-panel .group-modal-close:hover { color: #333; } #gm-extractor-panel .group-form { margin-bottom: 20px; } #gm-extractor-panel .group-form input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px; } #gm-extractor-panel .group-form button { background-color: #4A90E2; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; } #gm-extractor-panel .group-form button:hover { background-color: #357ABD; } #gm-extractor-panel .group-list { max-height: 300px; overflow-y: auto; } #gm-extractor-panel .group-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border: 1px solid #e0e0e0; border-radius: 4px; margin-bottom: 8px; background-color: #f9f9f9; } #gm-extractor-panel .group-item:hover { background-color: #f0f0f0; } #gm-extractor-panel .group-item-info { flex: 1; } #gm-extractor-panel .group-item-name { font-weight: 600; color: #333; margin-bottom: 4px; } #gm-extractor-panel .group-item-path { font-size: 12px; color: #666; } #gm-extractor-panel .group-item-actions { display: flex; gap: 8px; } #gm-extractor-panel .group-item-btn { padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; } #gm-extractor-panel .group-load-btn { background-color: #28a745; color: white; } #gm-extractor-panel .group-load-btn:hover { background-color: #218838; } #gm-extractor-panel .group-delete-btn { background-color: #dc3545; color: white; } #gm-extractor-panel .group-delete-btn:hover { background-color: #c82333; } #gm-extractor-panel .groups-sidebar { position: absolute; left: -180px; top: 0; width: 180px; height: 100%; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px 0 0 8px; padding: 12px; overflow-y: auto; transition: left 0.3s ease; z-index: 1; } #gm-extractor-panel .groups-sidebar.open { left: 0; } #gm-extractor-panel .groups-toggle { position: absolute; left: -20px; top: 20px; width: 20px; height: 40px; background-color: #6c757d; border-radius: 4px 0 0 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; transition: background-color 0.3s; } #gm-extractor-panel .groups-toggle:hover { background-color: #5a6268; } #gm-extractor-panel .groups-header { font-weight: 600; margin-bottom: 10px; color: #495057; font-size: 14px; display: flex; align-items: center; justify-content: space-between; } #gm-extractor-panel .group-item { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.2s ease; position: relative; } #gm-extractor-panel .group-item:hover { background-color: #e3f2fd; border-color: #4A90E2; } #gm-extractor-panel .group-name { font-weight: 600; font-size: 12px; color: #495057; margin-bottom: 2px; } #gm-extractor-panel .group-path { font-size: 10px; color: #6c757d; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #gm-extractor-panel .group-info { font-size: 10px; color: #28a745; } #gm-extractor-panel .group-delete { position: absolute; top: 4px; right: 4px; width: 16px; height: 16px; background-color: #dc3545; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 10px; display: none; align-items: center; justify-content: center; } #gm-extractor-panel .group-item:hover .group-delete { display: flex; } #gm-extractor-panel .save-group-btn { background-color: #17a2b8; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 600; margin-bottom: 10px; width: 100%; } #gm-extractor-panel .save-group-btn:hover { background-color: #138496; } #gm-extractor-panel .group-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 100000; display: none; align-items: center; justify-content: center; } #gm-extractor-panel .group-modal-content { background-color: white; padding: 20px; border-radius: 8px; width: 300px; max-width: 90%; } #gm-extractor-panel .group-modal h3 { margin: 0 0 15px 0; color: #495057; font-size: 16px; } #gm-extractor-panel .group-modal input { width: 100%; padding: 8px 10px; border: 1px solid #ced4da; border-radius: 4px; margin-bottom: 15px; font-size: 14px; } #gm-extractor-panel .group-modal-buttons { display: flex; gap: 10px; justify-content: flex-end; } #gm-extractor-panel .group-modal button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; } #gm-extractor-panel .group-modal .btn-primary { background-color: #007bff; color: white; } #gm-extractor-panel .group-modal .btn-secondary { background-color: #6c757d; color: white; } `); // 3. 定义所有函数(在HTML创建之前) // 智能选择器优化函数 function optimizeSelector(button) { const row = button.closest('.selector-row'); const input = row.querySelector('.selector-input'); const originalSelector = input.value.trim(); if (!originalSelector) return; // 显示优化选项 showOptimizeOptions(row, originalSelector); } // 立即暴露到全局作用域 window.optimizeSelector = optimizeSelector; // 显示优化选项 function showOptimizeOptions(row, originalSelector) { const optimizeDiv = row.querySelector('.selector-optimize'); // 分析选择器,生成多种优化策略 const strategies = analyzeSelector(originalSelector); if (strategies.length === 0) { showValidationResult(row, 'error', '未检测到可优化的行号选择器'); return; } // 创建优化选项界面 const optionsHtml = ` <div class="optimize-options"> <div style="font-weight: 600; margin-bottom: 6px; color: #495057;">选择优化策略:</div> ${strategies.map((strategy, index) => ` <span class="optimize-option" data-strategy="${index}" data-original="${originalSelector.replace(/"/g, '"')}"> ${strategy.name} </span> `).join('')} <div style="margin-top: 6px; font-size: 10px; color: #6c757d;"> 点击策略后会自动验证并显示结果 </div> </div> `; optimizeDiv.innerHTML = optionsHtml; // 为每个策略按钮添加事件监听器 optimizeDiv.querySelectorAll('.optimize-option').forEach(option => { option.addEventListener('click', () => { const strategyIndex = parseInt(option.dataset.strategy); const original = option.dataset.original; applyOptimization(strategyIndex, original, row); }); }); } // 分析选择器,生成优化策略 function analyzeSelector(selector) { const strategies = []; // 策略1: 移除所有行号选择器(激进) if (selector.includes(':nth-child(') || selector.includes(':nth-of-type(') || selector.includes(':first-child') || selector.includes(':last-child')) { strategies.push({ name: '移除所有行号', description: '移除所有 :nth-child, :first-child 等选择器', transform: (sel) => { return sel.replace(/:nth-child\(\d+\)/g, '') .replace(/:nth-of-type\(\d+\)/g, '') .replace(/:first-child/g, '') .replace(/:last-child/g, '') .replace(/:first-of-type/g, '') .replace(/:last-of-type/g, '') .replace(/\s*>\s*/g, ' > ') .replace(/\s+/g, ' ') .trim(); } }); } // 策略2: 只移除最后一个行号选择器(保守) if (selector.includes(':nth-child(') || selector.includes(':nth-of-type(')) { strategies.push({ name: '移除最后行号', description: '只移除选择器末尾的行号选择器', transform: (sel) => { return sel.replace(/:nth-child\(\d+\)$/, '') .replace(/:nth-of-type\(\d+\)$/, '') .replace(/:first-child$/, '') .replace(/:last-child$/, '') .trim(); } }); } // 策略3: 替换为通用选择器 if (selector.includes(':nth-child(')) { strategies.push({ name: '替换为通用', description: '将 :nth-child(n) 替换为通用选择器', transform: (sel) => { return sel.replace(/:nth-child\(\d+\)/g, ':not(:empty)') .trim(); } }); } // 策略4: 基于父元素优化 if (selector.includes(' > ')) { strategies.push({ name: '基于父元素', description: '保留父元素结构,移除子元素行号', transform: (sel) => { const parts = sel.split(' > '); if (parts.length > 1) { const lastPart = parts[parts.length - 1]; const optimizedLast = lastPart.replace(/:nth-child\(\d+\)/g, '') .replace(/:nth-of-type\(\d+\)/g, ''); parts[parts.length - 1] = optimizedLast; return parts.join(' > '); } return sel; } }); } return strategies; } // 应用优化策略 function applyOptimization(strategyIndex, originalSelector, row) { const input = row.querySelector('.selector-input'); // 获取策略 const strategies = analyzeSelector(originalSelector); const strategy = strategies[strategyIndex]; if (!strategy) return; // 应用优化 const optimizedSelector = strategy.transform(originalSelector); // 验证优化结果 const validation = validateOptimizedSelector(originalSelector, optimizedSelector); // 显示验证结果 showValidationResult(row, validation.type, validation.message, optimizedSelector); // 如果验证成功,应用选择器 if (validation.type === 'success') { input.value = optimizedSelector; input.style.backgroundColor = '#d4edda'; setTimeout(() => { input.style.backgroundColor = ''; }, 2000); } } // 验证优化后的选择器 function validateOptimizedSelector(originalSelector, optimizedSelector) { try { // 测试原始选择器 const originalElements = document.querySelectorAll(originalSelector); const originalCount = originalElements.length; // 测试优化后的选择器 const optimizedElements = document.querySelectorAll(optimizedSelector); const optimizedCount = optimizedElements.length; if (optimizedCount === 0) { return { type: 'error', message: '优化后的选择器无法匹配任何元素' }; } if (optimizedCount === originalCount) { return { type: 'warning', message: `优化后仍只匹配 ${originalCount} 个元素,可能没有扩展匹配范围` }; } if (optimizedCount > originalCount) { return { type: 'success', message: `✅ 从 ${originalCount} 扩展到 ${optimizedCount} 个元素` }; } return { type: 'warning', message: `优化后匹配 ${optimizedCount} 个元素,请检查是否为目标数据` }; } catch (error) { return { type: 'error', message: `选择器语法错误: ${error.message}` }; } } // 显示验证结果 function showValidationResult(row, type, message, optimizedSelector = null) { // 移除现有的验证结果 const existingResult = row.querySelector('.validation-result'); if (existingResult) { existingResult.remove(); } // 创建新的验证结果 const resultDiv = document.createElement('div'); resultDiv.className = `validation-result validation-${type}`; resultDiv.innerHTML = ` <div>${message}</div> ${optimizedSelector ? `<div style="margin-top: 4px; font-family: monospace; font-size: 11px; opacity: 0.8;">${optimizedSelector}</div>` : ''} `; // 插入到选择器行中 const optimizeDiv = row.querySelector('.selector-optimize'); optimizeDiv.appendChild(resultDiv); // 如果是成功或错误,3秒后隐藏优化区域 if (type === 'success' || type === 'error') { setTimeout(() => { optimizeDiv.style.display = 'none'; }, 3000); } } // 4. 创建HTML界面 const panel = document.createElement('div'); panel.id = 'gm-extractor-panel'; panel.innerHTML = ` <div class="extractor-header"> <span>🔍 内容提取器</span> <div class="header-buttons"> <button id="save-group-btn" class="header-btn" title="保存当前选择器组">💾</button> <button id="load-group-btn" class="header-btn" title="加载选择器组">📂</button> <span id="close-panel-btn" title="关闭">×</span> </div> </div> <div class="extractor-body"> <details> <summary>📖 如何获取选择器 (点击展开)</summary> <div class="instructions"> <ul> <li><b>第1步:</b> 在您想提取的文字上 (例如标题或名字),点击鼠标<b>右键</b>,在弹出的菜单中选择 <strong>检查 (Inspect)</strong>。</li> <li><b>第2步:</b> 浏览器下方或右侧会弹出开发者工具,并且有一行代码是<b>高亮</b>状态。</li> <li><b>第3步:</b> 在这行<b>高亮的代码上</b>,再次点击鼠标<b>右键</b>。</li> <li><b>第4步:</b> 在弹出的新菜单中,依次选择 <strong>复制 (Copy)</strong> > <strong>复制选择器 (Copy selector)</strong>。</li> <li><b>第5步:</b> 回到本面板,将刚刚复制的内容粘贴到下面的输入框里即可。</li> </ul> <div class="pro-tip"> <b>💡 小提示:</b> <ul style="margin: 8px 0; padding-left: 20px;"> <li><b>提取单行数据:</b> 右键点击某一行 → 复制选择器</li> <li><b>提取列表数据:</b> 右键点击某一行 → 复制选择器 → 点击"优化选择器"按钮,脚本会自动移除行号限制</li> <li><b>手动优化:</b> 如果选择器包含 <code>:nth-child(1)</code> 等行号,删除这部分即可获取所有行</li> </ul> </div> </div> </details> <div class="accumulation-controls"> <div class="accumulation-toggle"> <span>积累模式</span> <div class="toggle-switch" id="accumulation-toggle"> <div class="toggle-slider"></div> </div> </div> <div class="accumulation-info" id="accumulation-info"> 关闭 - 点击"预览和积累"只显示当前页面数据 </div> <button class="clear-btn" id="clear-accumulated-btn" style="display: none;">清空</button> </div> <div id="selectors-container"> </div> <div class="preview-section" id="preview-section" style="display: none;"> <div class="preview-title"> <span class="status-indicator status-success"></span> 数据预览 (前3行) </div> <div id="preview-content"></div> <div class="preview-info" id="preview-info"></div> </div> </div> <div class="extractor-footer"> <div class="footer-actions"> <button id="preview-btn">👁️ 预览和积累</button> <button id="export-csv-btn">📥 导出CSV</button> </div> </div> <!-- 组管理模态框 --> <div class="group-modal" id="group-modal"> <div class="group-modal-content"> <div class="group-modal-header"> <div class="group-modal-title">选择器组管理</div> <button class="group-modal-close" id="group-modal-close">×</button> </div> <div class="group-form"> <input type="text" id="group-name-input" placeholder="输入组名称(例如:商品列表)" maxlength="50"> <button id="save-current-group-btn">💾 保存当前选择器组</button> </div> <div class="group-list" id="group-list"> <!-- 组列表将在这里动态生成 --> </div> </div> </div> `; document.body.appendChild(panel); const selectorsContainer = panel.querySelector('#selectors-container'); const previewSection = panel.querySelector('#preview-section'); const previewContent = panel.querySelector('#preview-content'); const previewInfo = panel.querySelector('#preview-info'); // 数据积累相关变量 let accumulatedData = []; let isAccumulationMode = false; // 选择器组相关变量 let savedGroups = []; const STORAGE_KEY = 'content_extractor_groups'; // 4. 界面的交互逻辑 // 选择器组管理函数 function loadSavedGroups() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { savedGroups = JSON.parse(stored); } } catch (e) { console.error('加载保存的组失败:', e); savedGroups = []; } renderGroupsList(); } function saveGroupsToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(savedGroups)); } catch (e) { console.error('保存组失败:', e); alert('保存失败,可能是存储空间不足'); } } function renderGroupsList() { const groupsList = panel.querySelector('#groups-list'); if (savedGroups.length === 0) { groupsList.innerHTML = '<div style="text-align: center; color: #6c757d; font-size: 12px; padding: 20px;">暂无保存的组</div>'; return; } groupsList.innerHTML = savedGroups.map((group, index) => ` <div class="group-item" data-index="${index}"> <div class="group-name">${group.name || '未命名组'}</div> <div class="group-path" title="${group.path}">${group.path}</div> <div class="group-info">${group.selectors.length} 个选择器</div> <button class="group-delete" onclick="deleteGroup(${index})" title="删除此组">×</button> </div> `).join(''); // 为每个组项添加点击事件 groupsList.querySelectorAll('.group-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.classList.contains('group-delete')) return; const index = parseInt(item.dataset.index); loadGroup(index); }); }); } function showSaveGroupModal() { const modal = document.createElement('div'); modal.className = 'group-modal'; modal.innerHTML = ` <div class="group-modal-content"> <h3>保存选择器组</h3> <input type="text" id="group-name-input" placeholder="组名称(可选)" maxlength="50"> <div class="group-modal-buttons"> <button class="btn-secondary" onclick="closeSaveModal()">取消</button> <button class="btn-primary" onclick="saveCurrentGroup()">保存</button> </div> </div> `; document.body.appendChild(modal); modal.style.display = 'flex'; // 自动聚焦到输入框 setTimeout(() => { modal.querySelector('#group-name-input').focus(); }, 100); // 回车保存 modal.querySelector('#group-name-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { saveCurrentGroup(); } }); } function closeSaveModal() { const modal = document.querySelector('.group-modal'); if (modal) { modal.remove(); } } // 切换积累模式 function toggleAccumulationMode() { isAccumulationMode = !isAccumulationMode; const toggle = panel.querySelector('#accumulation-toggle'); const info = panel.querySelector('#accumulation-info'); const clearBtn = panel.querySelector('#clear-accumulated-btn'); if (isAccumulationMode) { toggle.classList.add('active'); info.textContent = `开启 - 已积累 ${accumulatedData.length} 行数据,点击"预览和积累"会继续积累`; clearBtn.style.display = 'block'; } else { toggle.classList.remove('active'); info.textContent = '关闭 - 点击"预览和积累"只显示当前页面数据'; clearBtn.style.display = 'none'; } } // 清空积累的数据 function clearAccumulatedData() { accumulatedData = []; const info = panel.querySelector('#accumulation-info'); info.textContent = '开启 - 已积累 0 行数据,点击"预览和积累"会开始积累'; // 如果预览区域显示的是积累数据,需要重新预览当前页面 if (isAccumulationMode && previewSection.style.display !== 'none') { previewData(); } } // 检查数据是否重复 function isDuplicateData(newRow, existingData) { return existingData.some(existingRow => { return newRow.every((cell, index) => { return cell === (existingRow[index] || ''); }); }); } // 添加数据到积累池 function addToAccumulatedData(newData) { let addedCount = 0; newData.forEach(row => { if (!isDuplicateData(row, accumulatedData)) { accumulatedData.push(row); addedCount++; } }); return addedCount; } function toggleExtractorPanel() { panel.style.display = (panel.style.display === 'flex') ? 'none' : 'flex'; if (selectorsContainer.children.length === 0) { addSelectorRow(); } } function addSelectorRow(initialValue = '') { const row = document.createElement('div'); row.className = 'selector-row'; row.innerHTML = ` <div class="selector-input-row"> <button class="add-btn" title="在此行前添加选择器">+</button> <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${initialValue}"> <button class="remove-btn" title="移除此行">-</button> </div> `; selectorsContainer.appendChild(row); // 添加选择器优化提示 const optimizeDiv = document.createElement('div'); optimizeDiv.className = 'selector-optimize'; optimizeDiv.style.display = 'none'; optimizeDiv.innerHTML = ` <span>检测到行号选择器,点击优化可获取所有行数据</span> <button class="optimize-btn">优化选择器</button> `; row.appendChild(optimizeDiv); // 为加号按钮添加事件监听器 const addBtn = row.querySelector('.add-btn'); addBtn.addEventListener('click', () => { const newRow = addSelectorRow(); row.parentNode.insertBefore(newRow, row); }); // 为优化按钮添加事件监听器 const optimizeBtn = optimizeDiv.querySelector('.optimize-btn'); optimizeBtn.addEventListener('click', () => { optimizeSelector(optimizeBtn); }); // 监听输入变化,检测是否需要优化 const input = row.querySelector('.selector-input'); input.addEventListener('input', () => { const selector = input.value.trim(); if (selector && (selector.includes(':nth-child(') || selector.includes(':nth-of-type('))) { optimizeDiv.style.display = 'block'; } else { optimizeDiv.style.display = 'none'; } }); row.querySelector('.remove-btn').addEventListener('click', () => { row.remove(); // 如果移除了所有选择器,隐藏预览区域 if (selectorsContainer.children.length === 0) { previewSection.style.display = 'none'; } }); return row; } function previewData() { const selectorInputs = panel.querySelectorAll('.selector-input'); const selectors = Array.from(selectorInputs).map(input => input.value.trim()).filter(Boolean); if (selectors.length === 0) { alert('请输入至少一个CSS选择器!'); return; } const columnsData = []; const validSelectors = []; let hasError = false; selectors.forEach((selector, index) => { try { const elements = Array.from(document.querySelectorAll(selector)); const data = elements.map(el => el.innerText.trim()); columnsData.push(data); validSelectors.push(selector); } catch (e) { alert(`选择器 "${selector}" 无效,请检查语法!`); hasError = true; } }); if (hasError) return; const maxRows = Math.max(0, ...columnsData.map(col => col.length)); if (maxRows === 0) { previewContent.innerHTML = '<div style="text-align: center; color: #6c757d; padding: 20px;">没有找到任何数据</div>'; previewInfo.innerHTML = '根据您提供的选择器,没有在页面上找到任何内容。'; previewSection.style.display = 'block'; return; } // 处理数据积累 let displayData = []; let totalRows = maxRows; let addedCount = 0; if (isAccumulationMode) { // 将当前页面数据转换为行格式 const currentPageData = []; for (let i = 0; i < maxRows; i++) { const row = columnsData.map(col => col[i] || ''); currentPageData.push(row); } // 添加到积累池 addedCount = addToAccumulatedData(currentPageData); // 使用积累的数据进行显示 displayData = accumulatedData; totalRows = accumulatedData.length; // 更新积累信息 const info = panel.querySelector('#accumulation-info'); info.textContent = `开启 - 已积累 ${accumulatedData.length} 行数据 (本次新增 ${addedCount} 行),点击"预览和积累"会继续积累`; } else { // 普通模式,直接使用当前页面数据 for (let i = 0; i < maxRows; i++) { const row = columnsData.map(col => col[i] || ''); displayData.push(row); } } // 创建预览表格 let tableHTML = '<table class="preview-table"><thead><tr>'; validSelectors.forEach(selector => { tableHTML += `<th>${selector.length > 20 ? selector.substring(0, 20) + '...' : selector}</th>`; }); tableHTML += '</tr></thead><tbody>'; // 只显示前3行数据 const previewRows = Math.min(3, totalRows); for (let i = 0; i < previewRows; i++) { tableHTML += '<tr>'; displayData[i].forEach(cellData => { const displayData = cellData.length > 30 ? cellData.substring(0, 30) + '...' : cellData; tableHTML += `<td title="${cellData.replace(/"/g, '"')}">${displayData}</td>`; }); tableHTML += '</tr>'; } tableHTML += '</tbody></table>'; previewContent.innerHTML = tableHTML; // 更新预览信息 let infoText = `共找到 ${totalRows} 行数据,显示前 ${previewRows} 行。列数: ${validSelectors.length}`; if (isAccumulationMode && addedCount > 0) { infoText += ` (本次新增 ${addedCount} 行)`; } previewInfo.innerHTML = infoText; previewSection.style.display = 'block'; } function exportToCsv() { const selectorInputs = panel.querySelectorAll('.selector-input'); const selectors = Array.from(selectorInputs).map(input => input.value.trim()).filter(Boolean); if (selectors.length === 0) { alert('请输入至少一个CSS选择器!'); return; } let exportData = []; let fileName = 'content_export'; if (isAccumulationMode && accumulatedData.length > 0) { // 导出积累的数据 exportData = accumulatedData; fileName = `accumulated_export_${accumulatedData.length}rows`; } else { // 导出当前页面数据 const columnsData = selectors.map(selector => { try { const elements = Array.from(document.querySelectorAll(selector)); const data = elements.map(el => el.innerText.trim()); return data; } catch (e) { alert('选择器 "' + selector + '" 无效,请检查语法!'); return null; } }); if (columnsData.some(col => col === null)) return; const maxRows = Math.max(0, ...columnsData.map(col => col.length)); if (maxRows === 0) { alert('根据您提供的选择器,没有在页面上找到任何内容。'); return; } // 将列数据转换为行数据 for (let i = 0; i < maxRows; i++) { const row = columnsData.map(col => col[i] || ''); exportData.push(row); } fileName = 'current_page_export'; } if (exportData.length === 0) { alert('没有数据可以导出!'); return; } // 生成CSV内容 let csvContent = ''; const header = selectors.map(s => `"${s.replace(/"/g, '""')}"`).join(','); csvContent += header + '\r\n'; exportData.forEach(row => { const rowData = row.map(cellData => `"${String(cellData).replace(/"/g, '""')}"`); csvContent += rowData.join(',') + '\r\n'; }); // 使用Blob方式导出,避免URL长度限制 const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); try { if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", `${fileName}_${new Date().toISOString().slice(0,10)}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); // 使用setTimeout避免同步点击问题 setTimeout(() => { try { link.click(); } catch (e) { console.warn('点击下载链接失败,尝试降级方案:', e); // 降级方案 const dataUrl = 'data:text/csv;charset=utf-8,' + encodeURIComponent('\ufeff' + csvContent); window.open(dataUrl); } document.body.removeChild(link); URL.revokeObjectURL(url); }, 100); } else { // 降级方案:使用data URL const dataUrl = 'data:text/csv;charset=utf-8,' + encodeURIComponent('\ufeff' + csvContent); window.open(dataUrl); } } catch (e) { console.error('导出CSV失败:', e); // 提供备用方案:复制到剪贴板 if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(csvContent).then(() => { alert('导出失败,但数据已复制到剪贴板,您可以粘贴到Excel或其他应用中'); }).catch(() => { alert('导出失败,请检查浏览器设置'); }); } else { alert('导出失败,请检查浏览器设置'); } } // 显示成功提示 const originalText = panel.querySelector('#export-csv-btn').textContent; const exportBtn = panel.querySelector('#export-csv-btn'); exportBtn.textContent = `✅ 已导出 ${exportData.length} 行`; exportBtn.style.backgroundColor = '#28a745'; setTimeout(() => { exportBtn.textContent = originalText; exportBtn.style.backgroundColor = '#007bff'; }, 2000); } // 5. 绑定事件 panel.querySelector('#preview-btn').addEventListener('click', previewData); panel.querySelector('#export-csv-btn').addEventListener('click', exportToCsv); panel.querySelector('#close-panel-btn').addEventListener('click', toggleExtractorPanel); // 绑定积累模式相关事件 panel.querySelector('#accumulation-toggle').addEventListener('click', toggleAccumulationMode); panel.querySelector('#clear-accumulated-btn').addEventListener('click', clearAccumulatedData); // 绑定组管理相关事件 panel.querySelector('#save-group-btn').addEventListener('click', showGroupModal); panel.querySelector('#load-group-btn').addEventListener('click', showGroupModal); panel.querySelector('#group-modal-close').addEventListener('click', closeGroupModal); panel.querySelector('#save-current-group-btn').addEventListener('click', saveCurrentGroup); // 使用事件委托处理动态生成的按钮 panel.addEventListener('click', function(e) { if (e.target.classList.contains('group-item-btn')) { const action = e.target.getAttribute('data-action'); const index = parseInt(e.target.getAttribute('data-index')); if (action === 'load') { loadGroup(index); } else if (action === 'delete') { deleteGroup(index); } } else if (e.target.classList.contains('add-btn')) { // 处理"+"按钮点击 e.preventDefault(); addSelectorRow(); } else if (e.target.classList.contains('remove-btn')) { // 处理"-"按钮点击 e.preventDefault(); const row = e.target.closest('.selector-row'); if (row) { row.remove(); } } }); // 初始化加载保存的组 loadSavedGroups(); // 组管理相关函数 function showGroupModal() { const modal = panel.querySelector('#group-modal'); modal.classList.add('open'); renderGroupList(); } function closeGroupModal() { const modal = panel.querySelector('#group-modal'); modal.classList.remove('open'); } function saveCurrentGroup() { const nameInput = panel.querySelector('#group-name-input'); const groupName = nameInput.value.trim(); if (!groupName) { alert('请输入组名称!'); return; } const currentSelectors = Array.from(panel.querySelectorAll('.selector-input')) .map(input => input.value.trim()) .filter(selector => selector); if (currentSelectors.length === 0) { alert('当前没有选择器可以保存!'); return; } const groupData = { name: groupName, selectors: currentSelectors, url: window.location.href, timestamp: new Date().toISOString() }; const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); savedGroups.push(groupData); localStorage.setItem('extractorGroups', JSON.stringify(savedGroups)); nameInput.value = ''; renderGroupList(); alert(`组 "${groupName}" 保存成功!`); } function renderGroupList() { const groupList = panel.querySelector('#group-list'); const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); if (savedGroups.length === 0) { groupList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无保存的组</div>'; return; } groupList.innerHTML = savedGroups.map((group, index) => ` <div class="group-item"> <div class="group-item-info"> <div class="group-item-name">${group.name}</div> <div class="group-item-path">${new URL(group.url).pathname}</div> </div> <div class="group-item-actions"> <button class="group-item-btn group-load-btn" data-action="load" data-index="${index}">加载</button> <button class="group-item-btn group-delete-btn" data-action="delete" data-index="${index}">删除</button> </div> </div> `).join(''); } function loadGroup(index) { const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); const group = savedGroups[index]; if (!group) { alert('组不存在!'); return; } // 清空当前选择器 const selectorsContainer = panel.querySelector('#selectors-container'); selectorsContainer.innerHTML = ''; // 添加新的选择器 group.selectors.forEach(selector => { addSelectorRow(selector); }); closeGroupModal(); alert(`已加载组 "${group.name}",包含 ${group.selectors.length} 个选择器`); } function deleteGroup(index) { const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); const group = savedGroups[index]; if (!group) { alert('组不存在!'); return; } if (confirm(`确定要删除组 "${group.name}" 吗?`)) { savedGroups.splice(index, 1); localStorage.setItem('extractorGroups', JSON.stringify(savedGroups)); renderGroupList(); alert('组已删除!'); } } // 6. 添加拖拽功能 (function makeDraggable(panel) { const header = panel.querySelector('.extractor-header'); let isDragging = false; let offsetX, offsetY; header.addEventListener('mousedown', (e) => { if (e.button !== 0) return; if (e.target.id === 'close-panel-btn') return; isDragging = true; const panelRect = panel.getBoundingClientRect(); offsetX = e.clientX - panelRect.left; offsetY = e.clientY - panelRect.top; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!isDragging) return; let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; panel.style.right = ''; } function onMouseUp() { isDragging = false; document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } })(panel); })(); // 暴露函数到全局作用域 window.loadGroup = function(index) { const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); const group = savedGroups[index]; if (!group) { alert('组不存在!'); return; } // 清空当前选择器 const selectorsContainer = document.querySelector('#selectors-container'); if (selectorsContainer) { selectorsContainer.innerHTML = ''; // 添加新的选择器 group.selectors.forEach(selector => { // 创建新的选择器行 const row = document.createElement('div'); row.className = 'selector-row'; row.innerHTML = ` <div class="selector-input-row"> <button class="add-btn" onclick="addSelectorRow()">+</button> <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${selector}"> <button class="remove-btn" onclick="this.parentElement.parentElement.remove()">-</button> </div> <div class="selector-optimize" style="display: none;"> <button class="optimize-btn" onclick="optimizeSelector(this)">🔧 优化选择器</button> </div> `; selectorsContainer.appendChild(row); }); // 关闭模态框 const modal = document.querySelector('#group-modal'); if (modal) { modal.classList.remove('open'); } alert(`已加载组 "${group.name}",包含 ${group.selectors.length} 个选择器`); } }; window.deleteGroup = function(index) { const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]'); const group = savedGroups[index]; if (!group) { alert('组不存在!'); return; } if (confirm(`确定要删除组 "${group.name}" 吗?`)) { savedGroups.splice(index, 1); localStorage.setItem('extractorGroups', JSON.stringify(savedGroups)); // 重新渲染组列表 const groupList = document.querySelector('#group-list'); if (groupList) { if (savedGroups.length === 0) { groupList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无保存的组</div>'; } else { groupList.innerHTML = savedGroups.map((group, index) => ` <div class="group-item"> <div class="group-item-info"> <div class="group-item-name">${group.name}</div> <div class="group-item-path">${new URL(group.url).pathname}</div> </div> <div class="group-item-actions"> <button class="group-item-btn group-load-btn" data-action="load" data-index="${index}">加载</button> <button class="group-item-btn group-delete-btn" data-action="delete" data-index="${index}">删除</button> </div> </div> `).join(''); } } alert('组已删除!'); } }; // 暴露其他必要的函数到全局作用域 window.addSelectorRow = function(value = '') { const selectorsContainer = document.querySelector('#selectors-container'); if (selectorsContainer) { const row = document.createElement('div'); row.className = 'selector-row'; row.innerHTML = ` <div class="selector-input-row"> <button class="add-btn" onclick="addSelectorRow()">+</button> <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${value}"> <button class="remove-btn" onclick="this.parentElement.parentElement.remove()">-</button> </div> <div class="selector-optimize" style="display: none;"> <button class="optimize-btn" onclick="optimizeSelector(this)">🔧 优化选择器</button> </div> `; selectorsContainer.appendChild(row); return row; } }; window.optimizeSelector = function(button) { const row = button.closest('.selector-row'); const input = row.querySelector('.selector-input'); const optimizeDiv = row.querySelector('.selector-optimize'); if (!input.value.trim()) { alert('请先输入选择器!'); return; } // 显示优化选项 optimizeDiv.style.display = 'block'; optimizeDiv.innerHTML = ` <div class="optimize-options"> <button class="optimize-option" onclick="applyOptimization(this, 'remove-nth')">移除行号限制</button> <button class="optimize-option" onclick="applyOptimization(this, 'generalize')">通用化选择器</button> <button class="optimize-option" onclick="applyOptimization(this, 'parent')">使用父级选择器</button> <button class="optimize-option" onclick="applyOptimization(this, 'class-only')">仅保留类名</button> </div> `; }; window.applyOptimization = function(button, type) { const row = button.closest('.selector-row'); const input = row.querySelector('.selector-input'); const originalSelector = input.value.trim(); let optimizedSelector = originalSelector; switch (type) { case 'remove-nth': optimizedSelector = originalSelector.replace(/:nth-child\(\d+\)/g, ''); optimizedSelector = optimizedSelector.replace(/:nth-of-type\(\d+\)/g, ''); break; case 'generalize': optimizedSelector = originalSelector.replace(/\d+/g, ''); break; case 'parent': { const parts = originalSelector.split(' > '); if (parts.length > 1) { optimizedSelector = parts.slice(0, -1).join(' > '); } break; } case 'class-only': { const classMatch = originalSelector.match(/\.[\w-]+/g); if (classMatch) { optimizedSelector = classMatch.join(''); } break; } } input.value = optimizedSelector; // 验证优化结果 try { const elements = document.querySelectorAll(optimizedSelector); const originalElements = document.querySelectorAll(originalSelector); if (elements.length > originalElements.length) { alert(`✅ 优化成功!从 ${originalElements.length} 个元素扩展到 ${elements.length} 个元素`); } else if (elements.length === originalElements.length) { alert(`⚠️ 优化后仍匹配 ${elements.length} 个元素,可能没有扩展匹配范围`); } else { alert(`❌ 优化后只匹配 ${elements.length} 个元素,请检查选择器`); } } catch (e) { alert(`❌ 选择器语法错误: ${e.message}`); } // 隐藏优化选项 const optimizeDiv = row.querySelector('.selector-optimize'); optimizeDiv.style.display = 'none'; };