JavaScript中的字符串压缩

64

我正在寻找一个JavaScript函数,它可以接受一个字符串并返回一个压缩(更短)的字符串。

我正在开发一个Chrome Web应用程序,将长字符串(HTML)保存到本地数据库中。为了测试,我尝试对存储数据库的文件进行压缩,结果其大小缩小了五倍,因此我认为如果压缩它所存储的内容,可以帮助保持数据库更小。

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

当我用简短的示例字符串“手动”测试时(解码等于编码),它似乎能够工作,并且在Chrome中速度也相当快。但是当给出大字符串(100 ko)时,它似乎会混淆/混合字符串的后一半部分。

是否可能U-LZSS只适用于短字符串而无法处理较大的字符串?并且是否可能调整一些参数以提高上限?


2
除了大小之外,您的测试用例和实际数据是否存在其他差异,例如编码? u-lzss 似乎只能处理 UTF-8 编码的字符串。 - Frédéric Hamidi
2
如果 U-LZSS 不能处理长字符串,那么它就是有缺陷和错误的,不应该被使用。 - Gumbo
1
这似乎是相关的 - 我不会说是重复的,但足够接近以满足您的需求:https://dev59.com/DHVC5IYBdhLWcg3wcgqd - Piskvor left the building
1
显然,原始作者在源代码中放置注释时遇到了一些问题。叹气。压缩是代码可以相当模糊而没有提示意图的那些地方之一。 - T.J. Crowder
@Piskvor:你说得对,这是一个非常相似的问题;我不知道为什么以前没有找到它(我真的尝试过!);我会研究一下那些线索并在这里报告(可能要等到明年...;-) - Bambax
@Frédéric Hamidi:我也有这个疑问,但我不知道该如何测试?当我们在控制台中输入时,所有内容都是UTF-8编码的,对吗?如果我复制一个非UTF-8编码的字符串并将其粘贴到控制台中,会发生什么我也不确定...但是当我这样做时,它看起来并没有出现乱码。而我存储的页面(实际数据)都是UTF-8编码的(至少应该是:它们被作为UTF-8编码提供)。 - Bambax
9个回答

52

有没有与您的LZW实现兼容的PHP库?我下载的那个库返回JS生成的空字符串。 - Tomáš Zato
我不知道是否有任何PHP实现。主页上有一个小部分可以帮助您移植该库(如果需要):将LZString移植到另一种语言 - pieroxy
1
谢谢pieroxy,我尝试了你的库,但对于短字符串来说效率不高。例如,我压缩一个250字节的字符串,结果得到了一个300字节的输出。有什么方法可以解决这个问题吗? - davide
@davide 这很奇怪,因为这个库是专门为短字符串量身定制的。例如,如果我复制/粘贴您的评论(200个字符,即400字节),并使用demo页面进行压缩,则可以将其压缩到188字节,少于50%!如果您仍然遇到问题,请在GitHub上开启一个问题 - 它更适合此类讨论。 - pieroxy
1
使用 compressToBase64()decompressFromBase64() 完美地满足了我的需求。 - TripeHound
这真的很不错。将其用于将相当大的JSON字符串化对象压缩/解压为查询字符串参数,效果很好。感谢您的工作! - reads0520

23
似乎有一个压缩/解压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');
}());

document.getElementById('go').onclick = function () {
  const s = document.getElementById('string').value;
  testCompression(s, 'gzip');
};
<div>
<label>
String to compress:
<input id="string" />
</label>
</div>
<button id="go">Go</button>


1
很棒的答案!只有一个小问题需要澄清:您为CompressionStream / DecompressionStream构造函数命名的参数为“encoding”,但它实际上是压缩格式,而“encoding”应该是指您正在使用的TextEncoder / TextDecoder 的UTF8编码。 - Arbiter
@4esn0k 感谢您的回答!但是,我正在努力将压缩的数据输出到字符串中,即对于“hello world”,我应该得到“H4sIAAAAAAAACstIzcnJVyjPL8pJAQCFEUoNCwAAAA==”。我该怎么做? - Presian Nedyalkov
1
@PresianNedyalkov 你需要使用 https://dev59.com/k2ox5IYBdhLWcg3wVS8J#9458996 中的函数 - _arrayBufferToBase64(compressedData); - 4esn0k
1
谢谢@4esn0k,这个方法非常有效! - Presian Nedyalkov
谢谢。那 brotli 呢? - Mir-Ismaili

22

以下是我从LZW修改的编码函数(en)和解码函数(de),它们的大小分别为276字节和191字节,并且在完全工作的演示中。互联网上没有比我提供的更小、更快的例程。

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>');


1
无法运行 - 不能正确地重新创建输入: http://jsfiddle.net/5gmv74b6/ - vlad_tepesch
1
更加压缩的版本(英文=264,德文=179字节):https://gist.github.com/mr5z/d3b653ae9b82bb8c4c2501a06f3931c6 - mr5
1
你应该初始化变量f和o以避免ReferenceError错误:`在Object.de处` `Uncaught ReferenceError: f未定义` `Uncaught ReferenceError: o未定义` - PartialFlavor_55KP
3
你的代码无法处理 UTF-8 字符。只需在输入字符串中粘贴一个表情符号,解压缩后就会出现混乱。你不支持这些字符可能是导致你的压缩比优于我的原因。 - pieroxy

9

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

  1. 它可以工作
  2. 它将数据库大小减少了一半

...虽然不及5,但总比没有好。所以我使用了它。

(我希望我能接受Piskvor的答案,但那只是一个评论。)


3
Bambax - 你可以随时请求 @Piskvor 给出答案,这样你就能接受它了。 - Matt Mills

7
对我而言,使用UTF-8作为目标压缩字符串似乎不太合理...这看起来只会给自己找麻烦。如果数据传输大小很重要,我认为最好失去一些压缩率并使用纯7位ASCII作为目标。
如果存储限制是基于UTF-16字符,则可以寻找一个大的安全子集来进行转义或UTF-16兼容性,或者如果其他所有内容(例如数据库)没有问题,可以尝试将每个字符用作0..65535。 大多数软件层都应该没有问题(滥用),但请注意,在UTF-16范围内,0xD800-0xDFFF保留用于特殊用途(代理配对),因此某些组合在形式上是“编码错误”,理论上可能会被停止或扭曲。
在我为了好玩编写的一个玩具4 KB JavaScript演示中,我使用了一种编码方式来存储压缩结果,将四个二进制字节存储到从ASCII的85个字符子集中选择的五个字符中(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;
}

要解码,您需要进行反向计算,即从基于85的数字计算x,然后提取4个基于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)
    };
}

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

2
你的回答很好,但在JavaScript中没有UTF-8或任何7位ASCII。每个字符串都在内部以UTF-16编码,并且所有客户端数据库都将存储这种编码。请注意,这并不适用于JavaScript文件的大小,而仅适用于String对象在内存或localStorage中占用的大小。 - pieroxy
我曾经认为数据库使用UTF-8编码来处理字符串(对于ascii更紧凑,支持所有Unicode字符),所以将UTF-16作为压缩的目标会浪费空间。如果数据库存储的是UTF-16编码的字符串,那当然就用UTF-16作为最佳目标。请注意,在JavaScript内存中字符串采用UTF-16编码与此无关。 - 6502
3
在localStorage中,限制是根据字符数量而不是字节数来定义的。因此,无论它使用UTF-8还是UTF-16,在最终结果上并不重要。你可以存储2.5M个字符(在Firefox上为5M),而使用整个UTF-16空间仍然可以提供更多的数据。 - pieroxy
你能举一个你所说的方法的例子吗? - dy_
1
@dy_:我已经添加了一些 JavaScript 代码。 - 6502
显示剩余2条评论

3
当提议的CompressionStreams Web API在所有浏览器上发布后,您可以运行以下内容而无需导入任何模块。它已经适用于Node应用程序。
将字符串压缩为字节数组。
async function compress(inString) { 
  const compressedStream = await new Response(inString).body.pipeThrough(new CompressionStream('gzip'))
  const bytes = await new Response(compressedStream).arrayBuffer()  
  return bytes
}

将字节数组解压缩以获取原始字符串

async function decompress(bytes) {
  const decompressedStream = await new Response(bytes).body.pipeThrough(new DecompressionStream('gzip'))
  const outString = await new Response(decompressed).text()
  return outString
}

1
我认为你也应该了解lz-string,它快速压缩且具有一些优点,这些优点在其页面上列出:
其他库怎么样呢?
- 一些LZW实现会返回数字数组(作为标记存储非常低效),并且不支持255以上的任何字符。 - 一些其他LZW实现会返回字符串(作为标记存储较低效),并且不支持255以上的任何字符。 - 一个LZMA实现是异步的,并且非常慢 - 但是,它是LZMA,而不是实现本身慢。 - 一个GZip实现不是为浏览器而设计的,而是为node.js设计的,它的大小为70kb(它依赖于deflate.js和crc32.js)。
作者创建lz-string的原因:
  • 在移动端工作时,我需要一个快速的工具。
  • 从网站外部收集字符串时,我需要一种可以接受任何类型字符串输入的工具,包括任何大于255的UTF字符。
  • 库不占用70kb是一个明显的优点。产生的字符串应尽可能紧凑以便存储在localStorage中。因此,我无法找到任何在线上能满足我的需求的库。

这个库在其他语言中也有实现。我目前正在研究Python实现,但解压缩似乎存在问题。但如果你只使用JS,这个库看起来非常好。


1

在实现任何内容之前,请尝试通过文本文件进行实验,因为我认为以下内容不一定适用:

所以我想,如果压缩存储的东西,它将有助于保持数据库更小。

这是因为无损压缩算法对于重复模式(例如空格)非常有效。


1
谢谢,但我不理解你的回答。在Chrome中的数据库本身是Sqlite的实现,据我所知并没有使用任何形式的压缩。将整个数据库文件压缩可能会更简单,但我认为这在Chrome应用程序内部不可能实现。因此,在字符串进入数据库之前,我需要对它们进行压缩。 - Bambax
记住,在JavaScript中,所有字符串都是UTF-16编码的,这意味着每个字符占用16位。如果您只使用7位ASCII字符,则每个字符浪费9位。使用占用16位空间的压缩库将显示非常明显的收益。有在线演示可以测试此功能(请参见我对此问题的答案)。 - pieroxy

1
BWTC32Key使用BZip系列改进和Base32768来达到极高的效率,其可选加密为AES256-CTR以避免填充。任何想要的内容(包括字符串)都可以输入其中,并得到一个非常高效的UTF16字符串,其中包含经过大量压缩后的输入(可选压缩后加密,但在Base32768之前)。我将我很久以前制作的829KiB Minecraft命令块命令集通过BWTC32Key运行,得到了一个13078个字符的输出字符串。Minecraft命令块最多可以容纳32767个字符,但游戏的一些旧版本只允许使用一半大小的字符串进行游戏内使用,但通过使用MCEdit,您可以达到32767的大小,不过这个问题很快就被解决了。
总之,829KiB的纯文本比32767的限制要大得多,但是BWTC32Key使其适合不到16K个字符。作为一个更极端的例子,Titin蛋白质的完整化学名称有18.9万个字母。我可以使用BWTC32Key将其缩减到约640个字符。即使使用每个字符超过1个字节的ASCII表示形式(如UTF16)作为输入,仍然可以获得节省。

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