Python str.translate + Base64 = 常见的 URL-Safe 变体或混淆
看到 Python 代码里: b64 = base64.b64encode(data.encode("utf-8")).decode("ascii") return b64.translate(TRANS)这里的 .translate() 不是 Base64 库的方法——是 Python 字符串内置的 str.translate()。整个逻辑其实两步:b64encode 把数据编成标准 Base64 字符串 translate(TRANS) 按映射表把字符逐个替换常见用途:URL-Safe Base64、简单混淆、自定义字母表。 str.translate 是什么 str.translate(table) 按照 table 里的映射一次性替换多个字符——比一堆 .replace() 快得多: TRANS = str.maketrans({"a": "x", "b": "y"}) "abc".translate(TRANS) # "xyc"maketrans 三种参数形式: # 字典:字符 → 字符 str.maketrans({"a": "x", "b": "y"})# 两个字符串:一一对应 str.maketrans("abc", "xyz") # a→x, b→y, c→z# 三个参数:第三个是"要删除的字符" str.maketrans("", "", "aeiou") # 删除所有元音 "hello world".translate(...) # "hll wrld"底层是 dict[int, int|None],translate 每个字符按 code point 查表。 URL-Safe Base64(最常见用途) 标准 Base64 用了 + 和 /,在 URL 里出现要 percent-encode(%2B %2F)。URL-safe 变体换掉这两个字符: TRANS = str.maketrans({"+": "-", "/": "_"})b64 = base64.b64encode(data).decode("ascii") url_safe = b64.translate(TRANS)其实 Python 标准库直接提供了: base64.urlsafe_b64encode(data).decode("ascii")自己写 translate 版本效果一样,一般是兼容其它平台的历史代码。 反向:URL-Safe → 标准 TRANS_BACK = str.maketrans({"-": "+", "_": "/"})standard_b64 = url_safe.translate(TRANS_BACK)# 补 padding(长度必须是 4 的倍数) padded = standard_b64 + "=" * (-len(standard_b64) % 4)data = base64.b64decode(padded)URL-safe Base64 通常省掉了 = padding——解码时得手动补。 完全自定义字母表 某些爬虫 / 反调试代码会用非标准字母表做混淆: CUSTOM_ALPHABET = "NOPQRSTUVWXYZABCDEFGHIJKLM" + \ "nopqrstuvwxyzabcdefghijklm" + \ "0123456789-_"STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \ "abcdefghijklmnopqrstuvwxyz" + \ "0123456789+/"TRANS = str.maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET)编码时把标准 Base64 输出通过 TRANS 转成"看起来不一样但结构相同"的字符串。解码时反向映射再走标准 b64decode。 这是很低强度的混淆——攻击者拿到映射表就完全还原了。但作为"不让日志里的密码一眼可读"够用。 性能对比 translate 一次映射多个字符,比 .replace() 快很多: import timeits = "a" * 10000t1 = timeit.timeit( "s.replace('a', 'x').replace('b', 'y')", globals={"s": s}, number=10000, )TRANS = str.maketrans({"a": "x", "b": "y"}) t2 = timeit.timeit( "s.translate(TRANS)", globals={"s": s, "TRANS": TRANS}, number=10000, )print(t1 / t2) # translate 一般快 3-5 倍尤其映射表大的时候差距明显。 常见坑 1. maketrans 键值长度必须一样(字符串形式) str.maketrans("abc", "xy") # 报错:长度不匹配 str.maketrans("abc", "xyz") # OK字典形式没这个限制。 2. translate 对 bytes 用 bytes 表 b"abc".translate(bytes.maketrans(b"a", b"x"))字符串和字节的 translate 不通用。 3. 删除字符时用 None TRANS = {ord("a"): None} "abc".translate(TRANS) # "bc"或者用 str.maketrans("", "", "a") 生成的表。 一句话总结 看到 str.translate(TRANS) + Base64 的组合,大概率是做 URL-safe Base64 或简单字母表混淆。Python 标准库直接有 urlsafe_b64encode,自己用 translate 主要是历史代码。真正的性能优势在于批量替换比多次 replace 快得多。
Three.js 点云矩形框选:屏幕空间投影法
用 Three.js 显示点云(THREE.Points)后,做"鼠标拖矩形框选一批点"这类交互,标准做法是屏幕空间投影——把每个 3D 点 project 到 2D 屏幕,判是否落在鼠标矩形里。 为什么不用 Raycaster Three.js 的 Raycaster 主要针对射线,判"鼠标下选中哪个物体"用得多。但做矩形框选用它效率极差——要么在 CPU 上做 AABB,要么写 GPU shader,两条路都比"直接投影"复杂得多。 生产上标注工具、CAD、自动驾驶点云可视化基本都用屏幕空间法。 整体流程 鼠标 mousedown → 记录 (startX, startY) mousemove → 更新矩形 DOM mouseup → 遍历 points.geometry ├─ 每个点 → applyMatrix4(worldMatrix) ├─ → .project(camera) 转 NDC ├─ → NDC → 屏幕坐标 └─ 判断是否落在矩形内完整最小实现 先做一个 DOM 矩形跟着鼠标: const box = document.createElement("div"); box.style.cssText = ` position: fixed; border: 1px solid #00aaff; background: rgba(0,170,255,0.1); pointer-events: none; display: none; `; document.body.appendChild(box);let startX = 0, startY = 0, endX = 0, endY = 0, dragging = false;window.addEventListener("mousedown", e => { dragging = true; startX = e.clientX; startY = e.clientY; Object.assign(box.style, { left: startX + "px", top: startY + "px", width: "0px", height: "0px", display: "block", }); });window.addEventListener("mousemove", e => { if (!dragging) return; endX = e.clientX; endY = e.clientY; Object.assign(box.style, { left: Math.min(startX, endX) + "px", top: Math.min(startY, endY) + "px", width: Math.abs(endX - startX) + "px", height: Math.abs(endY - startY) + "px", }); });window.addEventListener("mouseup", () => { dragging = false; box.style.display = "none"; selectPoints(); });框选核心 function selectPoints() { const minX = Math.min(startX, endX); const maxX = Math.max(startX, endX); const minY = Math.min(startY, endY); const maxY = Math.max(startY, endY); const attr = pointCloud.geometry.attributes.position; const vec = new THREE.Vector3(); const selected = []; pointCloud.updateMatrixWorld(); for (let i = 0; i < attr.count; i++) { vec.fromBufferAttribute(attr, i); vec.applyMatrix4(pointCloud.matrixWorld); // 本地 → 世界 vec.project(camera); // 世界 → NDC (-1 ~ 1) // NDC → 屏幕 const sx = (vec.x * 0.5 + 0.5) * window.innerWidth; const sy = (-vec.y * 0.5 + 0.5) * window.innerHeight; if (sx >= minX && sx <= maxX && sy >= minY && sy <= maxY) { selected.push(i); } } console.log("选中", selected.length, "个点"); highlight(selected); }高亮选中的点 改颜色最简单的做法:给点云加一个 color attribute。 const geo = pointCloud.geometry; if (!geo.attributes.color) { const colors = new Float32Array(geo.attributes.position.count * 3); colors.fill(1); // 全白 geo.setAttribute("color", new THREE.BufferAttribute(colors, 3)); pointCloud.material.vertexColors = true; }function highlight(indices) { const colors = geo.attributes.color; // 先全部还原 for (let i = 0; i < colors.count; i++) colors.setXYZ(i, 1, 1, 1); // 选中的染红 for (const i of indices) colors.setXYZ(i, 1, 0, 0); colors.needsUpdate = true; }剔除相机背后的点 vec.project(camera) 对相机背后的点 z 会 > 1,也可能被误判进屏幕矩形。加一道过滤: if (vec.z < -1 || vec.z > 1) continue; // 相机视锥体外性能 十万点级别这套办法很流畅(一次 mouseup 内做完)。到一百万点就要考虑:用 Worker 并发做投影 GPU picking:把点索引编码到颜色渲染到 offscreen canvas,读像素判索引 空间分区(Octree/KDTree)预筛选一般标注场景十万级足够。 一句话总结 点云框选 = 每个点 project 到屏幕 + 矩形内包含判定。做完投影后加"背后点剔除",用 color attribute 高亮,是最直接可用的方案。
Three.js 点云矩形框选:屏幕投影与 SelectionBox
三种方案概览方案 适合场景 复杂度屏幕空间投影 通用标注、框选 低SelectionBox(Frustum) 需要真 3D 选取(不受遮挡) 中GPU Picking 超大点云(百万级)性能优先 高方案一:屏幕空间投影(推荐) 原理:把每个 3D 点投影到屏幕坐标,判断是否落在鼠标拖出的矩形内。 绘制选择框 const box = document.createElement('div'); box.style.cssText = ` position: fixed; border: 1px solid #00aaff; background: rgba(0, 170, 255, 0.1); pointer-events: none; `; document.body.appendChild(box);let startX = 0, startY = 0, endX = 0, endY = 0; let dragging = false;window.addEventListener('mousedown', e => { dragging = true; startX = endX = e.clientX; startY = endY = e.clientY; box.style.display = 'block'; });window.addEventListener('mousemove', e => { if (!dragging) return; endX = e.clientX; endY = e.clientY; const left = Math.min(startX, endX); const top = Math.min(startY, endY); box.style.left = left + 'px'; box.style.top = top + 'px'; box.style.width = Math.abs(endX - startX) + 'px'; box.style.height = Math.abs(endY - startY) + 'px'; });window.addEventListener('mouseup', () => { dragging = false; box.style.display = 'none'; selectPoints(); });框选核心逻辑 function selectPoints() { const minX = Math.min(startX, endX); const maxX = Math.max(startX, endX); const minY = Math.min(startY, endY); const maxY = Math.max(startY, endY); const positions = pointCloud.geometry.attributes.position; const selected = []; const vector = new THREE.Vector3(); for (let i = 0; i < positions.count; i++) { vector.fromBufferAttribute(positions, i); // 应用模型变换 vector.applyMatrix4(pointCloud.matrixWorld); // 投影到 NDC(Normalized Device Coordinates) vector.project(camera); // NDC [-1,1] 转屏幕像素 const sx = (vector.x * 0.5 + 0.5) * window.innerWidth; const sy = (vector.y * -0.5 + 0.5) * window.innerHeight; if (sx >= minX && sx <= maxX && sy >= minY && sy <= maxY) { selected.push(i); } } console.log('选中点数:', selected.length); highlightPoints(selected); }高亮选中点 function highlightPoints(indices) { const colors = pointCloud.geometry.attributes.color; // 重置所有点为白色 for (let i = 0; i < colors.count; i++) { colors.setXYZ(i, 1, 1, 1); } // 选中点设为红色 for (const idx of indices) { colors.setXYZ(idx, 1, 0, 0); } colors.needsUpdate = true; }方案二:官方 SelectionBox(3D Frustum) Three.js examples 提供了现成的框选工具: import { SelectionBox } from 'three/addons/interactive/SelectionBox.js'; import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js';const selectionBox = new SelectionBox(camera, scene); const helper = new SelectionHelper(renderer, 'selectBox');// CSS:.selectBox { border: 1px solid #55aaff; background: rgba(75,160,255,.1); }document.addEventListener('pointerdown', e => { selectionBox.startPoint.set( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1, 0.5 ); });document.addEventListener('pointermove', e => { if (!helper.isDown) return; selectionBox.endPoint.set( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1, 0.5 ); // 实时更新选中(可选,性能开销大) // selectionBox.select(); });document.addEventListener('pointerup', () => { const allSelected = selectionBox.select(); // allSelected 是 Object3D 数组(Mesh/Points 等) console.log(allSelected); });SelectionBox 用 Frustum 判断,不受相机遮挡影响,适合需要选取被遮挡点的场景。 方案三:GPU Picking 对百万级点云,CPU 遍历太慢,改用 Render-To-Texture(RTT):每个点用唯一颜色(编码点 index)渲染到离屏 FBO 读取矩形区域的像素颜色 解码颜色得到点 index// 伪代码 renderer.setRenderTarget(pickingTexture); renderer.render(scene, camera); // 用 picking shader 渲染const pixels = new Uint8Array(width * height * 4); renderer.readRenderTargetPixels(pickingTexture, x, y, width, height, pixels);const selected = decodeIndices(pixels); renderer.setRenderTarget(null);适合:自动驾驶标注平台、大规模点云编辑器等专业场景。 注意事项矩形太小时:endX - startX < 2 可跳过框选避免误触 相机控件冲突:拖动框选时需要禁用 OrbitControls(controls.enabled = false) 性能:方案一遍历所有点,百万级点云需要 Worker 或 GPU 方案
JS 判断空值:不把 0 当空的几种写法
JS 里的"空值"判断经常被 !value 搞坏——它会把 0、false、NaN 都当成空,实际业务里这三个通常是有效值。 不推荐:!value if (!value) { /* 错误率很高 */ }会把这些都当"空":null、undefined ✓(确实空) '' ✓(确实空) 0 ✗(数字零是有效值) false ✗(布尔假是有效值) NaN ✗(通常需要单独处理)推荐:精确判断 if (value == null || value === '') { // 空值:null、undefined、空字符串 }value == null 利用了宽松相等,同时捕获 null 和 undefined,不会误伤 0 或 false。 封装成工具函数: function isEmpty(value) { return value == null || value === ''; }isEmpty(null) // true isEmpty(undefined) // true isEmpty('') // trueisEmpty(0) // false isEmpty(false) // false isEmpty([]) // false(空数组不是空) isEmpty({}) // false(空对象不是空)加上空白字符串处理 如果 " " 也要算空: function isEmpty(value) { return value == null || (typeof value === 'string' && value.trim() === ''); }isEmpty(' ') // true isEmpty(' x ') // false用 ??= 做默认值赋值 ES2021 的空值合并赋值更精确: let count = 0; count ||= 10; // count = 10,因为 0 是 falsy(错误) count ??= 10; // count = 0,因为 0 不是 null/undefined(正确)只想在 null / undefined 时设默认值,用 ??= 而不是 ||=。 几种场景对应的写法场景 推荐写法只判断 null/undefined value == nullnull/undefined/空字符串 value == null || value === ''加上空白字符串 value == null || String(value).trim() === ''设默认值(0 是有效值) value ??= defaultVal设默认值(0 也算空) value ||= defaultVal表单校验里的典型误用 // ❌ 数量字段输入 0,会被误判为"未填写" if (!form.quantity) { showError('数量不能为空'); }// ✓ 正确写法 if (form.quantity == null || form.quantity === '') { showError('数量不能为空'); }记住:!value 是"falsy 检查",不是"空值检查"。两者在大多数场景下不等价。
JavaScript 判断空值:0 不算空的正确写法与 !value 的陷阱
JavaScript 里"空值"的定义因场景而异,!value 是最常见的误用——它会把 0、false、NaN、'' 全部当作空值处理,往往不符合预期。 陷阱:!value 的误判范围 const values = [0, false, NaN, '', null, undefined]; values.filter(v => !v); // [0, false, NaN, '', null, undefined] ← 全部被误判为空如果 0 是有效值(比如数量、评分、坐标),用 !value 会导致它被误过滤。 == null:同时覆盖 null 和 undefined == 宽松相等只在 null 和 undefined 之间成立,是检测这两者最简洁的方式: null == null // true undefined == null // true 0 == null // false '' == null // false false == null // false用法: function isNullish(value) { return value == null; // 等价于 value === null || value === undefined }加上空字符串判断 大多数场景下"空值"包括 null、undefined 和空字符串: function isEmpty(value) { return value == null || value === ''; }isEmpty(null) // true isEmpty(undefined) // true isEmpty('') // true isEmpty(0) // false ← 0 不算空 isEmpty(false) // false isEmpty([]) // false isEmpty(' ') // false(有空格的字符串不算空)加上空白字符串判断 如果全是空格的字符串也要视为空: function isEmpty(value) { return value == null || (typeof value === 'string' && value.trim() === ''); }isEmpty(' ') // true场景对照表场景 推荐写法只判断 null/undefined value == nullnull/undefined/空字符串 value == null || value === ''同时包含空白字符串 value == null || value?.trim() === ''非空对象(有属性) Object.keys(value).length > 0非空数组 Array.isArray(value) && value.length > 0空值合并运算符 ?? ES2020 引入的 ?? 只在左侧为 null 或 undefined 时取右侧默认值,不误判 0 和 false: const count = userInput ?? 0; // userInput 为 null/undefined 时取 0// 对比 ||: const count2 = userInput || 0; // userInput 为 0 时也会取右侧!需要默认值时优先用 ?? 而非 ||。
