【问题标题】:String compression in JavaScriptJavaScript 中的字符串压缩
【发布时间】:2010-12-31 13:19:10
【问题描述】:

我正在寻找一个 JavaScript 函数,它给定一个字符串返回一个压缩(更短)的字符串。

我正在开发一个将长字符串 (HTML) 保存到本地数据库的 Chrome 网络应用程序。出于测试目的,我尝试压缩存储数据库的文件,它缩小了 5 倍,所以我认为如果我压缩它存储的内容,它将有助于使数据库更小。

我在 JavaScript 中找到了 LZSS 的实现:http://code.google.com/p/u-lzss/ ("U-LZSS")。

当我用简短的示例字符串(解码 === 编码)“手动”测试它时,它似乎工作了,而且在 Chrome 中它也相当快。但是当给定大字符串(100 ko)时,它似乎会混淆/混淆字符串的后半部分。

是否有可能 U-LZSS 需要短字符串而无法处理较大的字符串?是否可以调整一些参数以移动该上限?

【问题讨论】:

  • 除了大小之外,您的测试用例和您的实际数据之间是否还有其他差异,例如编码? u-lzss 似乎只适用于 UTF-8 编码的字符串。
  • 如果 U-LZSS 不能处理长字符串,那只是错误且不正确,不应使用。
  • 这似乎相关 - 我不会说重复,但足够接近可以满足您的需求:stackoverflow.com/questions/294297/…
  • 显然原作者放cmetsin the source有些问题。 sigh 压缩是代码在没有意图提示的情况下非常不透明的地方之一。
  • @Piskvor:你说得对,这是一个非常接近的问题;我不知道我以前怎么没找到它(我真的试过了!);我会调查那里的线索并在这里报告(明年某个时候...... ;-)

标签: javascript google-chrome-extension lossless-compression


【解决方案1】:

看来,有一个压缩/解压API的提议:https://github.com/wicg/compression/blob/master/explainer.md

根据https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html 的博客文章,它已在 Chrome 80(目前处于 Beta 版)中实现。

我不确定我是否在流和字符串之间进行了良好的转换,但这是我使用新 API 的尝试:

function compress(string, encoding) {
  const byteArray = new TextEncoder().encode(string);
  const cs = new CompressionStream(encoding);
  const writer = cs.writable.getWriter();
  writer.write(byteArray);
  writer.close();
  return new Response(cs.readable).arrayBuffer();
}

function decompress(byteArray, encoding) {
  const cs = new DecompressionStream(encoding);
  const writer = cs.writable.getWriter();
  writer.write(byteArray);
  writer.close();
  return new Response(cs.readable).arrayBuffer().then(function (arrayBuffer) {
    return new TextDecoder().decode(arrayBuffer);
  });
}

const test = "http://www.ScriptCompress.com - Simple Packer/Minify/Compress JavaScript Minify, Fixify & Prettify 75 JS Obfuscators In 1 App 25 JS Compressors (Gzip, Bzip, LZMA, etc) PHP, HTML & JS Packers In 1 App PHP Source Code Packers Text Packer HTML Packer or v2 or v3 or LZW Twitter Compress or More Words DNA & Base64 Packer (freq tool) or v2 JS JavaScript Code Golfer Encode Between Quotes Decode Almost Anything Password Protect Scripts HTML Minifier v2 or Encoder or Escaper CSS Minifier or Compressor v2 SVG Image Shrinker HTML To: SVG or SVGZ (Gzipped) HTML To: PNG or v2 2015 JS Packer v2 v3 Embedded File Generator Extreme Packer or version 2 Our Blog DemoScene JS Packer Basic JS Packer or New Version Asciify JavaScript Escape JavaScript Characters UnPacker Packed JS JavaScript Minify/Uglify Text Splitter/Chunker Twitter, Use More Characters Base64 Drag 'n Drop Redirect URL DataURI Get Words Repeated LZMA Archiver ZIP Read/Extract/Make BEAUTIFIER & CODE FIXER WHAK-A-SCRIPT JAVASCRIPT MANGLER 30 STRING ENCODERS CONVERTERS, ENCRYPTION & ENCODERS 43 Byte 1px GIF Generator Steganography PNG Generator WEB APPS VIA DATAURL OLD VERSION OF WHAK PAKr Fun Text Encrypt Our Google";

async function testCompression(text, encoding = 'deflate') {
  console.log(encoding + ':');
  console.time('compress');
  const compressedData = await compress(text, encoding);
  console.timeEnd('compress');
  console.log('compressed length:', compressedData.byteLength, 'bytes');
  console.time('decompress');
  const decompressedText = await decompress(compressedData, encoding);
  console.timeEnd('decompress');
  console.log('decompressed length:', decompressedText.length, 'characters');
  console.assert(text === decompressedText);
}

(async function () {
  await testCompression(test, 'deflate');
  await testCompression(test, 'gzip');
}());

【讨论】:

  • 很好的答案!为了清楚起见,一点点挑剔:您命名为“编码”的 CompressionStream/DecompressionStream 构造函数的参数,但它是 the compression format -“编码”指的是您使用 TextEncoder/TextDecoder 进行的 UTF8 编码。
  • @4esn0k 感谢您的回答!但是,我正在努力将压缩数据输出到字符串,即对于“hello world”,我应该得到“H4sIAAAAAAAACstIzcnJVyjPL8pJAQCFEUoNCwAAAA==”。我该怎么做?
  • @PresianNedyalkov 你需要使用来自stackoverflow.com/a/9458996/839199 的函数 - _arrayBufferToBase64(compressedData);
  • 谢谢@4esn0k,效果很好!
  • 谢谢。那么brotli呢?
【解决方案2】:

我刚刚发布了一个小型的LZW 实现,专门为此目的量身定制,因为现有的实现都不能满足我的需求。

这就是我未来使用的,我可能会在某个时候尝试改进库。

【讨论】:

  • 是否有与您的 LZW 实现兼容的 PHP 库?我下载的那个返回的是 JS 生成的数据的空字符串。
  • 我不知道任何 php 实现。如果需要,主页上有一小部分可以帮助您移植 lib:Porting LZString to another language
  • 感谢 pieroxy,我尝试了您的库,但使用短字符串时效率不高。例如。我压缩了一个 250 字节的字符串,得到了 300 字节的输出。有什么办法解决吗?
  • @davide 这很奇怪,因为这个库是专门为短字符串量身定制的。例如,如果我复制/粘贴您的评论(200 个字符,400 个字节)并用the demo page 压缩它,它会将其压缩到 188 个字节,小于 50%!如果您一直遇到问题,只需在 GitHub 上打开一个问题 - 它比这里更适合这种讨论。
  • 使用 compressToBase64()decompressFromBase64() 完美满足我的需求。
【解决方案3】:

这里是我从 LZW 在一个完整的工作演示中修改的编码(276 字节,函数 en)和解码(191 字节,函数 de)函数。互联网上没有比我在这里为您提供的更小或更快的例程了。

function en(c){var x='charCodeAt',b,e={},f=c.split(""),d=[],a=f[0],g=256;for(b=1;b<f.length;b++)c=f[b],null!=e[a+c]?a+=c:(d.push(1<a.length?e[a]:a[x](0)),e[a+c]=g,g++,a=c);d.push(1<a.length?e[a]:a[x](0));for(b=0;b<d.length;b++)d[b]=String.fromCharCode(d[b]);return d.join("")}

function de(b){var a,e={},d=b.split(""),c=f=d[0],g=[c],h=o=256;for(b=1;b<d.length;b++)a=d[b].charCodeAt(0),a=h>a?d[b]:e[a]?e[a]:f+c,g.push(a),c=a.charAt(0),e[o]=f+c,o++,f=a;return g.join("")}

var compressed=en("http://www.ScriptCompress.com - Simple Packer/Minify/Compress JavaScript Minify, Fixify & Prettify 75 JS Obfuscators In 1 App 25 JS Compressors (Gzip, Bzip, LZMA, etc) PHP, HTML & JS Packers In 1 App PHP Source Code Packers Text Packer HTML Packer or v2 or v3 or LZW Twitter Compress or More Words DNA & Base64 Packer (freq tool) or v2 JS JavaScript Code Golfer Encode Between Quotes Decode Almost Anything Password Protect Scripts HTML Minifier v2 or Encoder or Escaper CSS Minifier or Compressor v2 SVG Image Shrinker HTML To: SVG or SVGZ (Gzipped) HTML To: PNG or v2 2015 JS Packer v2 v3 Embedded File Generator Extreme Packer or version 2 Our Blog DemoScene JS Packer Basic JS Packer or New Version Asciify JavaScript Escape JavaScript Characters UnPacker Packed JS JavaScript Minify/Uglify Text Splitter/Chunker Twitter, Use More Characters Base64 Drag 'n Drop Redirect URL DataURI Get Words Repeated LZMA Archiver ZIP Read/Extract/Make BEAUTIFIER & CODE FIXER WHAK-A-SCRIPT JAVASCRIPT MANGLER 30 STRING ENCODERS CONVERTERS, ENCRYPTION & ENCODERS 43 Byte 1px GIF Generator Steganography PNG Generator WEB APPS VIA DATAURL OLD VERSION OF WHAK PAKr Fun Text Encrypt Our Google");
var decompressed=de(compressed);

document.writeln('<hr>'+compressed+'<hr><h1>'+compressed.length+' characters versus original '+decompressed.length+' characters.</h1><hr>'+decompressed+'<hr>');

【讨论】:

  • 不起作用 - 无法正确重新创建输入:jsfiddle.net/5gmv74b6
  • 更多压缩版本(en=264,de=179 字节):gist.github.com/mr5z/d3b653ae9b82bb8c4c2501a06f3931c6
  • 你应该初始化 vars f,o 以避免出现 ReferenceError: at Object.de Uncaught ReferenceError: f is not defined Uncaught ReferenceError: o is not defined
  • 您的代码使用 UTF-8 字符失败。只需在输入字符串上粘贴一个笑脸,解压后就搞砸了。您缺乏对这些的支持可能是您的压缩率比我的更好的原因。
【解决方案4】:

对我来说,使用 UTF-8 作为目标来压缩字符串似乎并不合理……看起来只是在找麻烦。我认为如果线下大小很重要,最好放弃一些压缩并使用纯 7 位 ASCII 作为目标。

如果存储限制基于 UTF-16 字符,那么如果您关心转义或 UTF-16 合规性,则可以寻找一个大的安全子集,或者您可以尝试将每个 char 用作 0..65535(如果是其他所有字符)涉及的(例如数据库)没有问题。 大多数软件层在(ab)使用方面应该没有问题,但请注意,在 UTF-16 范围内,0xD800-0xDFFF 保留用于特殊用途(代理对),因此某些组合在形式上是“编码错误”,理论上可以停止或变形。

在一个玩具4 KB JavaScript demo 中,我为了好玩而写了一个编码,将四个二进制字节存储到五个字符中,这些字符是从 85 个字符的 ASCII 子集中选择出来的,这些字符很干净,可以嵌入到 JavaScript 字符串(85 ^5 略大于 (2^8)^4,但仍符合 JavaScript 整数的精度)。这使得压缩数据例如对于JSON 是安全的,无需任何转义。

以下代码构建了 85 个“安全”字符的列表:

let cset = "";
for (let i=35; i<35+85+1; i++) {
    if (i !== 92) cset += String.fromCharCode(i);
}

然后将 4 个字节(b0b1b2b3 每个从 0...255)编码为 5 个字符,代码为:

// First convert to 0...4294967295
let x = ((b0*256 + b1)*256 + b2)*256 + b3;

// Then convert to base 85
let result = "";
for (let i=0; i<5; i++) {
    let x2 = Math.floor(x / 85);
    result += cset[x - x2*85];
    x = x2;
}

要解码,请执行相反的操作,即从 base-85 数字计算 x,然后提取 4 个 base-256 数字(即字节)。

注意:在环面代码中,我使用了稍微不同的字符集,而不是跳过 92 \,而是将其替换为 126 ~。有兴趣的可以看下完整解压代码

// There are two Huffman-encoded code streams
//    T - single chars (0..127) and sequence lengths (128...255)
//    A - high bits of relative addresses of sequence (0..255)
//
// Expansion algorithm is:
//    1) Read a code X from T
//    2) If it's a char (X < 128) then add to output
//    3) otherwise (X>=128) read sequence address ADDR from stream A (high bits)
//       and from input (low bits) and copy X-128 bytes from ADDR bytes "ago"
//

let Z = 5831; // expanded size
let i = 0, // source ptr
    a = 0, // current bits accumulator
    n = 0; // number of available bits in a

// Read a single bit
let b = function(){
    if (!n) {
        // There are no more bits available in the accumulator, read a new chunk:
        // 5 ASCII escape-safe chars will be transformed in 4 8-bit binary bytes
        // (like BASE64, just a bit more dense)
        a = 0;
        let w = 5;
        while (w--) {
            let y = s.charCodeAt(i+w);          // get next char
            a = a*85 + (y > 125 ? 92 : y) - 35; // extract base-85 "digit" (note, uses ~ instead of \ that requires quoting)
        }
        n = 32; // we got 32 bits in a
        i += 5; // we consumed 5 characters from source
    }
    return (a >> --n) & 1;  // extract a single bit
};

// Read a code of z bits by concatenating bits coming from b()
let v = function(z){
    return (--z ? v(z) : 0)*2+b();
};

// Read an Huffman (sub-)tree: a bit will tell if we need to
// read a two sub-trees or a leaf
let h = function(){
    return b() ? [h(), h()] : v(8);
};

// Read A and T Huffman trees
let A = h(), T = h();

// Extract a code given a node:
//   if the node is an array (intermediate node) then we need to read a bit
//   from the input binary stream to decide which way to go down the tree,
//   if it's a number then we just return the value.
//   `n.map` is truthy for arrays and falsy for numbers.
let d = function(n){
    return n.map ? d(n[b()]) : n;
};

let S="";  // Output

// While we're not done
while(S.length<Z){
    // Extract a code from T
    x = d(T);
    if (x < 128) {
        // This is a single character, copy to output
        S += String.fromCharCode(x);
    } else {
        // This is a sequence of x-128 bytes, get address and copy it
        // Note: high 8 bits are from the Huffman tree A and 8 low bits
        // are instead directly form the bit stream as they're basically
        // noise and there's nothing to gain by trying to compress them.
        S += S.substr(S.length-(d(A)<<8)-v(8), x-128)
    };
}

(请注意,我没有测试这个重新格式化/注释的版本,可能存在拼写错误)

【讨论】:

  • 这看起来很有趣。次要的挑剔:您正在使用的函数 转义的一种形式(或者更确切地说,转义是编码的子集,并且可以正确编码)-它将“潜在问题”字符映射到一组 85 个 ASCII“可能安全”字符。
  • 我不确定我理解你的意思。我选择使用从 35 到 126 的前 85 个字符(跳过 92 个),以便可以将生成的压缩数据简单地用双引号括起来。压缩数据几乎是随机的,例如,如果我不跳过 92 而只是重复它,那么解码器会因为简化而缩短一点,但 HTML 大小仍然比 4096 字节大很多,显然是完全不可接受的 :-D ... 说得更好,我发现转义压缩数据比选择不需要转义的编码更糟糕。
  • 您的回答很好,但是在 JavaScript 中没有 UTF-8 也没有任何 7 位 ASCII。每个字符串都在内部以 UTF-16 编码,这就是所有客户端数据库将存储的内容。请注意,这不适用于 JavaScript 文件的大小,而仅适用于 String 对象在内存或 localStorage 中的大小。
  • @6502 localStorage 中的限制是根据字符定义的,而不是字节。所以到底是使用 UTF-8 还是 UTF-16 并不重要。您可以存储 250 万个字符(在 Firefox 上为 5M),并且使用整个 UTF-16 空间仍然可以为您提供更多数据。
  • @dy_: 我添加了一些 javascript 代码
【解决方案5】:

BWTC32Key 使用 BZip 系列改进和 Base32768 来获得极高的效率,其可选加密是 AES256-CTR 以避免填充。您想要的任何东西(包括字符串)都可以输入其中,结果将是一个非常有效的 UTF16 字符串,其中包含重压缩后的输入(以及压缩后但在 Base32768 之前的可选加密。)我运行了我的 829KiB 自制 Minecraft 纲要来自 eons 之前通过 BWTC32Key 的命令块命令,我得到了一个 13078 字符的输出字符串。 Minecraft 命令块最多可以包含 32767 个字符,但是一些旧版本的游戏只允许在游戏中使用一半大小的字符串,尽管使用 MCEdit 可以达到 32767 大小,不过这个问题很快就得到了解决。

无论如何,829KiB 的纯文本远远大于 32767 的限制,但 BWTC32Key 使它适合小于 16K 的字符。举个更极端的例子,Titin 蛋白的化学全名是 18.9 万个字母。我可以使用 BWTC32Key 将其降低到 640 左右。即使使用高于每个字符 1 个字节的 ASCII 表示(如 UTF16)作为输入仍然可以节省成本。

【讨论】:

    【解决方案6】:

    在 Piskvor 的建议下,我测试了在这个问题的答案中找到的代码:JavaScript implementation of Gzip (投票最多的答案:LZW 实现)并发现:

    1. 有效
    2. 它将数据库的大小减少了两倍

    ... 小于 5 但总比没有好!所以我就用了。

    (我希望我能接受 Piskvor 的回答,但这只是评论)。

    【讨论】:

    • Bambax - 您可以随时要求@Piskvor 做出回答,以便您接受。
    【解决方案7】:

    在实现任何东西之前尝试使用文本文件,因为我认为以下内容不一定成立:

    所以我认为,如果我压缩它存储的内容,这将有助于使数据库更小。

    这是因为无损压缩算法非常适合重复模式(例如空格)。

    【讨论】:

    • 谢谢,但我不明白你的回答。 Chrome 中的数据库本身是 Sqlite 的实现,不使用任何类型的压缩 AFAIK。将数据库文件作为一个整体进行压缩会更简单,但我认为这在 Chrome 应用程序中是不可能的。所以我需要在字符串进入数据库之前对其进行压缩。
    • 请记住,在 JavaScript 中所有字符串都是UTF-16,这意味着每个字符的权重为 16 位。如果您只使用 7 位 ASCII 字符,那么字符串中的每个字符都会浪费 9 位。使用巧妙地占用 16 位空间的压缩库将显示出不可忽略的增益。有在线演示可以对此进行测试(请参阅我对这个问题的回答)
    【解决方案8】:

    我认为您还应该查看lz-string,它的速度很快,压缩得很好,并且在他们的页面上列出了一些优势:

    其他库呢?

    • 一些 LZW 实现返回数字数组(存储非常低效,因为令牌占用 64 位)并且不支持 255 以上的任何字符。
    • 其他一些 LZW 实现返回一个字符串(存储效率不那么低,但所有标记仍然占用 16 位)并且不支持 255 以上的任何字符。
    • 一个异步且非常慢的 LZMA 实现 - 但是,嘿,它是 LZMA,而不是慢的实现。
    • GZip 实现并不真正适用于浏览器,而是适用于 node.js,它的重量为 70kb(它依赖于 deflate.js 和 crc32.js)。

    作者创建lz-string的原因:

    • 在移动设备上工作我需要一些快速的东西。
    • 使用从我的网站外部收集的字符串,我需要可以将任何类型的字符串作为输入的东西,包括 255 以上的任何 UTF 字符。
    • 库不占用 70kb 是一个明确的优势。 产生尽可能紧凑的字符串以存储在 localStorage 中的东西。 所以我在网上找到的库都不能很好地满足我的需求。

    这个库有其他语言的实现,我目前正在研究python的实现,但目前解压似乎有问题,但如果你只坚持JS,对我来说真的很好。

    【讨论】:

      猜你喜欢
      • 2013-10-30
      • 2011-07-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多