防爬虫逆向日志爆炸,精简追踪不崩浏览器控制台 - 详解
概述
本文详细介绍了一种基于JavaScript的Web日志收集器实现,该实现支持多种数据类型检测、调用栈追踪和日志持久化功能避免控制台打印卡顿与奔溃。
技术背景
问题挑战
- 复杂数据类型处理:Web环境中存在多种数据类型,包括普通数组、类型化数组、函数、对象等
- 调用栈追踪:需要准确捕获和格式化方法调用链
- 性能监控:需要监控函数执行时间和性能指标
- 日志持久化:支持浏览器环境的日志保存
解决方案架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 类型检测器 │ │ 调用栈分析器 │ │ 日志格式化器 │
│ 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应用场景,有效避免控制台上万条打印导致浏览器奔溃。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/922223.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!相关文章
网站备案 有效期中小企业网络规划与设计方案
题目描述(力扣题库 84): 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。 解题思想: 单调栈: 利用先进后出的思想, 先算出长度更高的柱子所能勾勒…
使用 Jenkins 的流水线方案实施 CI/CD
使用 Jenkins 的流水线方案实施 CI/CDpre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mona…
抚松做网站wordpress小程序二开
这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。说起这个我是比较有感触的,之前我TM被删库…
湘潭网站建设出色磐石网络网站搭建详细教程
泛型,英文是generic。 泛型是一种参数化多态。就是把类型作为参数,使用时才指定具体类型。 这样一套代码可以应用于多种类型。比如Vec<T>,可以是整型向量Vec<i32>,也可以是浮点型向量Vec<f64>。
Rust中的泛型属…
重庆微信网站代理商把数据库wordpress
springboot注解扫描范围是由ComponentScan指定的;默认情况下为启动程序所在目录及其子包; 如果需要指定扫描路径,在启动程序中添加修改 ComponentScan(basePackages{"org.jstudio.a","org.jstudio.b"})
SpringBootApplication
public class xxx…
平顶山市城乡建设职工培训网站花生壳官网免费域名申请
目录
一、Linux的哲学思想
1.1 基础知识
1.2 根目录下的文件夹
二、Shell
1、Shell的定义
2、Shell的作用 三、Linux命令行
1、Linux通用命令行使用格式 四、Linux命令的分类
1、内部命令和外部命令的理解
2、内部命令和外部命令的区别
3、命令的执行过程
五、编辑…
网站建设公司上海大连高端网页公司
文章目录
管道相关命令
目标
准备工作
1 cut
1.1 目标
1.2 路径
1.3 实现
2 sort
2.1 目标
2.2 路径
2.3 实现
第一步: 对字符串排序
第二步:去重排序
第三步: 对数值排序
默认按照字符串排序
升序 -n
倒序 -r
第四步: 对成绩排序【按照列排序】 …
解析网站怎么做成都网站建设招标
解决办法:安装laptop-mode-tools工具包。1.检查是否安装了grep laptop-mode-tools 工具包$ dpkg -l | grep laptop-mode-tools如果执行命令无结果输出,表示未安装(如果已安装,忽略第2步)2.安装laptop-mode执行命令:$ sudo apt-get…
怎样做网站后台iis做网站文件下载
一、源码特点 java Web火车查询管理系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql…
南昌模板建站定制个人养老金
生成的符号表达式,可能会存在过于冗长的问题,且多个符号表达式中,有可能存在相同的计算部分,如果不进行处理,计算过程中会导致某些算式计算多次,从而影响计算效率。
那么多个符号表达式生成函数时…
网站导航内链建设机加工订单网
一.Spring基础
1.Spring 框架是什么
Spring 是一款开源的轻量级 Java 开发框架,我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,例如,Spring core、Spring JDBC、Spring MVC 等,使用这些模块可…
记录---window.close()失效 + Chrome浏览器调试线上代码
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
情况说明主系统单点登录点击触发window.open()打开本系统。
是发布生产后的新需求:要求退出登录后直接关闭当前系统页面。
本地运行增加了window.close()方法实…
启发式合并 [USACO22DEC] Making Friends P
题意
\(N\) 牛 \(M\) 关系,按照编号从小到大,牛依次离开,每一头牛离开时它认识的牛会互相认识,求最后新增了多少朋友关系。
\(N,M\le 2\times 10^5\)
解法
我们将操作看成每个点边集合的合并,尝试使用启发式合并解…
ps企业站网站做多大网站建设功能覆盖范围
Truth
Truth 是用于Java测试的断言框架,灵感来自于FEST,并受到一些可扩展性需求的驱动,几乎完全由谷歌员工在业余时间编写,或者作为Java核心图书馆管理员的身份做出贡献。
作用
作为工程师,我们花费大部分的时间来阅…
动易的网站能否静态提供o2o网站建设
#文件防泄密软件#
中科数安是一家专注于信息安全领域的高科技企业,其提供的防止公司内部文件资料及数据外泄的解决方案主要包括图档透明加密和源代码防泄露系统等核心服务。 中科数安 | 图档、源代码防止外泄系统 PC地址:
www.weaem.com 1. 图档透明加…
【多线程】什么是原子操作(Atomic Operation)? - 详解
pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …