JavaScript 中的字符串压缩

IT技术 javascript google-chrome-extension lossless-compression
2021-02-17 12:02:36

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

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

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

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

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

6个回答

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

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

是否有与您的 LZW 实现兼容的 PHP 库?我下载的那个为 JS 生成的数据返回空字符串。
2021-04-17 12:02:36
谢谢 pieroxy,我试过你的图书馆,但短字符串效率不高。例如。我压缩了一个 250 字节的字符串,我获得了 300 字节的输出。有什么办法处理这个吗?
2021-04-17 12:02:36
@davide 这很奇怪,因为这个库是专门为短字符串量身定制的。例如,如果我复制/粘贴您的评论(200 个字符,因此 400 个字节)并使用演示页面压缩它,它会将其压缩为 188 个字节,小于 50%!如果你一直有问题,只需在 GitHub 上打开一个问题 - 它比这里更适合这种讨论。
2021-04-20 12:02:36
我不知道任何 php 实现。如果需要,主页上有一小部分可以帮助您移植 lib:将LZString 移植到另一种语言
2021-04-21 12:02:36
非常适合我需要使用compressToBase64()decompressFromBase64().
2021-05-15 12:02:36

这是我在一个完整的演示中从 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>');

更多压缩版本(en=264,de=179 字节):gist.github.com/mr5z/d3b653ae9b82bb8c4c2501a06f3931c6
2021-04-19 12:02:36
您应该初始化 vars f,o 以避免 ReferenceError: at Object.de Uncaught ReferenceError: f is not defined Uncaught ReferenceError: o is not defined
2021-04-19 12:02:36
不起作用 - 没有正确重新创建输入:jsfiddle.net/5gmv74b6
2021-05-12 12:02:36

看来,有一个压缩/解压缩 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 构造函数的参数命名为“编码”,但它是压缩格式——“编码”将指代您使用 TextEncoder/TextDecoder 进行的 UTF8 编码。
2021-04-18 12:02:36
谢谢@4esn0k,效果很好!
2021-04-21 12:02:36
@4esn0k 感谢您的回答!但是,我正在努力将压缩数据输出到字符串,即对于“hello world”,我应该得到“H4sIAAAAAAAACstIzcnJVyjPL8pJAQCFEUoNCwAAAA==”。我怎么做?
2021-05-05 12:02:36
@PresianNedyalkov 你需要使用来自stackoverflow.com/a/9458996/839199的函数- _arrayBufferToBase64(compressedData);
2021-05-14 12:02:36

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

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

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

在代码中,以下内容构建了 85 个“安全”字符的列表:

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

然后将 4 个字节 ( b0, b1,b2b3每个从 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)
    };
}

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

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

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

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

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

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

Bambax - 您可以随时要求@Piskvor 做出回答,以便您接受。
2021-05-03 12:02:36