将数字数组作为文本传输的大小有效方式

Size efficient way to transfer array of numbers as text

假设我有以下数字数组:
1,5,500,994,6950,54,54,845,101,54046506452,5980,960406,55,680,68045,66540,321032

使用 http post 将其传输到可将其解码回数字的网站的最有效大小方式是什么?

如果我将它作为文本传输 "1,5,500,994,6950,54,54,845,101,54046506452,5980,960406,55,680,68045,66540,321032" 那么每 1 个数字和分隔符占用 1 个字节,这是一种数据浪费,因为我可能会使用以下所有字符: a-z, A-Z, 0-9, -, ., _, ~ 允许在 http 中使用,无需百分比编码.

我可以将十进制数字系统重新计算为使用 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._ 的 65 数字系统,并使用 ~ 作为分隔符,使其看起来像 b~f~hT~pt~bP8~2~2~na~bK~ULUhIQ~bBa~dGuF~3~kE~qg3~pWT~bk.-。但在我的程序中,我主要使用长度为 3-5 个字符的数字。因此,大约 1/5 的数据只是分隔符。有没有更好的解决办法?

这个问题是故意问的,因为我对任何创造性的解决方案都持开放态度。

我会尝试将你的 12 个可能的字符中的每一个都转换成一个 4 位数,比如

byte[] fourBitArr = new byte[input.Length];
for (int i = 0; i < input.Length; i++) {
    switch (input[i])
    {
        case '0': fourBitArr[i] = 0x0; break;
        case '1': fourBitArr[i] = 0x1; break;
        case '2': fourBitArr[i] = 0x2; break;
        case '3': fourBitArr[i] = 0x3; break;
        case '4': fourBitArr[i] = 0x4; break;
        case '5': fourBitArr[i] = 0x5; break;
        case '6': fourBitArr[i] = 0x6; break;
        case '7': fourBitArr[i] = 0x7; break;
        case '8': fourBitArr[i] = 0x8; break;
        case '9': fourBitArr[i] = 0x9; break;
        case ',': fourBitArr[i] = 0xA; break;
        case '-': fourBitArr[i] = 0xB; break;
    }
}

然后我会创建一个 byte[] 并在每个字节中放入您的 2 个字符(通过将 4 位移到前面):

byte[] byteArr = new byte[(input.Length+1)/2];
for (int i = 0; i < byteArr.Length; i++)
{
    byteArr[i] = (byte)fourBitArr[2*i];
    if (fourBitArr.Length > 2 * i + 1)
        byteArr[i] += (byte)(fourBitArr[2 * i + 1] << 4);
    else
        byteArr[i] += (0xF << 4);
}

结果byte[]可以用Base64编码。这应该用 1,33 个字符对每个字节(您之前的 2 个字符)进行编码。

string output = Convert.ToBase64String(byteArr);

请注意,Base64 会创建一个包含 +/= 的字符串,它们在 URL 中具有特殊含义。您可以将它们中的每一个替换为以下允许的字符之一 -._~

上述算法缩短了您的示例输入

1,5,500,994,6950,54,54,845,101,54046506452,5980,960406,55,680,68045,66540,321032
oaUFoJmklgVapEWKVBoQWgRkBUYlWomgaUBgWqWGoIZApWZFoCMBIw==

这不是最优的,但与最优的相差不远,如果你认为输入的熵是每个字符 ~3.5 位,并且 URL 编码字符的熵大约是6,05 位。该算法可通过找到一种方法来优化,以使用 0xC、0xD、0xE、0xF 作为 fourByteArr 的可能值。

奖金: 将其恢复为原始字符串的代码:

byte[] byteArrReverse = Convert.FromBase64String(output);
StringBuilder sb = new StringBuilder();
foreach (byte b in byteArrReverse)
{
    for (int i = 0; i < 2; i++)
    {
        byte fourBit = (byte)((i == 0) ? b & 0xF : b >> 4);
        switch (fourBit)
        {
            case 0x0: sb.Append('0'); break;
            case 0x1: sb.Append('1'); break;
            case 0x2: sb.Append('2'); break;
            case 0x3: sb.Append('3'); break;
            case 0x4: sb.Append('4'); break;
            case 0x5: sb.Append('5'); break;
            case 0x6: sb.Append('6'); break;
            case 0x7: sb.Append('7'); break;
            case 0x8: sb.Append('8'); break;
            case 0x9: sb.Append('9'); break;
            case 0xA: sb.Append(','); break;
            case 0xB: sb.Append('-'); break;
        }
    }
}
string inputReverse = sb.ToString();

您可以使用每 3 位 2 个字节来打包数字,包括符号、点和分隔符信息。

在 ASCII 字符串中,我们可以使用每个字节的 7 个最低有效位,注意每个字节都需要包含 32 或更高的值。因此,对于一组 2 个字节,我们可以将其命名为:

abcdefgh ijklmnop

位 a 和 i 将始终为零。 位 h 和 p 将用于表示分离。 bcde 将是第一个数字 fgjk 将是第二个数字 lmno 将是第三个数字

为确保每个字节的值始终为 32 或更高,我们可以使用此编码

第一个数字:

9 = 1111
8 = 1110
7 = 1101
6 = 1100
5 = 1011
4 = 1010
3 = 1001
2 = 1000
1 = 0111
0 = 0110
- = 0101
. = 0100

第二个数字:

. = 0010
- = 1010
0 = 0110
1 = 1110
2 = 0001
3 = 1001
4 = 0101
5 = 1101
6 = 0011
7 = 1011
8 = 0111
9 = 1111

第三位: 前面 2 种形式中的任何一种,例如作为第一个数字。当使用 2 位数字块时,读取第三位数字中的所有位为零表示第三位数字不存在。

位 h 和 p 信号,如果你需要读取额外的字节来构建这个数字。

单个数字:

0bcde000

对于 2 位数的号码:

0bcdefg1 0jk00000

对于 3 位数字:

0bcdefg1 0jklmno0

对于 4 位数字:

0bcdefg1 0jklmno1 0rstu000

等等...

不使用字节分隔每个数字,您可以包含符号和小数点分隔符(如点)。

如果大多数数据为 3-5 个字符,则平均每个数据有 2-4 个字节。

这样,您的样本数据将是:

"1,5,500,994,6950,54,54,845,101,54046506452,5980,960406,55,680,68045,66540,321032"

81个字符共48个字节:

"8X[L⌂tgw0[ [ s6;N[-Qw1uY _}0ylSX_ clclW awS I/50"


00111000 (56)
01011000 (88)
01011011 01001100 (91 76)
01111111 01110100 (127 116)
01100111 01110111 00110000 (103 119 48)
01011011 00100000 (91 32)
01011011 00100000 (91 32)
01110011 00110110 (115 54)
00111011 01001110 (59 78)
01011011 00101101 01010001 01110111 00110001 01110101 01011001 00100000 (91 45 81 119 49 117 89 32)
01011111 01111101 00110000 (95 125 48)
01111001 01101100 (121 108)
01010011 01011000 (83 88)
01011111 00100000 (95 32)
01100011 01101100 (99 108)
01100011 01101100 01010111 00100000 (99 108 87 32)
01100001 01110111 01010011 01000000 (97 119 83 32)
01001001 00101111 00110101 00110000 (73 47 53 48)

这是一个完整的正长整数压缩解压代码,基于h3n的1字节2字符思想,谢谢!它使用15个数字系统:从0000到1110二进制和1111(0xF)作为分隔符。

压缩率:

public List<byte> EncodeNumbers(List<long> input)
{
    List<byte> bytes = new List<byte>();
    int bytes_i = 0;
    for (int a = 0; a < input.Count; a++)
    {
        int buffer_i = 65;
        byte[] buffer = new byte[buffer_i];
        while (input[a] > 0)
        {
            buffer[--buffer_i] = (byte)(input[a] %15);
            input[a] /= 15;
        }
        for (int b = 0; b < 65 -buffer_i; b++)
        {
            if (bytes_i %2 == 0)
            {
                bytes.Add((byte)(buffer[b +buffer_i] << 4));
                bytes_i++;
            }else{
                bytes[bytes_i++ /2] += buffer[b +buffer_i];
            }
        }
        if (a +1 != input.Count)
        {
            if (bytes_i %2 == 0)
            {
                bytes.Add(0xF << 4);
                bytes_i++;
            }else{
                bytes[bytes_i++ /2] += 0xF;
            }
        }
        else if (bytes_i %2 != 0)
        {
            bytes[bytes_i++ /2] += 0xF;
        }
    }
    return bytes;
}

解压:

public List<long> DecodeNumbers(List<byte> input)
{
    List<long> numbers = new List<long>();
    int buffer_i = 0;
    byte[] buffer = new byte[17]; // max long = 9223372036854775807 = 160E2AD3246366807 (17 chars)
    for (int a = 0; a < input.Count; a++)
    {
        for (int i = 0; i < 2; i++)
        {
            byte value = (byte)((i == 0) ? input[a] >> 4 : input[a] & 0xF);
            if (value != 0xF)
            {
                buffer[buffer_i++] = value;
            }else{
                long number = 0;
                for (int b = 0; b < buffer_i; b++)
                {
                    number += buffer[buffer_i -1 -b] *(long)Math.Pow(15, b);
                }
                buffer_i = 0;
                numbers.Add(number);
            }
        }
    }
    if (buffer_i != 0)
    {
        long number = 0;
        for (int b = 0; b < buffer_i; b++)
        {
            number += buffer[buffer_i -1 -b] *(long)Math.Pow(15, b);
        }
        numbers.Add(number);
    }
    return numbers;
}

用法:

List<long> numbers = new List<long>{4,10,14,51,5990,922337203685477,64,4685746,56545674,94,1,65454677,665555,1234567890,55555,22,2,3,2,0,99999,99955500099955577,1,2,666,654154,654,58,56,69,7,55,5647,321,25,0,697,9,9,9,9,9,96,5,546,4,645545,64564564,5465498654,6476854,85849865,6478596743,6,6,1,2,3,3,3,548745,6647};

string s = "plain text:\r\n";
string str = "";
foreach (long val in numbers)
{
    str += val + "|";
}
s += str + "\r\n" + str.Length + " bytes\r\n\r\n";

List<byte> encoded = EncodeNumbers(numbers);
s += "compressed base64:\r\n";
str = Convert.ToBase64String(encoded.ToArray());
s += str + "\r\n" + str.Length + " bytes\r\n\r\n";

List<long> decompressed = DecodeNumbers(encoded);
str = "";
foreach (long val in decompressed)
{
    str += val + "|";
}
s += "decompressed:\r\n" + str + "\r\n" + str.Length + " bytes";

Clipboard.SetText(s);

输出:

plain text:
4|10|14|51|5990|922337203685477|64|4685746|56545674|94|1|65454677|665555|1234567890|55555|22|2|3|2|0|99999|99955500099955577|1|2|666|654154|654|58|56|69|7|55|5647|321|25|0|697|9|9|9|9|9|96|5|546|4|645545|64564564|5465498654|6476854|85849865|6478596743|6|6|1|2|3|3|3|548745|6647|
278 bytes

compressed base64:
T6/vNvG5X3GXGSLIRS9E9ihYH05uQZ9k8fWy3qL9IwX3NbfWDxFtrxfy8/L/HpafNlXR4iHg2i8fLy5vzcVPLZ89879J9/OvGhfxZvGv8xf5+fn5+fZvXyZvT8tBX1oFOU8h7FcB74fhBPeAvuXyfbdDSPb28fLz8/P6yNDx6C8=
172 bytes

decompressed:
4|10|14|51|5990|922337203685477|64|4685746|56545674|94|1|65454677|665555|1234567890|55555|22|2|3|2|0|99999|99955500099955574|1|2|666|654154|654|58|56|69|7|55|5647|321|25|0|697|9|9|9|9|9|96|5|546|4|645545|64564564|5465498654|6476854|85849865|6478596743|6|6|1|2|3|3|3|548745|6647|
278 bytes

由于数据类型转换,当数字接近long int的最大值时会丢失一些数据,这可以在99955500099955577 vs 99955500099955574上看到。