将任意 GUID 编码为可读 ASCII (33-127) 的最有效方法是啥?
Posted
技术标签:
【中文标题】将任意 GUID 编码为可读 ASCII (33-127) 的最有效方法是啥?【英文标题】:What is the most efficient way to encode an arbitrary GUID into readable ASCII (33-127)?将任意 GUID 编码为可读 ASCII (33-127) 的最有效方法是什么? 【发布时间】:2011-02-19 03:15:06 【问题描述】:GUID 的标准字符串表示形式大约需要 36 个字符。这是非常好的,但也非常浪费。我想知道,如何使用 33-127 范围内的所有 ASCII 字符以最短的方式对其进行编码。简单的实现产生 22 个字符,仅仅是因为 128 位 / 6 位 产生 22。
霍夫曼编码是我第二好的,唯一的问题是如何选择编码......
当然,编码必须是无损的。
【问题讨论】:
我很想知道这一点。你真的需要存储数十亿个 GUID 吗?因为任何少于数十亿的东西,甚至将字符串长度减半的意义几乎不值得算法上的麻烦。 霍夫曼编码肯定行不通 - 随机 GUID 中的所有符号的可能性均等。 @NickJohnson 不确定,因为 GUID 有奇怪的生成规则,其中一个包含生成日期,例如,给定 5 年的跨度,这 5 年内的霍夫曼编码可能会提供一个很好的减少。当然我说“可能”,因为我不知道日期是如何“散列”的。如果它的散列严重,霍夫曼可以创建压缩。 @v.oddou 您可以在 Wikipedia 上阅读有关 GUID 方案的信息;在类型 4(随机)UUID 中,除了少数位之外,所有位都是随机选择的。 【参考方案1】:这是一个老问题,但我必须解决它才能使我正在开发的系统向后兼容。
确切的要求是客户端生成的标识符将被写入数据库并存储在 20 个字符的唯一列中。它从未显示给用户,也没有以任何方式编入索引。
由于我无法消除该要求,我真的很想使用 Guid(即statistically unique),如果我可以将其无损编码为 20 个字符,那么考虑到限制,这将是一个很好的解决方案。
Ascii-85 允许您将 4 字节的二进制数据编码为 5 字节的 Ascii 数据。因此,使用这种编码方案,一个 16 字节的 guid 将恰好适合 20 个 Ascii 字符。一个 Guid 可以有 3.1962657931507848761677563491821e+38 个离散值,而 Ascii-85 的 20 个字符可以有 3.8759531084514355873123178482056e+38 个离散值。
在写入数据库时,我对截断有些担心,因此编码中不包含空白字符。我也遇到了collation 的问题,我通过从编码中排除小写字符来解决这个问题。此外,它只会通过paramaterized command 传递,因此任何特殊的 SQL 字符都会被自动转义。
我已经包含了执行 Ascii-85 编码和解码的 C# 代码,以防它对任何人有所帮助。显然,根据您的使用情况,您可能需要选择不同的字符集,因为我的限制让我选择了一些不寻常的字符,例如 'ß' 和 'Ø' - 但这是简单的部分:
/// <summary>
/// This code implements an encoding scheme that uses 85 printable ascii characters
/// to encode the same volume of information as contained in a Guid.
///
/// Ascii-85 can represent 4 binary bytes as 5 Ascii bytes. So a 16 byte Guid can be
/// represented in 20 Ascii bytes. A Guid can have
/// 3.1962657931507848761677563491821e+38 discrete values whereas 20 characters of
/// Ascii-85 can have 3.8759531084514355873123178482056e+38 discrete values.
///
/// Lower-case characters are not included in this encoding to avoid collation
/// issues.
/// This is a departure from standard Ascii-85 which does include lower case
/// characters.
/// In addition, no whitespace characters are included as these may be truncated in
/// the database depending on the storage mechanism - ie VARCHAR vs CHAR.
/// </summary>
internal static class Ascii85
/// <summary>
/// 85 printable ascii characters with no lower case ones, so database
/// collation can't bite us. No ' ' character either so database can't
/// truncate it!
/// Unfortunately, these limitation mean resorting to some strange
/// characters like 'Æ' but we won't ever have to type these, so it's ok.
/// </summary>
private static readonly char[] kEncodeMap = new[]
'0','1','2','3','4','5','6','7','8','9', // 10
'A','B','C','D','E','F','G','H','I','J', // 20
'K','L','M','N','O','P','Q','R','S','T', // 30
'U','V','W','X','Y','Z','|','','~','', // 40
'!','"','#','$','%','&','\'','(',')','`', // 50
'*','+',',','-','.','/','[','\\',']','^', // 60
':',';','<','=','>','?','@','_','¼','½', // 70
'¾','ß','Ç','Ð','€','«','»','¿','•','Ø', // 80
'£','†','‡','§','¥' // 85
;
/// <summary>
/// A reverse mapping of the <see cref="kEncodeMap"/> array for decoding
/// purposes.
/// </summary>
private static readonly IDictionary<char, byte> kDecodeMap;
/// <summary>
/// Initialises the <see cref="kDecodeMap"/>.
/// </summary>
static Ascii85()
kDecodeMap = new Dictionary<char, byte>();
for (byte i = 0; i < kEncodeMap.Length; i++)
kDecodeMap.Add(kEncodeMap[i], i);
/// <summary>
/// Decodes an Ascii-85 encoded Guid.
/// </summary>
/// <param name="ascii85Encoding">The Guid encoded using Ascii-85.</param>
/// <returns>A Guid decoded from the parameter.</returns>
public static Guid Decode(string ascii85Encoding)
// Ascii-85 can encode 4 bytes of binary data into 5 bytes of Ascii.
// Since a Guid is 16 bytes long, the Ascii-85 encoding should be 20
// characters long.
if(ascii85Encoding.Length != 20)
throw new ArgumentException(
"An encoded Guid should be 20 characters long.",
"ascii85Encoding");
// We only support upper case characters.
ascii85Encoding = ascii85Encoding.ToUpper();
// Split the string in half and decode each substring separately.
var higher = ascii85Encoding.Substring(0, 10).AsciiDecode();
var lower = ascii85Encoding.Substring(10, 10).AsciiDecode();
// Convert the decoded substrings into an array of 16-bytes.
var byteArray = new[]
(byte)((higher & 0xFF00000000000000) >> 56),
(byte)((higher & 0x00FF000000000000) >> 48),
(byte)((higher & 0x0000FF0000000000) >> 40),
(byte)((higher & 0x000000FF00000000) >> 32),
(byte)((higher & 0x00000000FF000000) >> 24),
(byte)((higher & 0x0000000000FF0000) >> 16),
(byte)((higher & 0x000000000000FF00) >> 8),
(byte)((higher & 0x00000000000000FF)),
(byte)((lower & 0xFF00000000000000) >> 56),
(byte)((lower & 0x00FF000000000000) >> 48),
(byte)((lower & 0x0000FF0000000000) >> 40),
(byte)((lower & 0x000000FF00000000) >> 32),
(byte)((lower & 0x00000000FF000000) >> 24),
(byte)((lower & 0x0000000000FF0000) >> 16),
(byte)((lower & 0x000000000000FF00) >> 8),
(byte)((lower & 0x00000000000000FF)),
;
return new Guid(byteArray);
/// <summary>
/// Encodes binary data into a plaintext Ascii-85 format string.
/// </summary>
/// <param name="guid">The Guid to encode.</param>
/// <returns>Ascii-85 encoded string</returns>
public static string Encode(Guid guid)
// Convert the 128-bit Guid into two 64-bit parts.
var byteArray = guid.ToByteArray();
var higher =
((UInt64)byteArray[0] << 56) | ((UInt64)byteArray[1] << 48) |
((UInt64)byteArray[2] << 40) | ((UInt64)byteArray[3] << 32) |
((UInt64)byteArray[4] << 24) | ((UInt64)byteArray[5] << 16) |
((UInt64)byteArray[6] << 8) | byteArray[7];
var lower =
((UInt64)byteArray[ 8] << 56) | ((UInt64)byteArray[ 9] << 48) |
((UInt64)byteArray[10] << 40) | ((UInt64)byteArray[11] << 32) |
((UInt64)byteArray[12] << 24) | ((UInt64)byteArray[13] << 16) |
((UInt64)byteArray[14] << 8) | byteArray[15];
var encodedStringBuilder = new StringBuilder();
// Encode each part into an ascii-85 encoded string.
encodedStringBuilder.AsciiEncode(higher);
encodedStringBuilder.AsciiEncode(lower);
return encodedStringBuilder.ToString();
/// <summary>
/// Encodes the given integer using Ascii-85.
/// </summary>
/// <param name="encodedStringBuilder">The <see cref="StringBuilder"/> to
/// append the results to.</param>
/// <param name="part">The integer to encode.</param>
private static void AsciiEncode(
this StringBuilder encodedStringBuilder, UInt64 part)
// Nb, the most significant digits in our encoded character will
// be the right-most characters.
var charCount = (UInt32)kEncodeMap.Length;
// Ascii-85 can encode 4 bytes of binary data into 5 bytes of Ascii.
// Since a UInt64 is 8 bytes long, the Ascii-85 encoding should be
// 10 characters long.
for (var i = 0; i < 10; i++)
// Get the remainder when dividing by the base.
var remainder = part % charCount;
// Divide by the base.
part /= charCount;
// Add the appropriate character for the current value (0-84).
encodedStringBuilder.Append(kEncodeMap[remainder]);
/// <summary>
/// Decodes the given string from Ascii-85 to an integer.
/// </summary>
/// <param name="ascii85EncodedString">Decodes a 10 character Ascii-85
/// encoded string.</param>
/// <returns>The integer representation of the parameter.</returns>
private static UInt64 AsciiDecode(this string ascii85EncodedString)
if (ascii85EncodedString.Length != 10)
throw new ArgumentException(
"An Ascii-85 encoded Uint64 should be 10 characters long.",
"ascii85EncodedString");
// Nb, the most significant digits in our encoded character
// will be the right-most characters.
var charCount = (UInt32)kEncodeMap.Length;
UInt64 result = 0;
// Starting with the right-most (most-significant) character,
// iterate through the encoded string and decode.
for (var i = ascii85EncodedString.Length - 1; i >= 0; i--)
// Multiply the current decoded value by the base.
result *= charCount;
// Add the integer value for that encoded character.
result += kDecodeMap[ascii85EncodedString[i]];
return result;
另外,这里是单元测试。它们没有我想要的那么彻底,而且我不喜欢 Guid.NewGuid()
的使用位置的不确定性,但它们应该可以帮助您入门:
/// <summary>
/// Tests to verify that the Ascii-85 encoding is functioning as expected.
/// </summary>
[TestClass]
[UsedImplicitly]
public class Ascii85Tests
[TestMethod]
[Description("Ensure that the Ascii-85 encoding is correct.")]
[UsedImplicitly]
public void CanEncodeAndDecodeAGuidUsingAscii85()
var guidStrings = new[]
"00000000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-0000000000FF",
"00000000-0000-0000-0000-00000000FF00",
"00000000-0000-0000-0000-000000FF0000",
"00000000-0000-0000-0000-0000FF000000",
"00000000-0000-0000-0000-00FF00000000",
"00000000-0000-0000-0000-FF0000000000",
"00000000-0000-0000-00FF-000000000000",
"00000000-0000-0000-FF00-000000000000",
"00000000-0000-00FF-0000-000000000000",
"00000000-0000-FF00-0000-000000000000",
"00000000-00FF-0000-0000-000000000000",
"00000000-FF00-0000-0000-000000000000",
"000000FF-0000-0000-0000-000000000000",
"0000FF00-0000-0000-0000-000000000000",
"00FF0000-0000-0000-0000-000000000000",
"FF000000-0000-0000-0000-000000000000",
"FF000000-0000-0000-0000-00000000FFFF",
"00000000-0000-0000-0000-0000FFFF0000",
"00000000-0000-0000-0000-FFFF00000000",
"00000000-0000-0000-FFFF-000000000000",
"00000000-0000-FFFF-0000-000000000000",
"00000000-FFFF-0000-0000-000000000000",
"0000FFFF-0000-0000-0000-000000000000",
"FFFF0000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-0000FFFFFFFF",
"00000000-0000-0000-FFFF-FFFF00000000",
"00000000-FFFF-FFFF-0000-000000000000",
"FFFFFFFF-0000-0000-0000-000000000000",
"00000000-0000-0000-FFFF-FFFFFFFFFFFF",
"FFFFFFFF-FFFF-FFFF-0000-000000000000",
"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF",
"1000000F-100F-100F-100F-10000000000F"
;
foreach (var guidString in guidStrings)
var guid = new Guid(guidString);
var encoded = Ascii85.Encode(guid);
Assert.AreEqual(
20,
encoded.Length,
"A guid encoding should not exceed 20 characters.");
var decoded = Ascii85.Decode(encoded);
Assert.AreEqual(
guid,
decoded,
"The guids are different after being encoded and decoded.");
[TestMethod]
[Description(
"The Ascii-85 encoding is not susceptible to changes in character case.")]
[UsedImplicitly]
public void Ascii85IsCaseInsensitive()
const int kCount = 50;
for (var i = 0; i < kCount; i++)
var guid = Guid.NewGuid();
// The encoding should be all upper case. A reliance
// on mixed case will make the generated string
// vulnerable to sql collation.
var encoded = Ascii85.Encode(guid);
Assert.AreEqual(
encoded,
encoded.ToUpper(),
"The Ascii-85 encoding should produce only uppercase characters.");
我希望这可以为某人省去一些麻烦。
另外,如果您发现任何错误,请告诉我 ;-)
【讨论】:
请注意,该代码返回的字符如¾
大于127,而不是严格的ASCII。根据您的编码(例如 UTF-8),您可能会得到转义序列。在 UTF-8 编码的字符串中,128-255 范围内的每个字符代码使用 2 个字节。
老实说,为什么选择 Ascii85?仅仅因为它看起来“标准化”?还不如使用base95。既不是 URL 安全的,也不是 html 安全的。【参考方案2】:
使用基数 85。 请参阅第 4.1 节。 为什么是 85 个? A Compact Representation of IPv6 Addresses
IPv6 地址(如 GUID)由 8 个 16 位片段组成。
【讨论】:
看起来不错,但是字符 '%' 和 '?'如果要在数据库查询中使用编码,则不适合。我们将它们替换为 ':' 和 '.',因为我们不关心 IPv6 格式问题。 @Mark 我可以看出这可能是个问题,但在许多情况下,如果您将其存储在列中并执行TableName.Guid85Encoded like '%blah%'
则不会有问题,因为 % 和 ?位于 like 运算符的左侧。
@CMCDragonkai 正如链接的 RFC 所述:“介于 85 和 94 之间的任何值”,但“选择 85 允许使用尽可能小的 ASCII 字符子集”。 91 增加了复杂性而不提高效率。
看看这个:iiis.org/CDs2010/CD2010SCI/CCCT_2010/…Base91的编码效率比Base85/64高,编码率比Base85高。此外,Base91 提供了与任何位长输入序列的兼容性,而无需额外的填充声明,除了他的码字自身。可以使用 Base91 作为 Base85 和 Base64 的替代品,以便在受限情况下获得一些好处。
如果这不明显:链接的 RFC 是 4 月 1 日的笑话。没有人认真提议在 Base85 中表示 IPv6 地址。实际上,在 99% 的实际情况下,具有非标准表示的缺点应该超过保存字符少(与 Base64 相比)的优势。如果有人对不惜一切代价节省空间非常感兴趣,那么只需以二进制(16 字节)存储并在读/写时转换为文本。【参考方案3】:
您有 95 个可用字符 - 因此,超过 6 位,但不及 7 位(实际上约为 6.57)。您可以使用 128/log2(95) = 大约 19.48 个字符来编码为 20 个字符。如果以编码形式保存 2 个字符对您来说值得失去可读性,例如(伪代码):
char encoded[21];
long long guid; // 128 bits number
for(int i=0; i<20; ++i)
encoded[i] = chr(guid % 95 + 33);
guid /= 95;
encoded[20] = chr(0);
这基本上是通用的“在某个基本代码中编码一个数字”,除了不需要反转“数字”,因为顺序是任意的(并且小端更直接和自然)。为了从编码字符串中取回 guid,以非常相似的方式,以 95 为底的多项式计算(当然是在从每个数字中减去 33 之后):
guid = 0;
for(int i=0; i<20; ++i)
guid *= 95;
guid += ord(encoded[i]) - 33;
基本上使用霍纳的多项式求值方法。
【讨论】:
你的解码 sn-p 有一个错误,它实际上确实需要反向迭代字符串。 for(int i = 20; i > 0; --i) guid *= 95; guid += ord(encoded[i-1]) - 33; 【参考方案4】:只需转到Base64。
【讨论】:
我更喜欢这个,因为它比 base85 更容易找到 base64 编码的实现,而且 base85 只为您节省了 2 个字符。如果您将这些 guid 存储在 base85 中的系统成为遗留系统,并且有人必须将这些东西移植/导出到另一个使用另一种编程语言的系统并解码 GUID,那么 base85 编码可能是一个障碍,因为实现不那么广泛可用. 实际上有 66 个未保留的 URL 字符(请参阅 tools.ietf.org/html/rfc3986#section-2.3),因此您可以编写一个自定义的 base-66 编码器,它比简单的 Base64 略好。 Base-66 编码器在这里可用:***.com/a/18001426/76900【参考方案5】:使用从 33(顺便说一句,空格有什么问题?)到 127 的完整范围可以为您提供 95 个可能的字符。以 95 为基数表示 2^128
可能的 guid 值将使用 20 个字符。这(模数的东西,比如将保持不变的 nybbles)是你能做的最好的事情。省去麻烦 - 使用 base 64。
【讨论】:
base95 中的 128 位更准确地说是 19.5 位。如果您不考虑长度,如果省略最重要的零,您将得到平均长度。 空格的排除可能是因为它没有可见字符,并且被认为是空格,在某些情况下(通常是前导或尾随空格),人和计算机都会将其剥离。原谅近 12 年的颠簸,但到目前为止没有人回答你,值得一提。此外,base95 不是 URL 或 HTML 安全的,因此某些字符需要转义,这可以在该限制下扩展字符串长度。【参考方案6】:假设您的所有 GUID 都是由相同的算法生成的,在应用任何其他编码之前,您可以通过不对算法半字节进行编码来节省 4 位:-|
【讨论】:
【参考方案7】:任意 GUID? “朴素”算法将产生最佳结果。进一步压缩 GUID 的唯一方法是利用“任意”约束排除的数据中的模式。
【讨论】:
你读过这个问题吗?他问的是如何更有效地对其进行编码,而不是如何压缩它。 您是否了解编码 128 位比 22 个 6 位字符更有效需要某种形式的压缩和输入中的某种模式?【参考方案8】:我同意 Base64 方法。它会将 32 个字母的 UUID 缩减为 22 个字母的 Base64。
这里是简单的 Hex php 的 Base64 转换函数:
function hex_to_base64($hex)
$return = '';
foreach(str_split($hex, 2) as $pair)
$return .= chr(hexdec($pair));
return preg_replace("/=+$/", "", base64_encode($return)); // remove the trailing = sign, not needed for decoding in PHP.
function base64_to_hex($base64)
$return = '';
foreach (str_split(base64_decode($base64), 1) as $char)
$return .= str_pad(dechex(ord($char)), 2, "0", STR_PAD_LEFT);
return $return;
【讨论】:
以上是关于将任意 GUID 编码为可读 ASCII (33-127) 的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章