防爬虫逆向日志爆炸,精简追踪不崩浏览器控制台 - 详解

概述

本文详细介绍了一种基于JavaScript的Web日志收集器实现,该实现支持多种数据类型检测、调用栈追踪和日志持久化功能避免控制台打印卡顿与奔溃。

技术背景

问题挑战

  1. 复杂数据类型处理:Web环境中存在多种数据类型,包括普通数组、类型化数组、函数、对象等
  2. 调用栈追踪:需要准确捕获和格式化方法调用链
  3. 性能监控:需要监控函数执行时间和性能指标
  4. 日志持久化:支持浏览器环境的日志保存

解决方案架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   类型检测器      │    │   调用栈分析器    │    │   日志格式化器    │
│  TypeChecker    │    │ StackAnalyzer   │    │ LogFormatter    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────────┐
                    │    日志收集器     │
                    │ WebLogCollector │
                    └─────────────────┘
                                 │
                    ┌─────────────────┐
                    │    持久化管理器   │
                    │ LogPersistence  │
                    └─────────────────┘

核心组件详解

1. 类型检测器 (TypeChecker)

类型检测器负责识别和处理各种JavaScript数据类型:

const TypeChecker = {
// 检查是否为类型化数组
isTypedArray(value) {
return value instanceof ArrayBuffer ||
value instanceof Int8Array ||
value instanceof Uint8Array ||
// ... 其他类型化数组
},
// 检查是否为普通数组
isArray(value) {
return Array.isArray(value);
},
// 检查是否为函数
isFunction(value) {
return typeof value === 'function';
}
};

技术要点

  • 支持所有ES6+类型化数组类型
  • 区分普通数组和类型化数组
  • 处理函数和对象的特殊检测

2. 调用栈分析器 (StackAnalyzer)

调用栈分析器提供精确的方法调用追踪:

class StackAnalyzer {
static getStackTrace() {
try {
throw new Error();
} catch (error) {
return error.stack;
}
}
static parseCallChain(skipFrames = 2) {
const stack = this.getStackTrace();
const stackLines = stack.split('\n').slice(skipFrames);
return stackLines.map(line => {
const methodMatch = line.match(/at\s+(.+?)\s+\(/);
const fileMatch = line.match(/\((.+?):(\d+):(\d+)\)/);
if (methodMatch && fileMatch) {
const fileName = fileMatch[1].split('/').pop();
return `${methodMatch[1]}@${fileName}:${fileMatch[2]}`;
}
return methodMatch ? methodMatch[1] : line.trim();
}).join(' → ');
}
}

技术要点

  • 通过抛出错误获取调用栈信息
  • 正则表达式解析方法名和文件信息
  • 支持跳过指定数量的栈帧

3. 日志格式化器 (LogFormatter)

日志格式化器处理各种数据类型的序列化:

class LogFormatter {
static formatValue(value, depth = 0) {
const maxDepth = 3;
if (depth > maxDepth) return '[深度超限]';
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (TypeChecker.isFunction(value)) {
return `[Function: ${value.name || 'anonymous'}]`;
}
if (TypeChecker.isArray(value)) {
if (value.some(item => TypeChecker.isObject(item))) {
return `[${value.map(item => this.formatValue(item, depth + 1)).join(', ')}]`;
}
return `[${value.join(', ')}] (长度: ${value.length})`;
}
if (TypeChecker.isTypedArray(value)) {
return `[${value.constructor.name}] (长度: ${value.length})`;
}
if (TypeChecker.isObject(value)) {
try {
return JSON.stringify(value, null, 2);
} catch (error) {
if (error.message.includes('circular')) {
return '[循环引用对象]';
}
return `[对象: ${value.constructor?.name || 'Object'}]`;
}
}
return String(value);
}
}

技术要点

  • 递归深度控制防止无限循环
  • 循环引用检测和处理
  • 类型化数组特殊处理
  • 函数名提取和格式化

4. 日志持久化管理器 (LogPersistence)

支持多环境的日志保存:

class LogPersistence {
constructor() {
this.isNodeEnvironment = typeof window === 'undefined';
}
saveToFile(content, filename) {
if (this.isNodeEnvironment) {
this.saveToNodeFile(content, filename);
} else {
this.saveToBrowserFile(content, filename);
}
}
saveToBrowserFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}

技术要点

  • 环境检测和适配
  • Blob API用于浏览器文件下载
  • 内存管理(URL.revokeObjectURL)

主日志收集器实现

核心功能

class WebLogCollector {
constructor(options = {}) {
this.options = {
enableConsole: true,
maxLogCount: 10000,
autoSave: true,
logLevel: 'info',
enableStackTrace: true,
...options
};
this.logs = [];
this.logCounter = 0;
this.persistence = new LogPersistence();
this.startTime = Date.now();
this.bindToGlobal();
}
recordLog(level, ...args) {
const timestamp = LogFormatter.formatTimestamp();
const callChain = this.options.enableStackTrace ?
StackAnalyzer.parseCallChain() : 'N/A';
const methodName = StackAnalyzer.getCurrentMethod();
const formattedArgs = args.map(arg =>
LogFormatter.formatValue(arg)
).join(' ');
const logEntry = {
id: ++this.logCounter,
timestamp,
level,
method: methodName,
callChain,
message: formattedArgs,
duration: Date.now() - this.startTime
};
this.logs.push(logEntry);
if (this.options.enableConsole) {
const consoleMethod = console[level] || console.log;
consoleMethod(`[${timestamp}] [${level.toUpperCase()}] ${formattedArgs}`);
}
if (this.options.autoSave && this.logs.length >= this.options.maxLogCount) {
this.exportLogs();
}
}
}

性能监控功能

// 同步性能监控
performance(name, fn) {
const start = performance.now();
this.info(`开始执行: ${name}`);
try {
const result = fn();
const end = performance.now();
this.info(`完成执行: ${name}, 耗时: ${(end - start).toFixed(2)}ms`);
return result;
} catch (error) {
const end = performance.now();
this.error(`执行失败: ${name}, 耗时: ${(end - start).toFixed(2)}ms, 错误: ${error.message}`);
throw error;
}
}
// 异步性能监控
async performanceAsync(name, fn) {
const start = performance.now();
this.info(`开始执行异步任务: ${name}`);
try {
const result = await fn();
const end = performance.now();
this.info(`完成异步任务: ${name}, 耗时: ${(end - start).toFixed(2)}ms`);
return result;
} catch (error) {
const end = performance.now();
this.error(`异步任务失败: ${name}, 耗时: ${(end - start).toFixed(2)}ms, 错误: ${error.message}`);
throw error;
}
}

使用示例

基础使用

// 初始化日志收集器
const collector = new WebLogCollector({
enableConsole: true,
maxLogCount: 5000,
autoSave: true
});
// 基础日志记录
collector.info('用户登录', { userId: 123, username: 'john' });
collector.warn('API响应时间过长', { duration: 5000 });
collector.error('数据库连接失败', error);
// 性能监控
const result = collector.performance('数据处理', () => {
return processData(largeDataset);
});
// 异步性能监控
const asyncResult = await collector.performanceAsync('API调用', async () => {
return await fetch('/api/data');
});

爬虫场景应用

// 爬虫页面分析
collector.info('开始爬取页面', { url: targetUrl });
collector.debug('页面元素数量', document.querySelectorAll('*').length);
// 数据提取监控
collector.performance('数据提取', () => {
const data = extractDataFromPage();
collector.info('提取数据完成', { count: data.length });
return data;
});
// 错误处理
try {
await performCrawling();
} catch (error) {
collector.error('爬虫执行失败', {
error: error.message,
stack: error.stack,
url: window.location.href
});
}

日志导出和分析

// 导出不同格式的日志
collector.exportLogs('json');  // JSON格式
collector.exportLogs('txt');  // 文本格式
collector.exportLogs('csv');  // CSV格式
// 获取统计信息
const stats = collector.getStatistics();
console.log('日志统计:', stats);
/*
输出示例:
{
totalLogs: 1250,
byLevel: { info: 800, warn: 200, error: 50 },
byMethod: { 'crawlPage': 100, 'extractData': 150 },
averageDuration: 125.5,
startTime: 1640995200000,
currentTime: 1640995800000
}
*/

高级特性

1. 内存管理

// 自动清理机制
if (this.logs.length >= this.options.maxLogCount) {
this.exportLogs();
this.logs = []; // 清空内存
}

2. 循环引用处理

// 检测和处理循环引用
try {
return JSON.stringify(value, null, 2);
} catch (error) {
if (error.message.includes('circular')) {
return '[循环引用对象]';
}
return `[对象: ${value.constructor?.name || 'Object'}]`;
}

性能优化建议

1. 日志级别控制

const collector = new WebLogCollector({
logLevel: 'warn', // 只记录警告和错误
enableStackTrace: false // 生产环境关闭调用栈追踪
});

2. 批量处理

// 批量记录日志
const batchLogs = [];
for (let i = 0; i < 1000; i++) {
batchLogs.push({ id: i, data: `item-${i}` });
}
collector.info('批量数据', batchLogs);

3. 异步日志处理

// 异步日志记录
async function asyncLog(level, ...args) {
return new Promise(resolve => {
setTimeout(() => {
collector.recordLog(level, ...args);
resolve();
}, 0);
});
}

最佳实践

1. 日志结构化

// 结构化日志记录
collector.info('用户登陆信息', {
action: 'login',
userId: user.id,
timestamp: Date.now(),
userAgent: navigator.userAgent,
sessionId: session.id
});

2. 错误上下文

// 完整的错误上下文
try {
await riskyOperation();
} catch (error) {
collector.error('操作失败', {
error: error.message,
stack: error.stack,
context: {
userId: currentUser.id,
operation: 'dataProcessing',
inputData: sanitizedInput
}
});
}

3. 性能监控

// 关键路径性能监控
collector.performance('关键业务逻辑', () => {
return executeBusinessLogic();
});

源码

/**
* Web日志收集器 - 用于爬虫注入和调试追踪
* 支持多种数据类型检测、调用栈追踪和日志持久化
*/
// 类型化数组检测工具
const TypeChecker = {
// 检查是否为类型化数组
isTypedArray(value) {
return value instanceof ArrayBuffer ||
value instanceof Int8Array ||
value instanceof Uint8Array ||
value instanceof Uint8ClampedArray ||
value instanceof Int16Array ||
value instanceof Uint16Array ||
value instanceof Int32Array ||
value instanceof Uint32Array ||
value instanceof Float32Array ||
value instanceof Float64Array ||
value instanceof BigInt64Array ||
value instanceof BigUint64Array;
},
// 检查是否为普通数组
isArray(value) {
return Array.isArray(value);
},
// 检查是否为函数
isFunction(value) {
return typeof value === 'function';
},
// 检查是否为对象
isObject(value) {
return value !== null && typeof value === 'object';
}
};
// 调用栈分析器
class StackAnalyzer {
/**
* 获取当前调用栈信息
* @returns {string} 格式化的调用栈字符串
*/
static getStackTrace() {
try {
throw new Error();
} catch (error) {
return error.stack;
}
}
/**
* 解析并格式化调用栈
* @param {number} skipFrames - 跳过的栈帧数量
* @returns {string} 格式化的调用链
*/
static parseCallChain(skipFrames = 2) {
const stack = this.getStackTrace();
const stackLines = stack.split('\n').slice(skipFrames);
const methodNames = stackLines.map(line => {
// 匹配方法名和文件信息
const methodMatch = line.match(/at\s+(.+?)\s+\(/);
const fileMatch = line.match(/\((.+?):(\d+):(\d+)\)/);
if (methodMatch) {
const methodName = methodMatch[1];
if (fileMatch) {
const fileName = fileMatch[1].split('/').pop();
const lineNumber = fileMatch[2];
return `${methodName}@${fileName}:${lineNumber}`;
}
return methodName;
}
return line.trim();
});
return methodNames
.filter(name => name && !name.includes('StackAnalyzer'))
.join(' → ');
}
/**
* 获取当前执行的方法名
* @returns {string} 当前方法名
*/
static getCurrentMethod() {
const stack = this.getStackTrace();
const lines = stack.split('\n');
const currentLine = lines[2] || lines[1];
const match = currentLine.match(/at\s+(.+?)\s+\(/);
return match ? match[1] : 'unknown';
}
}
// 日志格式化器
class LogFormatter {
/**
* 格式化日志参数
* @param {any} value - 要格式化的值
* @param {number} depth - 递归深度
* @returns {string} 格式化后的字符串
*/
static formatValue(value, depth = 0) {
const maxDepth = 3;
if (depth > maxDepth) {
return '[深度超限]';
}
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (TypeChecker.isFunction(value)) {
return `[Function: ${value.name || 'anonymous'}]`;
}
if (TypeChecker.isArray(value)) {
if (value.length === 0) return '[]';
if (value.some(item => TypeChecker.isObject(item))) {
return `[${value.map(item => this.formatValue(item, depth + 1)).join(', ')}]`;
}
return `[${value.join(', ')}] (长度: ${value.length})`;
}
if (TypeChecker.isTypedArray(value)) {
return `[${value.constructor.name}] (长度: ${value.length})`;
}
if (TypeChecker.isObject(value)) {
try {
return JSON.stringify(value, null, 2);
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('circular')) {
return '[循环引用对象]';
}
return `[对象: ${value.constructor?.name || 'Object'}]`;
}
}
return String(value);
}
/**
* 格式化时间戳
* @returns {string} 格式化的时间戳
*/
static formatTimestamp() {
const now = new Date();
return now.toISOString().replace('T', ' ').replace('Z', '');
}
}
// 日志持久化管理器
class LogPersistence {
constructor() {
this.isNodeEnvironment = typeof window === 'undefined';
}
/**
* 保存日志到文件(浏览器环境)
* @param {string} content - 日志内容
* @param {string} filename - 文件名
*/
saveToFile(content, filename) {
if (this.isNodeEnvironment) {
this.saveToNodeFile(content, filename);
} else {
this.saveToBrowserFile(content, filename);
}
}
/**
* 浏览器环境保存
* @param {string} content - 日志内容
* @param {string} filename - 文件名
*/
saveToBrowserFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Node.js环境保存
* @param {string} content - 日志内容
* @param {string} filename - 文件名
*/
saveToNodeFile(content, filename) {
try {
const fs = require('fs');
const path = require('path');
const os = require('os');
const logDir = path.join(os.homedir(), 'web-logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const filePath = path.join(logDir, filename);
fs.writeFileSync(filePath, content, 'utf8');
console.log(`日志已保存到: ${filePath}`);
} catch (error) {
console.error('保存日志文件失败:', error);
}
}
}
// 主日志收集器类
class WebLogCollector {
constructor(options = {}) {
this.options = {
enableConsole: true,
maxLogCount: 10000,
autoSave: true,
logLevel: 'info',
enableStackTrace: true,
...options
};
this.logs = [];
this.logCounter = 0;
this.persistence = new LogPersistence();
this.startTime = Date.now();
// 绑定方法到全局作用域
this.bindToGlobal();
}
/**
* 绑定方法到全局作用域
*/
bindToGlobal() {
if (typeof window !== 'undefined') {
window.logCollector = this;
window.trace = this.trace.bind(this);
window.debug = this.debug.bind(this);
window.info = this.info.bind(this);
window.warn = this.warn.bind(this);
window.error = this.error.bind(this);
}
}
/**
* 记录日志
* @param {string} level - 日志级别
* @param {any} args - 日志参数
*/
recordLog(level, ...args) {
const timestamp = LogFormatter.formatTimestamp();
const callChain = this.options.enableStackTrace ?
StackAnalyzer.parseCallChain() : 'N/A';
const methodName = StackAnalyzer.getCurrentMethod();
const formattedArgs = args.map(arg =>
LogFormatter.formatValue(arg)
).join(' ');
const logEntry = {
id: ++this.logCounter,
timestamp,
level,
method: methodName,
callChain,
message: formattedArgs,
duration: Date.now() - this.startTime
};
this.logs.push(logEntry);
if (this.options.enableConsole) {
const consoleMethod = console[level] || console.log;
consoleMethod(`[${timestamp}] [${level.toUpperCase()}] ${formattedArgs}`);
}
if (this.options.autoSave && this.logs.length >= this.options.maxLogCount) {
this.exportLogs();
}
}
/**
* 追踪级别日志
* @param {...any} args - 日志参数
*/
trace(...args) {
this.recordLog('trace', ...args);
}
/**
* 调试级别日志
* @param {...any} args - 日志参数
*/
debug(...args) {
this.recordLog('debug', ...args);
}
/**
* 信息级别日志
* @param {...any} args - 日志参数
*/
info(...args) {
this.recordLog('info', ...args);
}
/**
* 警告级别日志
* @param {...any} args - 日志参数
*/
warn(...args) {
this.recordLog('warn', ...args);
}
/**
* 错误级别日志
* @param {...any} args - 日志参数
*/
error(...args) {
this.recordLog('error', ...args);
}
/**
* 性能监控日志
* @param {string} name - 性能标记名称
* @param {Function} fn - 要监控的函数
* @returns {any} 函数执行结果
*/
performance(name, fn) {
const start = performance.now();
this.info(`开始执行: ${name}`);
try {
const result = fn();
const end = performance.now();
this.info(`完成执行: ${name}, 耗时: ${(end - start).toFixed(2)}ms`);
return result;
} catch (error) {
const end = performance.now();
this.error(`执行失败: ${name}, 耗时: ${(end - start).toFixed(2)}ms, 错误: ${error.message}`);
throw error;
}
}
/**
* 异步性能监控
* @param {string} name - 性能标记名称
* @param {Function} fn - 要监控的异步函数
* @returns {Promise<any>} 函数执行结果
  */
  async performanceAsync(name, fn) {
  const start = performance.now();
  this.info(`开始执行异步任务: ${name}`);
  try {
  const result = await fn();
  const end = performance.now();
  this.info(`完成异步任务: ${name}, 耗时: ${(end - start).toFixed(2)}ms`);
  return result;
  } catch (error) {
  const end = performance.now();
  this.error(`异步任务失败: ${name}, 耗时: ${(end - start).toFixed(2)}ms, 错误: ${error.message}`);
  throw error;
  }
  }
  /**
  * 导出日志
  * @param {string} format - 导出格式 ('json' | 'txt' | 'csv')
  */
  exportLogs(format = 'json') {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  let content, filename;
  switch (format) {
  case 'json':
  content = JSON.stringify(this.logs, null, 2);
  filename = `web-logs-${timestamp}.json`;
  break;
  case 'txt':
  content = this.logs.map(log =>
  `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.method}: ${log.message}`
  ).join('\n');
  filename = `web-logs-${timestamp}.txt`;
  break;
  case 'csv':
  content = 'ID,Timestamp,Level,Method,CallChain,Message,Duration\n' +
  this.logs.map(log =>
  `${log.id},"${log.timestamp}",${log.level},"${log.method}","${log.callChain}","${log.message}",${log.duration}`
  ).join('\n');
  filename = `web-logs-${timestamp}.csv`;
  break;
  default:
  throw new Error(`不支持的导出格式: ${format}`);
  }
  this.persistence.saveToFile(content, filename);
  this.logs = []; // 清空日志
  }
  /**
  * 获取日志统计信息
  * @returns {Object} 统计信息
  */
  getStatistics() {
  const stats = {
  totalLogs: this.logs.length,
  byLevel: {},
  byMethod: {},
  averageDuration: 0,
  startTime: this.startTime,
  currentTime: Date.now()
  };
  this.logs.forEach(log => {
  stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
  stats.byMethod[log.method] = (stats.byMethod[log.method] || 0) + 1;
  });
  if (this.logs.length > 0) {
  stats.averageDuration = this.logs.reduce((sum, log) => sum + log.duration, 0) / this.logs.length;
  }
  return stats;
  }
  /**
  * 清空日志
  */
  clear() {
  this.logs = [];
  this.logCounter = 0;
  this.startTime = Date.now();
  }
  }
  // 创建全局实例
  const webLogCollector = new WebLogCollector();

总结

本文介绍的Web日志收集器提供了完整的日志记录、调用栈追踪和性能监控解决方案。该方案适用于各种Web应用场景,有效避免控制台上万条打印导致浏览器奔溃。

posted on 2025-09-29 20:19  slgkaifa  阅读(15)  评论(0)    收藏  举报

导航