【问题标题】:Size efficient way to transfer array of numbers as text将数字数组作为文本传输的有效大小方法
【发布时间】:2015-05-24 20:55:17
【问题描述】:

假设我有以下数字数组:
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 的数据将只是分隔符。没有更好的解决方案吗?

这个问题是故意提出的,因为我愿意接受任何创造性的解决方案。

【问题讨论】:

  • 实际字节(作为 int)转换为 base64 字符串?还是某种压缩算法(deflate)?
  • 一些压缩算法。该程序需要知道它只适用于数字,因此不需要保留a-z, A-Z, -, ., _, ~,但它需要能够将其解压缩回数字数组。
  • 数字签名了吗?最大的价值是什么?
  • 使用固定表的霍夫曼编码可能会很好。
  • @DaveBish 实际上有些是负数和小数,但我可以通过添加和乘以固定数字来消除它。

标签: c# text numbers transfer


【解决方案1】:

我会尝试将您的 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();

【讨论】:

  • 如果您需要对其中一个步骤进行任何说明,请随时询问
  • 谢谢!我根据您的想法发布了我的解决方案。它使用 15 位数字系统和第 16 个字符作为分隔符。
【解决方案2】:

这是一个完整的正长整数压缩和解压缩代码,基于 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 与 99955500099955574 上看到。

【讨论】:

  • 确实是不错的解决方案。这个 base15 优化在你看到的时候看起来很明显 :-) 还是没有想到。
【解决方案3】:

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

在 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 位块时,读取第三位中的所有位为零表示第三位不存在。

如果您需要读取额外的字节来构建这个数字,那么位 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"

共 48 个字节,共 81 个字符:

"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)

【讨论】:

  • 您确定可以通过 HTTP POST 请求传输所有生成的字符吗?
  • @h3n 操作,忘记了,96 个可能的值中只有 84 个是有效的。我会重新考虑编码,看看我能找到一个有效的模式。谢谢你指点。如果我不能,我应该删除我的答案吗?
  • msb 为零,第一个数字为 4 位,第二个数字为 2 位,分隔符为 1 位,我使用 12 * 4 * 2 = 96 个可能的值,只有 88 个对 HTTP POST 有效,因此此方法使用 12 个可能的字符无效。只使用 11(没有符号或点)会给我们 88 个值,这是可能的,但我想这不是一个好的解决方案。
猜你喜欢
  • 2023-03-03
  • 2012-05-21
  • 2011-08-20
  • 1970-01-01
  • 2017-02-05
  • 2014-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多