将大数字(或字符串)压缩为小值

6

我的ASP.NET页面有以下查询字符串参数:

…?IDs=1000000012,1000000021,1000000013,1000000022&...

在这里,IDs参数将始终包含由某些字符分隔的数字,例如此处为,。目前有4个数字,但通常它们会在37之间。

现在,我正在寻找一种方法来将上述每个大数字转换为尽可能小的值;具体来说是压缩IDs查询字符串参数的值。欢迎压缩每个数字算法或整个IDs查询字符串参数的压缩。

  1. 编码或解码不是问题;只需压缩IDs查询字符串参数的值。
  2. 创建一些唯一的小值用于IDs,然后从某些数据源检索其值超出了范围。

是否有一种算法可以将这样的大数字压缩为小值或者将IDs查询字符串参数的值整体压缩?


1
这些数字的范围是什么?所有的数字(0-9)都被使用了吗?数字2-8总是为0吗? - H H
1
不是答案 - 但解决方案需要考虑压缩背后的原理吗?如果它在生成的页面中被大量包含,那么几乎肯定要使用gzip压缩,在比通过微小的压缩管理更好的性能下为您压缩所有HTML。如果目的是为了提高用户输入URL的速度,那么答案将需要考虑这一点。 - Pool
还有其他人认为他应该从每个数字中减去-1000000000,然后在服务器端添加回来:D。但说真的,我看不出这样做的理由。你应该实现一个更好的系统。你需要这样做的确切原因是什么?你遇到了什么问题? - Noon Silk
那些数字是由第三方工具生成的唯一数字,并由不同的数据库团队管理。正如我在帖子中所说,我想压缩每个数字或ID参数的值,以便不会有太长的查询字符串URL。 - Dave
相关:是否有一种快速且不花哨的C#代码/算法来压缩一个逗号分隔数字字符串,使其接近最大信息密度?(来源:http://stackoverflow.com/questions/6023117/is-there-a-fast-and-non-fancy-c-code-algorithm-to-compress-a-string-of-comma-sep) - David Cary
显示剩余4条评论
6个回答

16
你需要这么多空间存储数字,因为你使用十进制来表示它们。一种改进方法是使用十六进制(hex)。例如,你可以将255(3位数字)表示为ff(2位数字)。
你可以通过使用更大的数字基数来进一步扩展这个概念...所有有效的查询字符串参数字符集:
A-Z、a-z、0-9、'.'、'-'、'~'、'_'、'+'
这给了你一个包含67个字符的基数(参见QueryString的Wikipedia)。
请查看此SO帖子以了解将十进制转换为任意数字基数的方法。
编辑:
在链接的SO帖子中,请查看以下部分:
string xx = IntToString(42, 
            new char[] { '0','1','2','3','4','5','6','7','8','9',
            'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
            'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x'});

这几乎是你所需要的。只需添加它缺少的几个字符,就可以扩展它:

yz.-~_+

那篇文章缺少回到十进制的方法。我不会写 :-) 但是过程如下:
定义一个计数器,我称之为TOTAL。
查看最右边的字符,并找到它在数组中的位置。
TOTAL =(字符在数组中的位置) 例如:输入为BA1。现在TOTAL为1(因为“1”位于数组的第1个位置)
现在查看第一个字符左侧的下一个字符,并找到它在数组中的位置。 TOTAL += 47 *(字符在数组中的位置) 例如:输入为BA1。现在TOTAL为(47 * 11)+1 = 518
现在查看前一个字符左侧的下一个字符,并找到它在数组中的位置。 TOTAL += 47 * 47 *(字符在数组中的位置) 例如:输入为BA1。总计现在为(47 * 47 * 10)+(47 * 11)+1 = 243508
依此类推。
我建议您编写一个单元测试,将一堆十进制数字转换为47进制,然后再转回来,以确保您的转换代码正常工作。
请注意,您用3个47进制数字表示了一个6位十进制数字 :-)

谢谢Eric J。如果我理解正确的话,我应该使用更高的进制来进行转换。如果是这样,你建议使用哪个数字作为基数?“...所有有效的查询字符串参数字符集:”你能否详细解释一下? - Dave
1
<a href="en.wikipedia.org/wiki/Base64">Base64</a> 是高度推荐的编码方式,比 base 67 更安全! - Blue Toque
@Dave:我建议使用Base 67,使用我在帖子中列出的字符。这些字符允许在查询字符串参数中使用而不需要进行URL编码。看一下链接。它提供了从十进制到任意进制转换的C#源代码。我会编辑我的帖子,概述如何返回到十进制。 - Eric J.
1
@Dave:更新完成。这种方法的性能应该非常好。与通过互联网调用Web服务器的时间相比,编码数字所需的时间应该微不足道。 - Eric J.
@Oplopanax:Base64(标准实现)并不是最优的,因为它使用的一些字符会被 URL 编码,导致查询字符串比必要的更长。为什么 base 67 是不安全的?假设 Dave 写了一个单元测试来确保他的转换正常工作,据我所见,它没有任何不安全的地方。 - Eric J.
显示剩余2条评论

4

你的数字范围是多少?假设它们可以适合16位整数,我会:

  • 将所有数字存储为16位整数(每个数字2个字节,范围为-32,768到32,767)
  • 构建一个16位整数的字节流(XDR可能是一个不错的选择;至少要确保正确处理字节序
  • Base64编码字节流,使用修改后的用于URL的base64编码(每个数字大约为3个字符)

额外的好处是,您不再需要逗号字符,因为您知道每个数字都是2个字节。

另外,如果这还不够好,我会使用zlib来压缩你的整数流,然后将zlib压缩流进行base64编码。如果16位不够大(即如果你确实需要1000000000范围内的数字),你也可以切换到32位整数。

编辑:

也许有点晚了,但这里有一个可能满足你需求的实现:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Scratch {
    class Program {
        static void Main(string[] args) {
            //var ids = new[] { 1000000012, 1000000021, 1000000013, 1000000022 };
            var rand = new Random();
            var ids = new int[rand.Next(20)];
            for(var i = 0; i < ids.Length; i++) {
                ids[i] = rand.Next();
            }

            WriteIds(ids);
            var s = IdsToString(ids);
            Console.WriteLine("\nResult string is: {0}", s);
            var newIds = StringToIds(s);
            WriteIds(newIds);
            Console.ReadLine();
        }

        public static void WriteIds(ICollection<Int32> ids) {
            Console.Write("\nIDs: ");
            bool comma = false;
            foreach(var id in ids) {
                if(comma) {
                    Console.Write(",");
                } else {
                    comma = true;
                }
                Console.Write(id);
            }
            Console.WriteLine();
        }

        public static string IdsToString(ICollection<Int32> ids) {
            var allbytes = new List<byte>();
            foreach(var id in ids) {
                var bytes = BitConverter.GetBytes(id);
                allbytes.AddRange(bytes);                
            }
            var str = Convert.ToBase64String(allbytes.ToArray(), Base64FormattingOptions.None);
            return str.Replace('+', '-').Replace('/', '_').Replace('=', '.');
        }

        public static ICollection<Int32> StringToIds(string idstring) {
            var result = new List<Int32>();
            var str = idstring.Replace('-', '+').Replace('_', '/').Replace('.', '=');
            var bytes = Convert.FromBase64String(str);
            for(var i = 0; i < bytes.Length; i += 4) {
                var id = BitConverter.ToInt32(bytes, i);
                result.Add(id);
            }
            return result;
        }
    }
}

谢谢Daniel,这是C#语言,数字可能是这样的: 1000000012、1000000021、1000000013、1000000022 - Dave
太好了,Daniel。非常感谢。 - Dave

4

这里有另一个非常简单的方案,可以为形如N + delta的一组数字提供很好的压缩,其中N是一个较大的常数。

public int[] compress(int[] input) {
    int[] res = input.clone();
    Arrays.sort(res);
    for (int i = 1; i < res.length; i++) {
        res[i] = res[i] - res[i - 1];
    }
    return res;
}

这将把集合{1000000012,1000000021,1000000013,1000000022}缩减为列表[1000000012,1,9,1],然后您可以通过另一个答案中描述的方法,以base47编码表示数字进一步压缩。

使用简单的十进制编码,字符数从44个减少到16个,即减少了63%。(使用base47将获得更好的压缩效果)。

如果不能对ID进行排序,则无法获得很好的压缩。 对于此示例,{1000000012,1000000021,1000000013,1000000022}可以压缩为列表[1000000012,9,-8,9],对于此示例只多出一个字符。

总之,与通用压缩算法或编码方案相比,这种方法更适用于此类输入。


@ Mark:假设排序没问题,它可以处理数字集合中不止一个N的值,尽管每个新的N都会增加一定量的不可压缩性。 - Stephen C

1

如果唯一的问题是URL长度,您可以将数字转换为base64字符,然后在服务器端将它们转换回数字


2
Base64并不是最优的选择,因为它使用了字符'+', '/', 和 '=',而且它们会被URL编码(使它们比必要的更长)。 - Eric J.
1
将字符串编码为base64编码会使它们变得更大而不是更小(可以在http://www.opinionatedgeek.com/dotnet/tools/Base64Encode/Default.aspx上尝试)。当您想要以ascii形式表示二进制数据时,Base64编码非常方便,但不提供任何压缩。 - Darwyn
我不是指“将字符串转换为base64”...我是在说:“将数字转换为base64”...即将当前数字的十进制表示转换为base64字符串,这应该会压缩它们。但我同意Eric J的观点,有些字符不应该使用。 - Aziz
@Eric:你看了Aziz提供的链接吗?它描述了“base64url”编码,可以避免URL编码扩展。 - David Cary

0

你获取的ID有多规律?如果ID是随机生成的,那么我即将提出的方法效率不会很高。但是,如果你举例的ID代表了你所获得类型的特点,那么以下方法或许可行?

我通过一个例子来说明这个想法。

例如,你有一个ID为1000000012,希望将其压缩。为什么不将其存储为[{1},{0,7},{12}]?这意味着第一个数字是1,后面跟着7个0,然后是12。因此,如果我们使用{x}表示x的一个实例,而使用{x,y}表示x连续出现y次。

你可以在这基础上进行一些模式匹配和/或函数拟合。

例如,模式匹配:1000100032可以表示为[{1000,2}{32}]。

例如,函数拟合: 如果你的ID有10个数字,那么把ID分成两个5位数,并存储通过这两个点的直线方程。如果ID = 1000000012,则y1 = 10000,y2 = 12。因此,您的斜率为-9988,截距为10000(假设x1 = 0,x2 = 1)。在这种情况下,它并没有改进,但如果数字更随机,则可能会改进。同样,您可以使用分段线性函数存储一系列ID。
无论如何,这主要取决于您的ID结构。

0

我猜你这么做是为了绕过请求URL长度限制...

其他答案建议将十进制ID数字编码为十六进制、base47或base64,但你可以(理论上)通过使用LZW(或类似算法)来压缩ID列表,比这些方法更好。根据ID列表中冗余的程度,即使在重新编码压缩字节为文本后,你也可以获得超过40%的减少。

简而言之,我建议你找到一个用JavaScript实现的现成文本压缩库,并在客户端使用它来压缩ID列表。然后使用base47/base64对压缩后的字节串进行编码,并将编码后的字符串作为URL参数传递。在服务器端执行反向操作,即解码后再解压缩。

编辑:作为一个实验,我创建了一个包含36个不同标识符的列表,类似于你提供的标识符,并使用gzip进行了压缩。原始文件大小为396字节,压缩文件大小为101字节,压缩+base64文件大小为138字节。总体上减少了65%。对于更大的文件,压缩比率实际上可能会更高。然而,当我尝试对小输入集(例如仅包含4个原始标识符)进行此操作时,没有压缩,编码后的大小甚至比原始大小还要大。

谷歌“lzw库javascript”

理论上,可能有更简单的解决方案。将参数作为“post数据”发送而不是在请求URL中,并让浏览器使用其了解的编码之一应用压缩。这样可以获得更多的节省,因为无需将压缩数据编码为合法的URL字符。

问题在于如何让浏览器以独立于浏览器的方式压缩请求。


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