18

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

a7d2cd9e0e09bebb6a520af48205ced1

变成这样的东西:

hW9lM5f27

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

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

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

4

6 回答 6

10

这里有一个小功能供考虑:

/** 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 。

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

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

于 2010-07-22T23:27:32.000 回答
5

以下是 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)));
}

如果您需要使用 URL 和文件名安全字母表的 Base-64 编码,您可以使用以下函数:

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));
}
于 2010-07-23T08:44:51.993 回答
3

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

//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

有人看到它有任何问题或有任何改进的余地吗?

于 2010-07-23T08:32:02.663 回答
2

我建议不要使用 1-1 通信:

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

我可能会考虑使用(辅助)查找方法来获得真正“漂亮”的值。一旦建立了这种替代方法,选择如何生成该范围内的值(例如随机数)就可以摆脱源哈希值(因为无论如何都会丢失对应关系),并且可以使用任意“漂亮”目标集,也许是 [az][AZ][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),但正如您在上面看到的,目标空间仍然很大请参阅在最小编码大小方面保持 1-1 关系所需的顶部的数学。

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

于 2010-07-22T23:29:00.910 回答
2

你可以只做普通的旧基础转换。哈希以十六进制表示,然后您可以创建一个您想要表示哈希的大小的字母表。Base64可以很好地用于此目的,尽管您可能希望编写自己的函数,以便最终对值进行编码,而不是对字符串进行编码。

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

于 2010-07-22T22:52:27.527 回答
1

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

于 2010-07-22T22:50:27.093 回答