【问题标题】:Embedding binary data in web page?在网页中嵌入二进制数据?
【发布时间】:2013-06-18 03:46:38
【问题描述】:

我有一个包含 6000 个元素的数据结构,对于每个元素,我需要存储 7 位信息。如果我天真地将它存储为一个包含 6000 个元素的数组,其中填充了数字,它会占用大约 22 KB。我正在尝试减小页面的大小——存储 6000*7 位信息的最佳方式是什么(应该在 5 KB 左右)。我想要一个像数据结构一样的“比特流”。我考虑过将其编码为字符串甚至图像,但不确定。我没有编码为字符串的原因是因为我无法从数学上保证没有一个字符不是不可打印的 ASCII 字符之一(例如 ASCII 1-25)

【问题讨论】:

  • 将位移到不包括 0 的某个范围的字符串可能是最简单的解决方案(类似于 charFromCode[value +32] 以获取安全范围内的所有值) - 轻松访问每个元素...
  • 可以看到this
  • @Ishank - 我实际上尝试了指向该页面的库(pieroxy.net/blog/pages/lz-string/index.html),有趣的是它是一个非常糟糕的压缩库(作者自己承认,如果你有它会增加输出的大小超过 750 个字符,在我的情况下是正确的)。该解决方案适用于压缩要存储在本地存储中的小字符串。任何其他客户端压缩方案都可能有效,但我正在寻找纯粹基于 bithacks 的东西。
  • 为什么要减少呢?在浏览器中节省网络带宽或内存占用,或用于存储(例如 localStorage)?
  • 您是否启用了服务器压缩?我会认为 gzip 会很好地压缩一个简单的数组? 22k 是观察到的网络效应,还是只是服务器文件大小的增加?

标签: javascript performance bit-manipulation steganography page-size


【解决方案1】:

让我们考虑两种解决方案。

基数 32

为了好玩,让我们考虑使用 base-32 数字。是的,你可以在 JavaScript 中做到这一点。

首先将四个 7 位值打包成一个整数:

function pack(a1,a2,a3,a4){
    return ((a1 << 8 | a2) << 8 | a3) << 8 | a4;
}

现在,转换为基数 32。

function encode(n){
    var str = "000000" + n.toString(32);
    str = str.slice(0,6);
    return str;
}

不应超过六位数。我们确保它正好是六个。

走向另一个方向:

function decode(s){
    return parseInt(s, 32);
}

function unpack(x){
    var a1 = x & 0xff0000>>24, a2 = x & 0x00ff0000>>16, a3 = x & 0x0000ff00>>8, a4 = x & 0x000000ff;
    return [a1, a2, a3, a4];
}

剩下的就是围绕这个包装逻辑来处理 6000 个元素。压缩:

function compress(elts){
    var str = '';
    for(var i = 0; i < elts.length; i+=4){
        str += encode(pack(elts[i], elts[i+1], elts[i+2], elts[i+3])
    }
    return str;
}

然后解压缩:

function uncompress(str){
    var elts = [];
    for(var i = 0; i < str.length; i+=6){
        elts = elts.concat(unpack(decode(str.slice(i, i+6)));
    }
    return elts;
}

如果您将所有 6,000 个元素的结果连接起来,您将拥有 1,500 个压缩数字,每个包含 6 个字符的数字将变成大约 9K。每个 7 位值大约 1.5 个字节。这绝不是信息论的最大压缩,但也没有那么糟糕。 解码只需反转过程:

Unicode

首先我们将两个 7 位值打包成一个整数:

function pack(a1,a2){
    return (a1 << 8 | a2) << 8;
}

我们将对所有 6,000 个输入执行此操作,然后使用我们的朋友 String.fromCharCode 将所有 3,000 个值转换为 3,000 个字符的 Unicode 字符串:

function compress(elts){
    var packeds = [];
    for (var i = 0; i < elts.length; i+=2) {
        packeds.push(pack(elts[i], elts[i+1]);
    }
    return String.fromCharCode.apply(0, packeds);
}

反过来说,很简单:

function uncompress(str) {
    var elts = [], code;
    for (var i = 0; i < str.length; i++) {
        code=str.charCodeAt(i);
        elts.push(code>>8, code & 0xff);
    }
    return elts;
}

这将占用每两个 7 位值两个字节,因此比 base 32 方法的效率高约 33%。

如果上面的字符串将被写到脚本标签中作为 Javascript 赋值,例如var data="HUGE UNICODE STRING";,那么字符串中的引号需要被转义:

javascript_assignment = 'var data = "' + compress(elts).replace(/"/g,'\\"') + '";';

上述代码并非用于生产,特别是不处理输入数量不是四或二的倍数的极端情况。

【讨论】:

  • 辉煌;如果您可以将问题修改为人们可能涉及的用例,这将是一个与人们分享的好例子。
【解决方案2】:

正如 dandavis 所说,可以将不可打印的 ASCII 字符编码为 JSON 字符串。但是对于随机数据,它给了我 13KB(因为必须转义许多字符)。您可以将字符串编码为 base64,然后再编码为 JSON 字符串。它给了我 7.9KB 的随机数据。

var randint = function (from, to) {
    return Math.floor(Math.random() * (to - from + 1)) + from;
}

var data = '';
for (var i = 0; i < 6000; ++i) {
    data += String.fromCharCode(randint(0, 127));
}
// encoding `data` as JSON-string at this point gave me 13KB

var b64data = btoa(data);
// encoding `b64data` as JSON-string gave me 7.9KB

解码

var data = atob(b64data);
var adata = [];
for (var i = 0; i < data.length; ++i) {
    adata.push(data.charCodeAt(i));
}

肯定应该有更有效的方法来编码您的数据,但我相信这是对复杂性和效率的一种折衷。 PS。在某些浏览器中,您可能需要自己编写atobbtoa

【讨论】:

  • 详述“在某些浏览器中可能需要自己编写 atob 和 btoa”部分:atob 和 btoa 需要 IE 11、Edge 16、Firefox 52、Chrome 49、Safari 10.1(或 9.3在 iOS 上)或 Opera 45(或 Mini)。默认情况下,它不会在早期浏览器中出现,如果您在 Node.JS 上运行,也不会出现在 Android 4 应用程序的 WebView 中(Chrome 更新不会修复这些 WebView 组件)。
【解决方案3】:

实际上,如果您使用 JSON 将任何潜在的恶意代码编码为 JS 转义代码,则字符串可以正常工作:

var codes=",Ñkqëgdß\u001f", // (10 chars JSON encoded to store all chars ranges)
mySet=codes[4].charCodeAt().toString(2).split("").map(Number).map(Boolean).reverse();

alert(mySet); // shows: [true,false,false,false,true,true,true] 


/*  broken down into bite-sized steps: (pseudo code)
char == "g" (codes[4])
"g".charCodeAt() == 103
(103).toString(2) == "1100111"
.split().map(Number) ==  [1,1,0,0,1,1,1]
.map(Boolean).reverse() == [true,true,true,false,false,true,true]  */

要填充数组,请反转过程:

var toStore= [true, false, true, false, true, false, true];
var char= String.fromCharCode(parseInt(toStore.map(Number).reverse().join(""),2));
codes+=char;

//verify (should===true):   
codes[10].charCodeAt().toString(2).split("")
   .map(Number).map(Boolean).reverse().toString() === toStore.toString();

要将结果导出到 ascii 文件 JSON.stringify(codes),或者如果保存到 localStorage,您可以只保存原始字符串变量,因为浏览器使用 localStorage 的每个字符两个字节...

【讨论】:

    猜你喜欢
    • 2010-09-06
    • 1970-01-01
    • 1970-01-01
    • 2011-05-25
    • 2014-10-17
    • 2016-08-11
    • 1970-01-01
    • 2011-07-08
    • 1970-01-01
    相关资源
    最近更新 更多