如何在NodeJs中将zip文件下载并解压到内存中?

55

我想从互联网下载一个zip文件并在内存中解压缩它,而不需要将其保存到临时文件中。我该如何做?

这是我尝试过的:

var url = 'http://bdn-ak.bloomberg.com/precanned/Comdty_Calendar_Spread_Option_20120428.txt.zip';

var request = require('request'), fs = require('fs'), zlib = require('zlib');

  request.get(url, function(err, res, file) {
     if(err) throw err;
     zlib.unzip(file, function(err, txt) {
        if(err) throw err;
        console.log(txt.toString()); //outputs nothing
     });
  });

[编辑] 如建议的那样,我尝试使用adm-zip库,但仍然无法使其正常工作:

var ZipEntry = require('adm-zip/zipEntry');
request.get(url, function(err, res, zipFile) {
        if(err) throw err;
        var zip = new ZipEntry();
        zip.setCompressedData(new Buffer(zipFile.toString('utf-8')));
        var text = zip.getData();
        console.log(text.toString()); // fails
    });

7
请注意,zlib 并不处理 zip 文件格式,它只处理 gzip 和 deflate 格式。 zlib.unzip 函数的命名有误导性,因为它只能解压缩 gzip 和 deflate 格式。您需要一个支持 zip 格式的库。 - Dan D.
1
这个zip文件看起来很有前途 https://github.com/springmeyer/node-zipfile/blob/master/README.md - Tina CG Hoehr
@Dan:实际上,zlib也处理zlib格式(反过来使用deflate)。但这在这里完全无关紧要,所以+1 :-) - Cameron
可能是重复的问题。https://dev59.com/g3I95IYBdhLWcg3w7CfL - Larry Battle
2
你在编辑中的第二个例子使用了 request.get,这会自动调用返回数据的 toString()。但是 adam-zip 需要的是 Buffer 而不是 String。使用 request({url: url, encoding: null}, function(err, res, zipFile) { ... 代替 request.get 来让 request 返回 Buffer。(尽管我这样做时出现了“CRC32 校验和失败”的错误 :( )你应该真的跳过 request 直接使用下面 Mihai 的答案。 - Nathan Friedly
4个回答

99
您需要一个能够处理缓冲区的库。最新版本的adm-zip可以胜任:
npm install adm-zip
我的解决方案使用 http.get 方法,因为它返回缓冲区块。
代码:
var file_url = 'http://notepad-plus-plus.org/repository/7.x/7.6/npp.7.6.bin.x64.zip';

var AdmZip = require('adm-zip');
var http = require('http');

http.get(file_url, function(res) {
  var data = [], dataLen = 0; 

  res.on('data', function(chunk) {
    data.push(chunk);
    dataLen += chunk.length;

  }).on('end', function() {
    var buf = Buffer.alloc(dataLen);

    for (var i = 0, len = data.length, pos = 0; i < len; i++) { 
      data[i].copy(buf, pos); 
      pos += data[i].length; 
    } 

    var zip = new AdmZip(buf);
    var zipEntries = zip.getEntries();
    console.log(zipEntries.length)

    for (var i = 0; i < zipEntries.length; i++) {
      if (zipEntries[i].entryName.match(/readme/))
        console.log(zip.readAsText(zipEntries[i]));
    }
  });
});

这个想法是创建一个缓冲区数组,并在最后将它们合并成一个新的缓冲区。这是因为缓冲区无法调整大小。

更新

这是一个更简单的解决方案,它使用request模块以缓冲区方式获取响应,通过在选项中设置encoding: null实现。它还自动跟随重定向和解析 http/https。

var file_url = 'https://github.com/mihaifm/linq/releases/download/3.1.1/linq.js-3.1.1.zip';

var AdmZip = require('adm-zip');
var request = require('request');

request.get({url: file_url, encoding: null}, (err, res, body) => {
  var zip = new AdmZip(body);
  var zipEntries = zip.getEntries();
  console.log(zipEntries.length);

  zipEntries.forEach((entry) => {
    if (entry.entryName.match(/readme/i))
      console.log(zip.readAsText(entry));
  });
});

响应的body是一个缓冲区,可以直接传递给AdmZip,从而简化整个过程。


2
我想强调的是,仅仅通过npm install adm-zip安装adm-zip是不行的,因为只有在github上的最新版本才支持buffers。 - enyo
2
最新的npm版本支持缓冲区。 - Nikolai
11
无法处理从Github标签中下载的zip文件 - 错误:无效或不支持的zip格式。未找到END标头。 - Sam
@Sam 如果你像我之前说的那样完全依靠记忆来写整个代码,是行不通的——如果你不使用流,它会被转换成字符串。当我下载 GitHub 压缩文件时,这个答案对我很有用:https://dev59.com/IWct5IYBdhLWcg3whtyh#12029764 - yurisich
11
我使用了 axios 来发送请求,如果你将 responseType 设置为 'arraybuffer',则它有一个选项可以将整个内容作为 ArrayBuffer 下载。然后,你可以直接将 response.data 传递给 AdmZip。 - Ciprian Tomoiagă
显示剩余4条评论

5

遗憾的是,你无法像Node zlib库允许的那样将响应流传输到解压作业中。你必须缓存并等待响应结束。我建议你在处理大文件时将响应导向fs流,否则你的内存会很快被填满!

我不完全理解你试图做什么,但我认为这是最佳方法。你应该只在真正需要数据的时间将数据保存在内存中,然后流式传输到csv解析器

如果你想在内存中保留所有数据,你可以使用替换csv解析器方法fromPathfrom,它接受缓冲区,并在getData中直接返回unzipped

你可以使用AMDZip代替node-zip(如@mihai所说),但请注意AMDZip尚未发布在npm上,因此你需要:

$ npm install git://github.com/cthackers/adm-zip.git

注意:假设zip文件只包含一个文件

var request = require('request'),
    fs = require('fs'),
    csv = require('csv')
    NodeZip = require('node-zip')

function getData(tmpFolder, url, callback) {
  var tempZipFilePath = tmpFolder + new Date().getTime() + Math.random()
  var tempZipFileStream = fs.createWriteStream(tempZipFilePath)
  request.get({
    url: url,
    encoding: null
  }).on('end', function() {
    fs.readFile(tempZipFilePath, 'base64', function (err, zipContent) {
      var zip = new NodeZip(zipContent, { base64: true })
      Object.keys(zip.files).forEach(function (filename) {
        var tempFilePath = tmpFolder + new Date().getTime() + Math.random()
        var unzipped = zip.files[filename].data
        fs.writeFile(tempFilePath, unzipped, function (err) {
          callback(err, tempFilePath)
        })
      })
    })
  }).pipe(tempZipFileStream)
}

getData('/tmp/', 'http://bdn-ak.bloomberg.com/precanned/Comdty_Calendar_Spread_Option_20120428.txt.zip', function (err, path) {
  if (err) {
    return console.error('error: %s' + err.message)
  }
  var metadata = []
  csv().fromPath(path, {
    delimiter: '|',
    columns: true
  }).transform(function (data){
    // do things with your data
    if (data.NAME[0] === '#') {
      metadata.push(data.NAME)
    } else {
      return data
    }
  }).on('data', function (data, index) {
    console.log('#%d %s', index, JSON.stringify(data, null, '  '))
  }).on('end',function (count) {
    console.log('Metadata: %s', JSON.stringify(metadata, null, '  '))
    console.log('Number of lines: %d', count)
  }).on('error', function (error) {
    console.error('csv parsing error: %s', error.message)
  })
})

2
如果您使用的是MacOS或Linux,可以使用unzip命令从stdin解压缩。在这个例子中,我将zip文件从文件系统读入Buffer对象中,但也适用于下载的文件。
// Get a Buffer with the zip content
var fs = require("fs")
  , zip = fs.readFileSync(__dirname + "/test.zip");


// Now the actual unzipping:
var spawn = require('child_process').spawn
  , fileToExtract = "test.js"
    // -p tells unzip to extract to stdout
  , unzip = spawn("unzip", ["-p", "/dev/stdin", fileToExtract ])
  ;

// Write the Buffer to stdin
unzip.stdin.write(zip);

// Handle errors
unzip.stderr.on('data', function (data) {
  console.log("There has been an error: ", data.toString("utf-8"));
});

// Handle the unzipped stdout
unzip.stdout.on('data', function (data) {
  console.log("Unzipped file: ", data.toString("utf-8"));
});

unzip.stdin.end();

这实际上只是 Node 版本的:

cat test.zip | unzip -p /dev/stdin test.js

EDIT: 值得注意的是,如果输入的zip文件太大而无法从stdin中一次性读取,则此方法将无法工作。如果您需要读取更大的文件,并且您的zip文件仅包含一个文件,请改用funzip代替unzip

var unzip = spawn("funzip");

如果您的zip文件中包含多个文件(而您想要的文件不是第一个),我很抱歉地告诉您运气不佳。解压缩需要在.zip文件中查找,因为zip文件只是一个容器,而解压缩可能只会解压缩其中的最后一个文件。在这种情况下,您必须暂时保存该文件(node-temp很方便)。

2
我对没有留下评论就进行负投票的原因很感兴趣。说真的,这个为什么不能工作?我是一个初学者。 - Strawberry
我也不明白为什么会有匿名踩票,而没有评论...我猜可能是因为这只适用于一个文件,或者压缩文件相对较小的原因。 - enyo
1
谢谢您,我曾经花费了很长时间来搞定一堆糟糕/没有文档的ZIP库,只是为了解压缩存档。这个方法太棒了。 - Sam
在Mac或Linux上使用“unzip”的优点是,在解压缩后它将保留所有原始文件的权限,但是Node.js中的createWriteStream会默认将'0666'写入文件(adm-zip使用它)。 - mygoare
由于.zip文件格式的设计,不可能在不牺牲正确性的情况下从头到尾解释.zip文件。中央目录是.zip文件内容的权威,位于.zip文件的末尾而非开头。这样的流/管道需要缓冲整个.zip文件才能到达中央目录之前解释任何内容(这与此示例的目的相违背)。对于一个较复杂的zip文件进行此操作会导致"未找到中央目录签名"错误。 - Ryan McGeary
它要求从URL下载,而不是本地路径。 - likeachamp

1
两天前,模块node-zip已经发布,它是JavaScript版本Zip的包装器:JSZip
var NodeZip = require('node-zip')
  , zip = new NodeZip(zipBuffer.toString("base64"), { base64: true })
  , unzipped = zip.files["your-text-file.txt"].data;

2
node-zip不支持缓冲区,因此您被迫将其转换为字符串,这是一件坏事。 - Nikolai

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