Python str.translate 自定义 Base64:字符替换编解码原理
在逆向接口参数时,经常遇到这类代码: b64 = base64.b64encode(data.encode("utf-8")).decode("ascii") return b64.translate(TRANS)b64.translate(TRANS) 是 Python 字符串的内置 translate() 方法,不是某个库的 API。 translate() 做了什么 先 Base64 编码,再按照 TRANS 映射表替换字符。 translate() 一次可以替换多个字符,比链式 replace() 更高效: TRANS = str.maketrans({ "+": "-", "/": "_", })"a+b/c=".translate(TRANS) # → "a-b_c="TRANS 的常见形式 替换少量字符 TRANS = str.maketrans({ "+": "-", "/": "_", })这是标准的 URL Safe Base64 变体(RFC 4648),把 +/ 换成 -_,避免 URL 编码问题。 整体字符映射(自定义 Base64) src = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" dst = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm0123456789-_"TRANS = str.maketrans(src, dst)这是在对整个 Base64 字符集做轮换映射,常用于接口参数混淆。 解码:把映射反过来 编码是 src → dst,解码就是 dst → src: REVERSE_TRANS = str.maketrans(dst, src)完整编解码实现 import base64src = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" dst = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm0123456789-_"TRANS = str.maketrans(src, dst) REVERSE_TRANS = str.maketrans(dst, src)def encode(data: str) -> str: b64 = base64.b64encode(data.encode()).decode() return b64.translate(TRANS)def decode(data: str) -> str: raw = data.translate(REVERSE_TRANS) return base64.b64decode(raw).decode()x = encode("hello world") print(x) # 输出混淆后的字符串 print(decode(x)) # → hello world简单的 URL Safe Base64 变体 如果 TRANS 只替换 +/=,编解码更简单: import base64def url_safe_encode(data: str) -> str: b64 = base64.b64encode(data.encode()).decode() return b64.translate(str.maketrans({"+": "-", "/": "_", "=": ""}))def url_safe_decode(data: str) -> str: # 补齐 padding padded = data + "=" * (-len(data) % 4) raw = padded.translate(str.maketrans({"-": "+", "_": "/"})) return base64.b64decode(raw).decode()逆向时如何确认 TRANS 内容 遇到 b64.translate(TRANS) 时,找到 TRANS 的定义:如果是 str.maketrans(src, dst):把 dst 作为新的 src,src 作为新的 dst,即可反向还原 如果是 str.maketrans({...}):把字典的键值互换即可很多登录接口、签名参数都会用这种方式,目的是避免 +/= 在 URL 中被转义,或者简单混淆防止直接调用。
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 检查",不是"空值检查"。两者在大多数场景下不等价。
