如何将字节数组(MD5哈希)转换为字符串(36个字符)?

5
我有一个使用哈希函数创建的字节数组。我想将此数组转换为字符串。到目前为止,这将给我十六进制字符串。

现在我想使用不同于十六进制字符的东西,我想使用这些36个字符对字节数组进行编码:[a-z][0-9]。

我该怎么做呢?

编辑:我之所以要这样做,是因为我想拥有比十六进制字符串更小的字符串。


你的意思是将每个十六进制数转换为 ASCII 字符吗? - thumbmunkeys
已删除示例,使问题看起来不同。 - Kees C. Bakker
@x0r:我想要得到[a-z][0-9]范围内的字符,因此我想要对字节进行编码。 - Kees C. Bakker
1
你想要的是称为“基数36”的概念... 这个概念相当复杂 :-) 但是它确实可以实现。 - xanatos
如果该字节数组确实是MD5哈希的结果,则可以将其表示为GUID gResult = new Guid(oBytes);,GUID 可以用尽可能少的32个十六进制字符表示sString = gResult.ToString("N"); - stevehipwell
显示剩余2条评论
7个回答

6

我将我的任意长度进制转换函数从这个答案中移植到了C#:

static string BaseConvert(string number, int fromBase, int toBase)
{
    var digits = "0123456789abcdefghijklmnopqrstuvwxyz";
    var length = number.Length;
    var result = string.Empty;

    var nibbles = number.Select(c => digits.IndexOf(c)).ToList();
    int newlen;
    do {
        var value = 0;
        newlen = 0;

        for (var i = 0; i < length; ++i) {
            value = value * fromBase + nibbles[i];
            if (value >= toBase) {
                if (newlen == nibbles.Count) {
                    nibbles.Add(0);
                }
                nibbles[newlen++] = value / toBase;
                value %= toBase;
            }
            else if (newlen > 0) {
                if (newlen == nibbles.Count) {
                    nibbles.Add(0);
                }
                nibbles[newlen++] = 0;
            }
        }
        length = newlen;
        result = digits[value] + result; //
    }
    while (newlen != 0);

    return result;
}

由于此代码是来自PHP,可能不太符合C#语言的惯用法,也没有参数有效性检查。但是,您可以将其提供给一个十六进制编码的字符串,它将能够正常工作。

var result = BaseConvert(hexEncoded, 16, 36);

虽然不完全是您所要求的,但将byte[]编码为十六进制很简单。

看它如何实现


如果我将数字字符串更改为var digits =“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”;,为什么会得到相同的答案? - Kees C. Bakker
@Kees:digits仅是可用于使用的数字。要实际使用大写字母,您需要将适当更高的基数作为第二个和/或第三个参数传递。 - Jon
我明白了...只需将数字设为静态并将其长度传递给函数即可。完美的解决方案,非常易读。 - Kees C. Bakker
@Jon 为什么要加入 if (newlen == nibbles.Count) { nibbles.Add(0); } 这个判断语句?我看不出来为什么需要这样做。 - Paya
@Paya:如果 newlen == nibbles.Count 并且您没有添加任何内容,那么紧接着访问 nibbles[newlen++] 就会抛出异常。 - Jon
@Jon,是不是有任何输入可以使得该条件变为“true”?当我分析算法时,得出的结论是该条件永远不会评估为true - Paya

3
今晚我看到了一个代码审查问题,围绕着这里讨论的同一算法。请参见:https://codereview.stackexchange.com/questions/14084/base-36-encoding-of-a-byte-array/
我提供了一个改进后的实现,是早期答案的改进版(两者都使用BigInteger)。请参见:https://codereview.stackexchange.com/a/20014/20654。这个解决方案接收一个byte[]并返回一个Base36字符串。原始版本和我的版本都包含简单的基准信息。
为了完整起见,以下是从字符串中解码一个byte[]的方法。下面是来自上面链接中的编码函数的文本。请参考代码块后面的文本,获取一些有关解码的简单基准信息。
const int kByteBitCount= 8; // number of bits in a byte
// constants that we use in FromBase36String and ToBase36String
const string kBase36Digits= "0123456789abcdefghijklmnopqrstuvwxyz";
static readonly double kBase36CharsLengthDivisor= Math.Log(kBase36Digits.Length, 2);
static readonly BigInteger kBigInt36= new BigInteger(36);

// assumes the input 'chars' is in big-endian ordering, MSB->LSB
static byte[] FromBase36String(string chars)
{
    var bi= new BigInteger();
    for (int x= 0; x < chars.Length; x++)
    {
        int i= kBase36Digits.IndexOf(chars[x]);
        if (i < 0) return null; // invalid character
        bi *= kBigInt36;
        bi += i;
    }

    return bi.ToByteArray();
}

// characters returned are in big-endian ordering, MSB->LSB
static string ToBase36String(byte[] bytes)
{
    // Estimate the result's length so we don't waste time realloc'ing
    int result_length= (int)
        Math.Ceiling(bytes.Length * kByteBitCount / kBase36CharsLengthDivisor);
    // We use a List so we don't have to CopyTo a StringBuilder's characters
    // to a char[], only to then Array.Reverse it later
    var result= new System.Collections.Generic.List<char>(result_length);

    var dividend= new BigInteger(bytes);
    // IsZero's computation is less complex than evaluating "dividend > 0"
    // which invokes BigInteger.CompareTo(BigInteger)
    while (!dividend.IsZero)
    {
        BigInteger remainder;
        dividend= BigInteger.DivRem(dividend, kBigInt36, out remainder);
        int digit_index= Math.Abs((int)remainder);
        result.Add(kBase36Digits[digit_index]);
    }

    // orientate the characters in big-endian ordering
    result.Reverse();
    // ToArray will also trim the excess chars used in length prediction
    return new string(result.ToArray());
}

"

将“A test 1234. Made slightly larger!”编码为Base64结果为“165kkoorqxin775ct82ist5ysteekll7kaqlcnnu6mfe7ag7e63b5”。

在我的设备上,解码该Base64字符串1,000,000次需要12.6558909秒(我使用了与我的codereview答案提供的相同构建和机器条件)。

您提到您处理的是MD5哈希的byte[],而不是其十六进制字符串表示形式,因此我认为这个解决方案为您提供了最小的开销。

"

通过使用字典进行O(1)查找,而不是.IndexOf(chars[x]),或者更好的是:使用一个映射到base36Digits数组索引的每个base36字符代码的数组,可以提高FromBase36String的性能。 - Dai
1
我将你的代码复制到一个测试项目中,它将 { 203, 77, 29, 30, 215, 37, 184, 136 } 编码为 "1taukyt738g1h",但是解码后变成了 { 53, 178, 226, 225, 40, 218, 71, 119 } - Dai
  1. 没有实际性能统计数据来支持这个主张,我不敢轻易接受使用字典会带来改进的说法。相比之下,它需要额外的分配操作,而我现在用的方法不需要额外的堆内存分配,并且通常情况下应该具有更好的缓存局部性。数组映射只需要更多的引导。
2)谢谢,我使用http://rextester.com/发现你遇到的同样的问题。我需要在更好的开发机器上进行调试并测试我的基于N编码器是否存在同样的问题https://dev59.com/CmzXa4cB1Zd3GeqPWbAJ
- kornman00

2

使用 BigInteger(需要 System.Numerics 引用)

const string chars = "0123456789abcdefghijklmnopqrstuvwxyz";

// The result is padded with chars[0] to make the string length
// (int)Math.Ceiling(bytes.Length * 8 / Math.Log(chars.Length, 2))
// (so that for any value [0...0]-[255...255] of bytes the resulting
// string will have same length)
public static string ToBaseN(byte[] bytes, string chars, bool littleEndian = true, int len = -1)
{
    if (bytes.Length == 0 || len == 0)
    {
        return String.Empty;
    }

    // BigInteger saves in the last byte the sign. > 7F negative, 
    // <= 7F positive. 
    // If we have a "negative" number, we will prepend a 0 byte.
    byte[] bytes2;

    if (littleEndian)
    {
        if (bytes[bytes.Length - 1] <= 0x7F)
        {
            bytes2 = bytes;
        }
        else
        {
            // Note that Array.Resize doesn't modify the original array,
            // but creates a copy and sets the passed reference to the
            // new array
            bytes2 = bytes;
            Array.Resize(ref bytes2, bytes.Length + 1);
        }
    }
    else
    {
        bytes2 = new byte[bytes[0] > 0x7F ? bytes.Length + 1 : bytes.Length];

        // We copy and reverse the array
        for (int i = bytes.Length - 1, j = 0; i >= 0; i--, j++)
        {
            bytes2[j] = bytes[i];
        }
    }

    BigInteger bi = new BigInteger(bytes2);

    // A little optimization. We will do many divisions based on 
    // chars.Length .
    BigInteger length = chars.Length;

    // We pre-calc the length of the string. We know the bits of 
    // "information" of a byte are 8. Using Log2 we calc the bits of 
    // information of our new base. 
    if (len == -1)
    {
        len = (int)Math.Ceiling(bytes.Length * 8 / Math.Log(chars.Length, 2));
    }

    // We will build our string on a char[]
    var chs = new char[len];
    int chsIndex = 0;

    while (bi > 0)
    {
        BigInteger remainder;
        bi = BigInteger.DivRem(bi, length, out remainder);

        chs[littleEndian ? chsIndex : len - chsIndex - 1] = chars[(int)remainder];
        chsIndex++;

        if (chsIndex < 0)
        {
            if (bi > 0)
            {
                throw new OverflowException();
            }
        }
    }

    // We append the zeros that we skipped at the beginning
    if (littleEndian)
    {
        while (chsIndex < len)
        {
            chs[chsIndex] = chars[0];
            chsIndex++;
        }
    }
    else
    {
        while (chsIndex < len)
        {
            chs[len - chsIndex - 1] = chars[0];
            chsIndex++;
        }
    }

    return new string(chs);
}

public static byte[] FromBaseN(string str, string chars, bool littleEndian = true, int len = -1)
{
    if (str.Length == 0 || len == 0)
    {
        return new byte[0];
    }

    // This should be the maximum length of the byte[] array. It's 
    // the opposite of the one used in ToBaseN.
    // Note that it can be passed as a parameter
    if (len == -1)
    {
        len = (int)Math.Ceiling(str.Length * Math.Log(chars.Length, 2) / 8);
    }

    BigInteger bi = BigInteger.Zero;
    BigInteger length2 = chars.Length;
    BigInteger mult = BigInteger.One;

    for (int j = 0; j < str.Length; j++)
    {
        int ix = chars.IndexOf(littleEndian ? str[j] : str[str.Length - j - 1]);

        // We didn't find the character
        if (ix == -1)
        {
            throw new ArgumentOutOfRangeException();
        }

        bi += ix * mult;

        mult *= length2;
    }

    var bytes = bi.ToByteArray();

    int len2 = bytes.Length;

    // BigInteger adds a 0 byte for positive numbers that have the
    // last byte > 0x7F
    if (len2 >= 2 && bytes[len2 - 1] == 0)
    {
        len2--;
    }

    int len3 = Math.Min(len, len2);

    byte[] bytes2;

    if (littleEndian)
    {
        if (len == bytes.Length)
        {
            bytes2 = bytes;
        }
        else
        {
            bytes2 = new byte[len];
            Array.Copy(bytes, bytes2, len3);
        }
    }
    else
    {
        bytes2 = new byte[len];

        for (int i = 0; i < len3; i++)
        {
            bytes2[len - i - 1] = bytes[i];
        }
    }

    for (int i = len3; i < len2; i++)
    {
        if (bytes[i] != 0)
        {
            throw new OverflowException();
        }
    }

    return bytes2;
}

请注意,它们非常慢!真的是非常非常慢!(100k需要2分钟)。要加速它们,您可能需要重写除法/模运算,使其直接在缓冲区上工作,而不是每次像BigInteger一样重新创建临时变量。但仍然会很慢。问题在于编码第一个字节所需的时间是O(n),其中n是字节数组的长度(因为需要将整个数组除以36)。除非您想使用5个字节的块并且失去一些位。Base36的每个符号携带约5.169925001位。因此,8个这些符号将携带41.35940001位。非常接近40字节。
请注意,这些方法可以在小端模式和大端模式下同时工作。输入和输出的字节序相同。两种方法都接受len参数。您可以使用它来修剪多余的0(零)。请注意,如果您尝试使输出太小而无法容纳输入,则会抛出OverflowException异常。

1
你确定这个结果是正确的吗?它的结果与我的解决方案不同,而我的解决方案是demonstratably reversible的。我使用了相同的“chars”字符串。 - Jon
你为什么要两次反转数组?这似乎有点多余。 - Paya
@Paya 因为 BigInteger 是小端存储,所以一千被保存为 0001,而“人类”读取数字时是大端存储,所以我们希望将一千读作 1000。 - xanatos
@xanatos 但这假设你 - 作为一个人 - 会亲自阅读字节数组(和字符串),对吗?我的意思是,如果你只是对将字节数组编码为文本感兴趣,并且不关心个人阅读它(说实话,为什么有人要这样做?),那么你可以跳过一些反转,节省一些处理能力,仍然将其解码为原始字节数组。我的意思是,最初的问题是关于二进制到文本编码的,所以OP可能并不关心具有100%数学上正确的基数转换。 - Paya
@Paya 你说得对,但我认为如果数字的“方向”保持不变,那么理解起来会更容易。如果您不需要它,则可以将其删除。(但请注意,我认为Jon的解决方案更好,因为他重新实现了BigInteger的除法函数 :-)(正如我所说,最好重写除法/模操作,使它们直接在缓冲区上工作,而不是每次像BigInteger一样重新创建临时缓冲区) - xanatos
显示剩余9条评论

2
如果您想要一个较短的字符串,并且可以接受 [a-zA-Z0-9] 和 + 和 /,那么请看 Convert.ToBase64String

+1 最佳替代方案。然而,Base64 使用额外的字符(我认为是 +=)。 - C.Evenhuis
同时,Base64包含[A-Z],因此我不能使用它。 - Kees C. Bakker

0
System.Text.Encoding enc = System.Text.Encoding.ASCII;
string myString = enc.GetString(myByteArray);

你可以根据需要尝试不同的编码方式:

System.Text.ASCIIEncoding,
System.Text.UnicodeEncoding,
System.Text.UTF7Encoding,
System.Text.UTF8Encoding

为了满足要求 [a-z][0-9],您可以使用以下代码:

Byte[] bytes = new Byte[] { 200, 180, 34 };
string result = String.Join("a", bytes.Select(x => x.ToString()).ToArray());

您将拥有带有字符分隔符的字节字符串表示。要进行转换,您需要拆分并使用与 .Select() 相同的方法将 string[] 转换为 byte[]


这如何保证字符串中的所有字符都在[a-z][0-9]范围内? - Kees C. Bakker
关于答案:所有这些编码都适用于所需的字符范围。不需要再纠缠了。 - Stephan
不能保证所有内容都在此范围内,因为每个字节不仅可以表示字符和数字。如果需要匹配这些要求,您可以使用任何字符将每个字节保存为带分隔符的整数。它看起来像这样:250a244a...。这只是一种选项,您可以用它来匹配[a-z][0-9]。 - Samich

0

你可以使用模数。 这个例子将你的字节数组编码为[0-9][a-z]字符串。 如果需要,可以进行更改。

    public string byteToString(byte[] byteArr)
    {
        int i;
        char[] charArr = new char[byteArr.Length];
        for (i = 0; i < byteArr.Length; i++)
        {
            int byt = byteArr[i] % 36; // 36=num of availible charachters
            if (byt < 10)
            {
                charArr[i] = (char)(byt + 48); //if % result is a digit
            }
            else
            {
                charArr[i] = (char)(byt + 87); //if % result is a letter
            }
        }
        return new String(charArr);
    }

如果您不想因解码而丢失数据,可以使用此示例:

    public string byteToString(byte[] byteArr)
    {
        int i;
        char[] charArr = new char[byteArr.Length*2];
        for (i = 0; i < byteArr.Length; i++)
        {
            charArr[2 * i] = (char)((int)byteArr[i] / 36+48);
            int byt = byteArr[i] % 36; // 36=num of availible charachters
            if (byt < 10)
            {
                charArr[2*i+1] = (char)(byt + 48); //if % result is a digit
            }
            else
            {
                charArr[2*i+1] = (char)(byt + 87); //if % result is a letter
            }
        }
        return new String(charArr);
    }

现在你有一个字符串,当奇数字符是36的倍数且偶数字符是余数时,它会变成双倍长度。例如:200 = 36 * 5 + 20 => "5k"。

1
你会失去数据。无法将其转换回原始字符串。 - C.Evenhuis

0
通常使用2的幂 - 这样一个字符就映射到固定数量的位。例如,32位的字母表将映射到5位。在这种情况下唯一的挑战就是如何反序列化可变长度字符串。
对于36位,您可以将数据视为一个大数字,然后:
  • 除以36
  • 将余数作为字符添加到结果中
  • 重复,直到除法结果为0
也许说起来容易做起来难。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接