【问题标题】:PHP - What is a good way to produce a short alphanumeric string from a long md5 hash?PHP - 从长 md5 哈希生成短字母数字字符串的好方法是什么?
【发布时间】:2011-03-19 20:26:24
【问题描述】:

这是为了有一个漂亮的短 URL,它引用数据库中的 md5 哈希。我想转换这样的东西:

a7d2cd9e0e09bebb6a520af48205ced1

变成这样:

hW9lM5f27

两者包含的信息量大致相同。该方法不必是直接和可逆的,但这会很好(更灵活)。至少我想要一个随机生成的字符串,以十六进制哈希作为种子,因此它是可重现的。我相信有很多可能的答案,我很想知道人们会如何以优雅的方式做到这一点。

哦,这不必与原始哈希具有完美的 1:1 对应关系,但这将是一个好处(我想我已经暗示了可逆性标准)。如果可能的话,我想避免碰撞。

编辑 我意识到我最初的计算是完全错误的(感谢在这里回答的人,但我花了一段时间才弄清楚)并且你不能通过将所有小写和大写字母放入混合中来真正减少字符串长度。所以我想我会想要一些不直接从十六进制转换为基数 62 的东西。

【问题讨论】:

  • 使用 base-64 编码,您只能将输入减少到 (4/8)/(6/8) -> 4/6 ~ 66%(这是假设您在不添加任何新内容的情况下处理“丑陋”的 base64 字符)。我可能会考虑使用(辅助)查找方法来获得真正“漂亮”的值。
  • Re “所以我想我会想要一些不直接从十六进制转换为基数 62 的东西。” -- 如果你想在一个 URL 安全的字符串中编码 16 个字节,我下面的答案(22 个字符)可能是你能得到的最好的答案。你到底想达到什么目的?

标签: php random base


【解决方案1】:

以下是 Base-16 到 Base-64 转换和 Base-64 到 Base-16 任意输入长度的反向转换的两个转换函数:

function base16_to_base64($base16) {
    return base64_encode(pack('H*', $base16));
}
function base64_to_base16($base64) {
    return implode('', unpack('H*', base64_decode($base64)));
}

如果你需要Base-64 encoding with the URL and filename safe alphabet ,可以使用这些函数:

function base64_to_base64safe($base64) {
    return strtr($base64, '+/', '-_');
}
function base64safe_to_base64($base64safe) {
    return strtr($base64safe, '-_', '+/');
}

如果您现在想要一个函数来使用 URL 安全字符压缩您的十六进制 MD5 值,您可以使用这个:

function compress_hash($hash) {
    return base64_to_base64safe(rtrim(base16_to_base64($hash), '='));
}

还有反函数:

function uncompress_hash($hash) {
    return base64_to_base16(base64safe_to_base64($hash));
}

【讨论】:

  • 非常好。这看起来是进行纯可逆转换的最佳方法。我正在查看 PHP 手册中的打包/解包,但我无法理解。我决定根据我的需要使用“有损”压缩方法。 stackoverflow 是否允许两个接受的答案?
【解决方案2】:

当然,如果我想要一个函数来完美地满足我的需求,我最好自己制作。这是我想出的。

//takes a string input, int length and optionally a string charset
//returns a hash 'length' digits long made up of characters a-z,A-Z,0-9 or those specified by charset
function custom_hash($input, $length, $charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUFWXIZ0123456789'){
    $output = '';
    $input = md5($input); //this gives us a nice random hex string regardless of input 

    do{
        foreach (str_split($input,8) as $chunk){
            srand(hexdec($chunk));
            $output .= substr($charset, rand(0,strlen($charset)), 1);
        }
        $input = md5($input);

    } while(strlen($output) < $length);

    return substr($output,0,$length);
}

这是一个非常通用的随机字符串生成器,但它不仅仅是任何旧的随机字符串生成器,因为结果是由输入字符串决定的,对该输入的任何轻微更改都会产生完全不同的结果。你可以用这个做各种各样的事情:

custom_hash('1d34ecc818c4d50e788f0e7a9fd33662', 16); // 9FezqfFBIjbEWOdR
custom_hash('Bilbo Baggins', 5, '0123456789bcdfghjklmnpqrstvwxyz'); // lv4hb
custom_hash('', 100, '01'); 
// 1101011010110001100011111110100100101011001011010000101010010011000110000001010100111000100010101101

有人发现它有任何问题或有改进的余地吗?

【讨论】:

  • 我不明白你为什么一直计算输入的 hd5... $input = md5($input);在 DO 循环的每次迭代中
  • 因为否则如果您的输出大于 32 位,随机数字会重复。我最初使用的是 str_shuffle,但即使这样也会导致更大规模的重复。
  • 我不清楚,但经过测试,似乎相同的输入总是会返回相同的结果。 (这正是我想要的)
【解决方案3】:

我建议反对一对一的通信:

使用 base-64 编码,您只能将输入减少到 (4/8)/(6/8) -> 4/6 ~ 66%(这是假设您处理“丑陋" base64 字符,不添加任何新内容)。

我可能会考虑使用(辅助)查找方法来获得真正“漂亮”的值。一旦你建立了这个替代方法,选择如何在那个范围内生成值——例如随机数——可以没有源哈希值(因为无论如何都会丢失对应关系)并且可以使用任意“漂亮”目标集,也许是 [a-z][A-Z][0-9]。

您可以通过简单地按照除法进位方法和查找数组来转换为基数(上面的 62)。这应该是一个有趣的小练习。

注意:如果您从 [0, 62^5) 中选择随机数,那么您将获得一个完全打包编码输出的值(并且适合 32 位整数值)。然后,您可以连续多次执行此过程以获得良好的 5 倍数结果值,例如 xxxxxyyyyyzzzzzz(其中 x,y,z 是不同的组,总值在 (62^5)^3 范围内-> 62^15 -> “巨大的价值”)

编辑,供评论

因为 没有 1-1 对应,您可以制作真正短而漂亮的东西——也许“小”到 8 个字符长——使用 base62,8 个字符可以存储多达 218340105584896 个值,这可能比您需要的更多。甚至是“仅”允许存储 56800235584 个不同值的 6 个字符! (而且你仍然不能将该数字存储在一个普通的 32 位整数中 :-) 如果你减少到 5 个字符,你会再次减少空间(到 10 亿以下:916,132,832),但现在你有一些东西可以适合带符号的 32 位整数(尽管有点浪费)。

数据库应确保没有重复,尽管此值的索引将使用随机源“快速分段”(但您可以使用计数器或诸如此类的东西)。一个分布良好的 PRNG 应该在足够大的范围内具有最小的冲突(阅读:重试)(假设您保持种子滚动并且不重置它,或者适当地重置它)——Super 7 甚至可以保证在一个周期内没有重复(只有 ~32k),但正如您在上面看到的,目标空间仍然是 big。请参阅顶部的数学运算,以最小编码大小来维持 1-1 关系。

除法进位法只是解释了如何将您的源编号转换为不同的基数——也许是base62。可以应用相同的通用方法从“自然”基础(PHP 中的 base10)转到任何基础。

【讨论】:

  • 你为什么不建议1-1通信?我不知道你在说什么分而治之的方法,但听起来很有趣。
【解决方案4】:

这里有一个小函数供考虑:

/** Return 22-char compressed version of 32-char hex string (eg from PHP md5). */
function compress_md5($md5_hash_str) {
    // (we start with 32-char $md5_hash_str eg "a7d2cd9e0e09bebb6a520af48205ced1")
    $md5_bin_str = "";
    foreach (str_split($md5_hash_str, 2) as $byte_str) { // ("a7", "d2", ...)
        $md5_bin_str .= chr(hexdec($byte_str));
    }
    // ($md5_bin_str is now a 16-byte string equivalent to $md5_hash_str)
    $md5_b64_str = base64_encode($md5_bin_str);
    // (now it's a 24-char string version of $md5_hash_str eg "VUDNng4JvrtqUgr0QwXOIg==")
    $md5_b64_str = substr($md5_b64_str, 0, 22);
    // (but we know the last two chars will be ==, so drop them eg "VUDNng4JvrtqUgr0QwXOIg")
    $url_safe_str = str_replace(array("+", "/"), array("-", "_"), $md5_b64_str);
    // (Base64 includes two non-URL safe chars, so we replace them with safe ones)
    return $url_safe_str;
}

基本上,MD5 哈希字符串中有 16 个字节的数据。它有 32 个字符长,因为每个字节都被编码为 2 个十六进制数字(即 00-FF)。所以我们将它们分解成字节并构建一个 16 字节的字符串。但是因为这不再是人类可读或有效的 ASCII,我们将其 base-64 编码回可读字符。但是由于 base-64 会导致 ~4/3 扩展(我们每 8 位输入仅输出 6 位,因此需要 32 位来编码 24 位),16 字节变为 22 字节。但是因为 base-64 编码通常填充长度为 4 的倍数,所以我们只能取 24 个字符输出的前 22 个字符(其中最后 2 个是填充)。然后我们将 base-64 编码使用的非 URL 安全字符替换为 URL 安全的等效字符。

这是完全可逆的,但留给读者作为练习。

我认为这是你能做的最好的,除非你不关心人类可读/ASCII,在这种情况下你可以直接使用 $md5_bin_str。

如果您不需要保留所有位,您也可以使用此函数结果的前缀或其他子集。扔掉数据显然是缩短事情的最简单方法! (但它是不可逆的)

附:对于“a7d2cd9e0e09bebb6a520af48205ced1”(32 个字符)的输入,此函数将返回“VUDNng4JvrtqUgr0QwXO0Q”(22 个字符)。

【讨论】:

  • 根据我的计算,a-zA-Z0-9 的 9 个字符应该足以存储 md5 哈希,所以 22 个字符不如我希望的那么好。我不太了解base64,为什么它会增加大小?难道没有更合适的东西可以实际缩小字符串的大小吗?
  • 好吧,我的计算肯定是错误的,你确实需要 22 个字符来表示哈希,但我不知道我的数学哪里错了。如果 md5 哈希中的每个字符代表 16 位,并且有 32 个字符应该是 16*32 = 512 位(但维基百科说 md5 是 128 位)。所以 62*9 = 558 位。看起来 9 位数字应该能够包含 md5 的假定 512 位。 -- 呸,好吧,我刚刚意识到十六进制中的一个字符实际上是 4 位,而不是 16 位。为什么这让我如此困惑...
  • 每个十六进制数字 char = 4 位。 32 个十六进制字符 = 128 位 = 16 字节。 Base-64 仅使用每个输出字节的 6 位(以保持 ASCII 安全输出),因此编码 3 个字节(8+8+8)需要 4 个字节(6+6+6+6)。这就是 16 个原始字节需要 22 个编码字节的原因。 Base-64 牺牲了空间效率以实现更广泛的介质兼容性。
【解决方案5】:

你可以做简单的旧base conversion。哈希以十六进制表示,然后您可以创建一个您想要表示哈希的大小的字母表。 Base64 非常适合此目的,尽管您可能希望编写自己的函数,以便最终编码值,而不是字符串。

但是请注意,标准 Base64 包含您不想放入 URL 的字符; +、/ 和填充字符 =。在来回转换以获得 URL 安全的 Base64 编码时,您可以用其他字符替换这些字符(或者如果您编写自己的函数,则使用一组安全的字符开始)。

【讨论】:

    【解决方案6】:

    这取决于a7d2cd9e0e09bebb6a520af48205ced1 是什么。假设您正在谈论一个十六进制数字,因为它来自 md5,您可以运行 base64_encode。如果你有字符串形式的十六进制,你会想运行hexdec。不过要小心,不要遇到 maxint 问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-10
      • 1970-01-01
      • 1970-01-01
      • 2011-04-01
      • 1970-01-01
      • 2012-03-07
      相关资源
      最近更新 更多