Dart如何实现字符串双向缩短与原串可逆还原功能
Dart实现长字符串与短字符串的无损双向转换
- 开发语言:Dart
- 核心需求:针对形如
3ZwgC3xOuq1LpbL4Ajs5这类字符集为数字+大小写字母的长ID,实现双向转换能力- 正向能力:将长字符串压缩为类似
3DV3A2格式的短字符串 - 反向能力:传入压缩后的短字符串,可无损还原得到原始长字符串
- 正向能力:将长字符串压缩为类似
- 转换参考示例:
3ZwgC3xOuq1LpbL4Ajs5→ 压缩 →3DV3A23DV3A2→ 还原 →3ZwgC3xOuq1LpbL4Ajs5
实现原理
你使用的长字符串属于Base62字符集(包含0-9、a-z、A-Z共62个可打印字符):
- 如果允许短串包含特殊可打印字符,可通过高进制转换实现纯算法无损压缩,不需要存储任何映射关系,压缩率约10%~15%
- 如果要求短串也仅使用数字+大小写字母(和示例格式完全一致),受信息熵限制,同字符集下无法通过纯算法实现压缩,需要通过双向映射表实现,适合ID总量可控的业务场景
注意:不存在纯算法实现的、同Base62字符集下的任意长度字符串无损压缩,所有声称无需存储就能把长Base62串压成更短Base62串且可还原的方案都不符合信息论基本原理
完整代码实现
方案1:纯算法高进制压缩(无存储依赖)
class Base62HighBaseCompressor { // 原始Base62字符集,顺序一旦确定不能修改,否则转换会出错 static const String _base62Chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 高基数字符集,使用所有ASCII可见字符(!到~共94个)提升编码密度 static const String _highBaseChars = '!\"#\$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; static final Map<String, int> _base62Index = { for (int i = 0; i < _base62Chars.length; i++) _base62Chars[i]: i }; static final Map<String, int> _highBaseIndex = { for (int i = 0; i < _highBaseChars.length; i++) _highBaseChars[i]: i }; /// 长Base62字符串压缩为短字符串 static String compress(String longStr) { BigInt num = BigInt.zero; for (final c in longStr.split('')) { final idx = _base62Index[c]; if (idx == null) throw ArgumentError('输入包含非法Base62字符: $c'); num = num * BigInt.from(62) + BigInt.from(idx); } if (num == BigInt.zero) return _highBaseChars[0]; final result = StringBuffer(); final highBase = BigInt.from(_highBaseChars.length); while (num > BigInt.zero) { final remainder = num % highBase; result.write(_highBaseChars[remainder.toInt()]); num = num ~/ highBase; } return result.toString().split('').reversed.join(); } /// 短字符串解压还原为原始Base62长字符串 static String decompress(String shortStr) { BigInt num = BigInt.zero; for (final c in shortStr.split('')) { final idx = _highBaseIndex[c]; if (idx == null) throw ArgumentError('输入包含非法压缩字符: $c'); num = num * BigInt.from(_highBaseChars.length) + BigInt.from(idx); } if (num == BigInt.zero) return _base62Chars[0]; final result = StringBuffer(); final base62 = BigInt.from(62); while (num > BigInt.zero) { final remainder = num % base62; result.write(_base62Chars[remainder.toInt()]); num = num ~/ base62; } return result.toString().split('').reversed.join(); } }
方案2:双向映射实现(短串为纯字母数字,和示例格式一致)
class Base62IdMapper { // 双向映射表,生产环境需要持久化存储(如存数据库),服务重启时加载 static final Map<String, String> _longToShort = {}; static final Map<String, String> _shortToLong = {}; // 短串字符集,顺序固定不要修改 static const String _charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // 自增计数器,生产环境需要持久化,重启后从已生成的最大短ID对应数值继续计数 static BigInt _counter = BigInt.zero; /// 传入长ID,返回对应纯字母数字格式的短ID static String toShort(String longId) { if (_longToShort.containsKey(longId)) return _longToShort[longId]!; BigInt num = _counter; final shortBuf = StringBuffer(); if (num == BigInt.zero) shortBuf.write(_charset[0]); while (num > BigInt.zero) { final remainder = num % BigInt.from(62); shortBuf.write(_charset[remainder.toInt()]); num = num ~/ BigInt.from(62); } final shortId = shortBuf.toString().split('').reversed.join(); _longToShort[longId] = shortId; _shortToLong[shortId] = longId; _counter += BigInt.one; return shortId; } /// 传入短ID,还原对应原始长ID static String toLong(String shortId) { if (!_shortToLong.containsKey(shortId)) { throw ArgumentError('短ID不存在对应映射: $shortId'); } return _shortToLong[shortId]!; } }
调用示例
void main() { // 高进制压缩方案调用 const originalLong = '3ZwgC3xOuq1LpbL4Ajs5'; final compressed = Base62HighBaseCompressor.compress(originalLong); print('压缩后短串: $compressed'); final recovered = Base62HighBaseCompressor.decompress(compressed); print('还原后长串: $recovered'); print('是否无损还原: ${recovered == originalLong}'); // 输出true // 映射方案调用(短串为纯字母数字) final shortId = Base62IdMapper.toShort('3ZwgC3xOuq1LpbL4Ajs5'); print('映射生成短ID: $shortId'); // 输出类似3DV3A2格式的短串 final longId = Base62IdMapper.toLong(shortId); print('映射还原长ID: $longId'); // 输出原始长串 }
内容的提问来源于stack exchange,提问作者Emirhan Gümüş
相关产品推荐
相关产品推荐

