将任意 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) 的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

字符编码--小记

base64模块的使用

如何将字符串转换为可读流?

将时间戳转换为可读的日期/时间 PHP

Android将gmt时间转换为可读日期

PHP 将日期/时间转换为可读格式。