/** * YouTube Crawler Benchmark Test v2 * 基于 benchmark_v2.md 的7维度评分标准(目标 >= 90) * 测试对象: evolve_v2/youtube_crawler_v2.js (增强版类实现) */ const fs = require('fs'); const path = require('path'); const CRAWLER_PATH = path.join(__dirname, 'youtube_crawler.js'); const crawlerModule = require(CRAWLER_PATH); const { YouTubeTrendingCrawler, getMockTrendingVideos } = crawlerModule; const LOGS = []; function log(message, level = 'info') { const entry = { time: new Date().toISOString(), level, message }; LOGS.push(entry); console.log(`[${level.toUpperCase()}] ${message}`); } const SCORES = { functionality: { score: 0, max: 20, details: [] }, api_usage: { score: 0, max: 15, details: [] }, data_integrity: { score: 0, max: 20, details: [] }, code_quality: { score: 0, max: 15, details: [] }, error_handling: { score: 0, max: 15, details: [] }, extensibility: { score: 0, max: 10, details: [] }, performance: { score: 0, max: 5, details: [] } }; let CODE_TEXT = ''; try { CODE_TEXT = fs.readFileSync(CRAWLER_PATH, 'utf-8'); } catch (e) { log(`无法读取被测代码: ${e.message}`, 'error'); } // ==================== 维度1: 功能完整性 (20分) ==================== async function testRequiredFields() { log('\n--- 测试: 9个必填字段覆盖 (13.5分) ---'); let score = 0; const details = {}; try { const crawler = new YouTubeTrendingCrawler({ useMock: true }); const videos = await crawler.get_trending('US', 5); if (!videos || !videos.length) throw new Error('无返回数据'); const v = videos[0]; const checks = [ { key: 'video_id', pts: 1.5 }, { key: 'title', pts: 1.5 }, { key: 'channel_title', pts: 1.5 }, { key: 'published_at', pts: 1.5 }, { key: 'view_count', pts: 1.5 }, { key: 'source_url', pts: 1.5 }, { key: 'channel_id', pts: 1.5 }, { key: 'category_id', pts: 1.5 }, { key: 'crawl_time', pts: 1.5 }, ]; for (const c of checks) { if (v[c.key] !== undefined) { score += c.pts; details[c.key] = 'ok'; } else { details[c.key] = 'missing'; } } } catch (error) { log(`❌ 必填字段测试失败: ${error.message}`, 'error'); } score = Math.min(score, 13.5); SCORES.functionality.details.push({ test: 'required_fields', score: +score.toFixed(2), max: 13.5, details }); SCORES.functionality.score += score; log(`得分: ${score.toFixed(2)}/13.5`); return score; } function testCategorySupport() { log('\n--- 测试: 目标分类支持 (3分) ---'); let score = 0; const categories = ['10', '20', '24', '28']; const supported = categories.filter(c => CODE_TEXT.includes(c)).length; score = Math.min(3, supported); SCORES.functionality.details.push({ test: 'category_support', score, max: 3, supported }); SCORES.functionality.score += score; log(`得分: ${score}/3 (支持 ${supported}/4 个分类)`); return score; } function testRegionSupport() { log('\n--- 测试: 主要地区支持 (2分) ---'); let score = 0; const regions = ['US', 'JP', 'KR', 'GB', 'DE', 'FR', 'IN', 'BR', 'RU']; const supported = regions.filter(r => CODE_TEXT.includes(`'${r}'`) || CODE_TEXT.includes(`"${r}"`)).length; score = Math.min(2, Math.floor(supported / 3)); SCORES.functionality.details.push({ test: 'region_support', score, max: 2, supported }); SCORES.functionality.score += score; log(`得分: ${score}/2 (支持 ${supported}/9 个地区)`); return score; } function testOutputFormats() { log('\n--- 测试: 输出格式支持 (1.5分) ---'); let score = 0; if (CODE_TEXT.includes('JSON')) score += 0.5; if (CODE_TEXT.includes('json') || CODE_TEXT.includes('JSON.stringify')) score += 0.5; if (CODE_TEXT.includes('csv') || CODE_TEXT.includes('CSV')) score += 0.5; else score += 0; SCORES.functionality.details.push({ test: 'output_formats', score, max: 1.5 }); SCORES.functionality.score += score; log(`得分: ${score}/1.5`); return score; } // ==================== 维度2: API规范使用 (15分) ==================== function testApiPartParameter() { log('\n--- 测试: API part 参数 (3分) ---'); let score = 0; if (CODE_TEXT.includes('snippet')) score += 1; if (CODE_TEXT.includes('statistics')) score += 1; if (CODE_TEXT.includes('contentDetails')) score += 1; SCORES.api_usage.details.push({ test: 'part_parameter', score, max: 3 }); SCORES.api_usage.score += score; log(`得分: ${score}/3`); return score; } function testApiChartAndMaxResults() { log('\n--- 测试: chart 与 maxResults (3分) ---'); let score = 0; if (CODE_TEXT.includes('mostPopular')) score += 2; if (CODE_TEXT.includes('maxResults') && (CODE_TEXT.includes('Math.min') || CODE_TEXT.includes('Math.max'))) score += 1; SCORES.api_usage.details.push({ test: 'chart_maxresults', score, max: 3 }); SCORES.api_usage.score += score; log(`得分: ${score}/3`); return score; } function testNoSearchList() { log('\n--- 测试: 避免 search.list (2分) ---'); let score = CODE_TEXT.includes('search.list') ? 0 : 2; SCORES.api_usage.details.push({ test: 'no_search_list', score, max: 2 }); SCORES.api_usage.score += score; log(`得分: ${score}/2`); return score; } function testQuotaAndRateLimit() { log('\n--- 测试: 配额管理与频率控制 (4分) ---'); let score = 0; if (CODE_TEXT.includes('quota') || CODE_TEXT.includes('10000')) score += 1.5; if (CODE_TEXT.includes('setTimeout') || CODE_TEXT.includes('delay') || CODE_TEXT.includes('sleep')) score += 1.5; else score += 0; if (CODE_TEXT.includes('User-Agent') || CODE_TEXT.includes('user-agent')) score += 1; else score += 0; SCORES.api_usage.details.push({ test: 'quota_rate_limit', score, max: 4 }); SCORES.api_usage.score += score; log(`得分: ${score}/4`); return score; } function testApiKeyEnv() { log('\n--- 测试: API Key 环境变量 (2分) ---'); let score = CODE_TEXT.includes('process.env.YOUTUBE_API_KEY') ? 2 : 0; SCORES.api_usage.details.push({ test: 'api_key_env', score, max: 2 }); SCORES.api_usage.score += score; log(`得分: ${score}/2`); return score; } // ==================== 维度3: 数据完整性与准确性 (20分) ==================== async function testDataTypes() { log('\n--- 测试: 数据类型正确性 (8分) ---'); let score = 0; try { const crawler = new YouTubeTrendingCrawler({ useMock: true }); const videos = await crawler.get_trending('US', 3); const v = videos[0]; if (typeof v.view_count === 'number') score += 2.5; if (typeof v.published_at === 'string' && /\d{4}-\d{2}-\d{2}T/.test(v.published_at)) score += 2.5; // duration 检查 (数字秒数) if (typeof v.duration === 'number' && v.duration > 0) score += 1.5; // thumbnail_url 检查 if (typeof v.thumbnail_url === 'string' && v.thumbnail_url.includes('ytimg.com')) score += 1.5; } catch (e) { log(`❌ 数据类型测试失败: ${e.message}`, 'error'); } SCORES.data_integrity.details.push({ test: 'data_types', score, max: 8 }); SCORES.data_integrity.score += score; log(`得分: ${score}/8`); return score; } function testDataFormatting() { log('\n--- 测试: 数据格式化与兜底 (6分) ---'); let score = 0; // source_url 格式 (必须带 www) if (CODE_TEXT.includes('www.youtube.com/watch?v=')) score += 2; // crawl_time if (CODE_TEXT.includes('crawl_time') || CODE_TEXT.includes('crawlTime') || CODE_TEXT.includes('new Date().toISOString()')) score += 2; // 空值兜底 const nullGuards = ['||', '??', 'null', 'undefined', 'catch']; const hasGuards = nullGuards.some(g => CODE_TEXT.includes(g)); if (hasGuards) score += 2; SCORES.data_integrity.details.push({ test: 'data_formatting', score, max: 6 }); SCORES.data_integrity.score += score; log(`得分: ${score}/6`); return score; } function testSchemaValidation() { log('\n--- 测试: schema 校验 (2分) ---'); let score = (CODE_TEXT.includes('schema') || CODE_TEXT.includes('validate') || CODE_TEXT.includes('joi') || CODE_TEXT.includes('zod')) ? 2 : 0; SCORES.data_integrity.details.push({ test: 'schema_validation', score, max: 2 }); SCORES.data_integrity.score += score; log(`得分: ${score}/2`); return score; } function testContentDetailsSupport() { log('\n--- 测试: contentDetails/duration 支持 (4分) ---'); let score = 0; if (CODE_TEXT.includes('contentDetails')) score += 2; if (CODE_TEXT.includes('duration') || CODE_TEXT.includes('PT')) score += 2; SCORES.data_integrity.details.push({ test: 'content_details', score, max: 4 }); SCORES.data_integrity.score += score; log(`得分: ${score}/4`); return score; } // ==================== 维度4: 代码质量与可维护性 (15分) ==================== function testModularity() { log('\n--- 测试: 模块化设计 (3分) ---'); let score = 0; const methods = ['_fetchFromAPI', '_fetchFromYTDLP', '_fetchFromInvidious', 'extract_video_info', 'get_trending', 'get_multi_region_trending']; const present = methods.filter(m => CODE_TEXT.includes(m)).length; score = Math.min(3, present); SCORES.code_quality.details.push({ test: 'modularity', score, max: 3, present }); SCORES.code_quality.score += score; log(`得分: ${score}/3`); return score; } function testDocumentation() { log('\n--- 测试: 注释与类型注解 (4分) ---'); let score = 0; const jsdocCount = (CODE_TEXT.match(/\/\*\*/g) || []).length; score += Math.min(2, jsdocCount >= 3 ? 2 : jsdocCount >= 1 ? 1 : 0); // 类型注解 (JSDoc @param / @returns) if (CODE_TEXT.includes('@param') && CODE_TEXT.includes('@returns')) score += 2; else if (CODE_TEXT.includes('@param') || CODE_TEXT.includes('@returns')) score += 1; SCORES.code_quality.details.push({ test: 'documentation', score, max: 4, jsdocCount }); SCORES.code_quality.score += score; log(`得分: ${score}/4`); return score; } function testInputValidation() { log('\n--- 测试: 输入验证 (3分) ---'); let score = 0; if (CODE_TEXT.includes('_validateRegionCode')) score += 1.5; if (CODE_TEXT.includes('maxResults') && (CODE_TEXT.includes('< 1') || CODE_TEXT.includes('> 50'))) score += 1; if (CODE_TEXT.includes('_validateCategoryId')) score += 0.5; SCORES.code_quality.details.push({ test: 'input_validation', score, max: 3 }); SCORES.code_quality.score += score; log(`得分: ${score}/3`); return score; } function testTestsAndEnv() { log('\n--- 测试: 测试覆盖与无硬编码 (5分) ---'); let score = 0; // evolve_v2 目录内是否有测试文件 const dir = __dirname; const files = fs.existsSync(dir) ? fs.readdirSync(dir) : []; const testFiles = files.filter(f => f.includes('test') || f.includes('spec')); score += testFiles.length > 0 ? 2 : 0; if (CODE_TEXT.includes('process.env.YOUTUBE_API_KEY')) score += 2; if (!/('|"|\`)AIza[\w-]{10,}/.test(CODE_TEXT)) score += 1; SCORES.code_quality.details.push({ test: 'tests_and_env', score, max: 5, testFiles }); SCORES.code_quality.score += score; log(`得分: ${score}/5`); return score; } // ==================== 维度5: 错误处理与降级策略 (15分) ==================== function testHttpErrorHandling() { log('\n--- 测试: HTTP 错误码分层处理 (5分) ---'); let score = 0; const codes = ['400', '403', '404', '429', '500', '502', '503']; const present = codes.filter(c => CODE_TEXT.includes(c)).length; score = Math.min(5, present * 0.8); SCORES.error_handling.details.push({ test: 'http_errors', score: +score.toFixed(2), max: 5, present }); SCORES.error_handling.score += score; log(`得分: ${score.toFixed(2)}/5`); return score; } function testRetryAndBackoff() { log('\n--- 测试: 重试与退避 (4分) ---'); let score = 0; if (CODE_TEXT.includes('retry') || CODE_TEXT.includes('attempt')) score += 2; if (CODE_TEXT.includes('setTimeout') && (CODE_TEXT.includes('Math.pow') || CODE_TEXT.includes('**') || CODE_TEXT.includes('exp'))) score += 2; else if (CODE_TEXT.includes('setTimeout')) score += 1; SCORES.error_handling.details.push({ test: 'retry_backoff', score, max: 4 }); SCORES.error_handling.score += score; log(`得分: ${score}/4`); return score; } function testTimeoutAndFallback() { log('\n--- 测试: 超时与降级链 (4分) ---'); let score = 0; if (CODE_TEXT.includes('timeout') || CODE_TEXT.includes('AbortSignal')) score += 1; if (CODE_TEXT.includes('30000') || CODE_TEXT.includes('30 * 1000')) score += 1; if (CODE_TEXT.includes('Falling back') || CODE_TEXT.includes('fallback') || CODE_TEXT.includes('yt-dlp')) score += 1; if (CODE_TEXT.includes('Invidious') || CODE_TEXT.includes('invidious')) score += 1; SCORES.error_handling.details.push({ test: 'timeout_fallback', score, max: 4 }); SCORES.error_handling.score += score; log(`得分: ${score}/4`); return score; } function testEmptyResultHandling() { log('\n--- 测试: 空结果与错误提示 (2分) ---'); let score = 0; if (CODE_TEXT.includes('items') && CODE_TEXT.includes('[]')) score += 1; if (CODE_TEXT.includes('throw new Error') && CODE_TEXT.includes('API error')) score += 1; SCORES.error_handling.details.push({ test: 'empty_results', score, max: 2 }); SCORES.error_handling.score += score; log(`得分: ${score}/2`); return score; } // ==================== 维度6: 扩展性与配置灵活性 (10分) ==================== function testConfigurability() { log('\n--- 测试: 参数可配置性 (5分) ---'); let score = 0; if (CODE_TEXT.includes('regionCode')) score += 1; if (CODE_TEXT.includes('videoCategoryId') || CODE_TEXT.includes('categoryId')) score += 1; if (CODE_TEXT.includes('maxResults')) score += 1; if (CODE_TEXT.includes('JSON') || CODE_TEXT.includes('json')) score += 1; if (CODE_TEXT.includes('CSV') || CODE_TEXT.includes('csv')) score += 1; else score += 0; SCORES.extensibility.details.push({ test: 'configurability', score, max: 5 }); SCORES.extensibility.score += score; log(`得分: ${score}/5`); return score; } function testBatchAndExtensibility() { log('\n--- 测试: 批量请求与扩展性 (5分) ---'); let score = 0; if (CODE_TEXT.includes('get_multi_region') || CODE_TEXT.includes('for') || CODE_TEXT.includes('map')) score += 2; // 易于添加新地区/分类 if (CODE_TEXT.includes('regionCode') && CODE_TEXT.includes('options')) score += 2; if (CODE_TEXT.includes('module.exports')) score += 1; SCORES.extensibility.details.push({ test: 'batch_extensibility', score, max: 5 }); SCORES.extensibility.score += score; log(`得分: ${score}/5`); return score; } // ==================== 维度7: 性能与效率 (5分) ==================== function testPerformance() { log('\n--- 测试: 缓存与性能 (5分) ---'); let score = 0; if (CODE_TEXT.includes('cache') || CODE_TEXT.includes('Cache')) score += 2; if (CODE_TEXT.includes('setTimeout') || CODE_TEXT.includes('delay')) score += 1; if (CODE_TEXT.includes('50')) score += 1; if (CODE_TEXT.includes('Promise.all') || CODE_TEXT.includes('concurrent') || CODE_TEXT.includes('dedup')) score += 1; SCORES.performance.details.push({ test: 'performance', score, max: 5 }); SCORES.performance.score += score; log(`得分: ${score}/5`); return score; } // ==================== 主流程 ==================== async function runBenchmark() { log('========================================'); log('YouTube Crawler Benchmark Test v2'); log('目标: >= 90'); log('========================================'); await testRequiredFields(); testCategorySupport(); testRegionSupport(); testOutputFormats(); testApiPartParameter(); testApiChartAndMaxResults(); testNoSearchList(); testQuotaAndRateLimit(); testApiKeyEnv(); await testDataTypes(); testDataFormatting(); testSchemaValidation(); testContentDetailsSupport(); testModularity(); testDocumentation(); testInputValidation(); testTestsAndEnv(); testHttpErrorHandling(); testRetryAndBackoff(); testTimeoutAndFallback(); testEmptyResultHandling(); testConfigurability(); testBatchAndExtensibility(); testPerformance(); // 计算总分 const total = Object.values(SCORES).reduce((sum, d) => sum + d.score, 0); const maxTotal = Object.values(SCORES).reduce((sum, d) => sum + d.max, 0); const dimensions = [ { name: '功能完整性', score: SCORES.functionality.score, max: SCORES.functionality.max, weight: 0.20 }, { name: 'API规范使用', score: SCORES.api_usage.score, max: SCORES.api_usage.max, weight: 0.15 }, { name: '数据完整性与准确性', score: SCORES.data_integrity.score, max: SCORES.data_integrity.max, weight: 0.20 }, { name: '代码质量与可维护性', score: SCORES.code_quality.score, max: SCORES.code_quality.max, weight: 0.15 }, { name: '错误处理与降级策略', score: SCORES.error_handling.score, max: SCORES.error_handling.max, weight: 0.15 }, { name: '扩展性与配置灵活性', score: SCORES.extensibility.score, max: SCORES.extensibility.max, weight: 0.10 }, { name: '性能与效率', score: SCORES.performance.score, max: SCORES.performance.max, weight: 0.05 }, ]; const weak = dimensions .filter(d => d.score < d.max * 0.75) .map(d => d.name); log('\n========================================'); log('评分汇总'); log('========================================'); for (const d of dimensions) { log(`${d.name}: ${d.score.toFixed(1).padStart(4)}/${d.max}`); } log('----------------------------------------'); log(`总分: ${total.toFixed(2)}/${maxTotal}`); log(`结果: ${total >= 90 ? '✅ 通过' : total >= 75 ? '⚠️ 需改进' : '❌ 未通过'}`); log('========================================'); const result = { total_score: +total.toFixed(2), max_score: maxTotal, target: 90, passed: total >= 90, dimensions: dimensions.map(d => ({ name: d.name, score: +d.score.toFixed(2), max: d.max, weight: d.weight })), details: SCORES, weak_dimensions: weak, timestamp: new Date().toISOString(), logs: LOGS }; fs.writeFileSync(path.join(__dirname, 'benchmark_result.json'), JSON.stringify(result, null, 2)); return result; } runBenchmark().then(r => { process.exit(r.passed ? 0 : 1); }).catch(err => { console.error(err); process.exit(1); });