通常的情况下遇到压缩包,都是解开后再进行文件操作。
比如把A.zip
解压缩成-> a.txt
,再用程序打开a.txt
正常读取。
上网查各种例子也是文件解开到文件。
很多年前我们也是这么干的,直到发现了这些情况:
似乎是4G时代吧,VoLTE的时代,各个厂商都用极其冗长的格式来存储并传输数据。
导致压缩比非常大比如大到100:1
。也就是1GB的压缩包,解开后是100GB的数据。
这些无谓的磁盘开销改为直接读取压缩包就可以完全避免。
服务器虚拟化后,有些磁盘读写(尤其是写)特别慢。
比如200MB的数据如果解压成4GB再进行处理,单个数据节点需要4小时,不解压只要2分钟左右。
时间的差距实在是巨大。
所以不得已,只能直接读写压缩文件了。
不能调用外部的命令,还得兼顾跨平台,各种语言实现的方式都不太一样。
能读通常就能写,加上读文本比读二进制块更“高级”,所以就用读文本做例子吧。
带有⭐️的例子,能够完成“直接读文本行”的功能,其它例子得自己改。
是Windows下最常见的格式。
单个压缩包内可以包含多个文件。
虽然压缩率不及RAR和7Z,读取速度不及GZIP,但是在Windows下兼容性是最好的。
常见的软件:WinZip。
我完全不懂python,不过挺好用呢。
对zip的支持是自带的。
参考:🔗Python的数据压缩与存档。
import zipfile
...
if not zipfile.is_zipfile(SourceFile):
raise Exception(SourceFile+" is not in ZIP format!")
with zipfile.ZipFile(SourceFile) as zipMy:
for oneItem in zipMy.infolist():
with zipMy.open(oneItem) as filein:
lines = filein.readlines()
# do what ever with lines.
看上去Java是流式处理。
如果某文件被zip重复压缩了几次,用Java也可以一次读出最内层的数据(理论如此,没试过)。
用org.apache.commons.compress
。
在pom.xml
中添加依赖:
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-compressartifactId>
<version>1.24.0version>
dependency>
在.java
代码文件中:
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
...
try(ZipFile zipFile = new ZipFile(SourceFile, "GBK")){
Enumeration<?> enums = zipFile.getEntries();
while(enums.hasMoreElements()){
ZipArchiveEntry entry = (ZipArchiveEntry) enums.nextElement();
if(!entry.isDirectory()){
try(BufferedReader br = new BufferedReader(new InputStreamReader(zipFile.getInputStream(entry), Charset.forName("GBK"))))
{
String aLine;
while((aLine=br.readLine()) != null) {
// do what ever with every Line.
}
}
}
}
}
同上,发现Go的Reader也能流式处理。
本身代码没几行,但是error
处理占了不少代码。
还有defer close
,显然没有Python的with
简练,也比不过Java的try with resource
,哎这也是Go的一大特点。
import (
...
"archive/zip"
...
)
...
zipReader, err := zip.OpenReader(SourceFile)
if err != nil {
panic(err)
}
defer func(zipReader *zip.ReadCloser) {
err := zipReader.Close()
if err != nil {
panic(err)
}
}(zipReader)
for _, f := range zipReader.File {
if !f.FileInfo().IsDir() {
inFile, err := f.Open()
if err != nil {
panic(err)
}
OneReader := bufio.NewReader(inFile)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}
}
}
在Lazarus(FPC)官方WIKI关于paszlib
中有接近的方式:
FPC官方相关的代码如下(稍改就行):
uses
Zipper;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
ExtractFileFromZip(FileNameEdit1.FileName,Edit1.Text);
end;
procedure TForm1.DoCreateOutZipStream(Sender: TObject; var AStream: TStream;
AItem: TFullZipFileEntry);
begin
AStream:=TMemorystream.Create;
end;
procedure TForm1.DoDoneOutZipStream(Sender: TObject; var AStream: TStream;
AItem: TFullZipFileEntry);
begin
AStream.Position:=0;
Memo1.lines.LoadFromStream(Astream);
Astream.Free;
end;
procedure TForm1.ExtractFileFromZip(ZipName, FileName: string);
var
ZipFile: TUnZipper;
sl:TStringList;
begin
sl:=TStringList.Create;
sl.Add(FileName);
ZipFile := TUnZipper.Create;
try
ZipFile.FileName := ZipName;
ZipFile.OnCreateStream := @DoCreateOutZipStream;
ZipFile.OnDoneStream:=@DoDoneOutZipStream;
ZipFile.UnZipFiles(sl);
finally
ZipFile.Free;
sl.Free;
end;
end;
新版Delphi可以这样:
uses
System.Zip,
...
var
line:String;
aLH:TZipHeader
...
begin
LZip:=TZipFile.Create;
LZip.Open(SourceFile,zmRead);
LZip.Encoding:=TEncoding.GetEncoding(936);
for i:=0 to LZip.FileCount-1 do
begin
LOutput := TMemoryStream.Create();
LZip.Read(i,LOutput,aLH);
var asr:=TStreamReader.Create(LOutput);
while not asr.EndOfStream do
begin
line:String;
line:=asr.ReadLine;
// do what ever with every Line.
end;
FreeAndNil(asr);
FreeAndNil(LOutput);
end;
FreeAndNil(LZip);
end;
使用zlib
。下面的例子用的是zlib1.3(August 18, 2023)
如果是从实际的zip文件来处理,似乎可以这样。
PS:同样是zlib提供的,gz可以按行读取(见下面gz的部分),而zip只能读数据块。
⚠️按行读字符串需要再判断一下\n
的位置啥的,自己组成字符串。。
unzFile zfile = unzOpen64(SourceFile);
unz_global_info64 globalInfo;
if (UNZ_OK != unzGoToFirstFile(zfile))
{
return false;
}
char fileName[512] = { 0 };
unz_file_info64 fileInfo;
do
{
if (UNZ_OK != unzGetCurrentFileInfo64(zfile, &fileInfo, fileName, sizeof(fileName), nullptr, 0, nullptr, 0))
{
return false;
}
if (fileInfo.external_fa == FILE_ATTRIBUTE_DIRECTORY) // 文件夹
{
//如果需要处理,就自己建目录啊。
}
else // 普通文件
{
if (UNZ_OK != unzOpenCurrentFile(zfile))
{
return false;
}
int size = 0;
while (unzReadCurrentFile(zfile, Buffer, bufferSize) != NULL)
{
//do what ever with Buffer
}
}
} while (unzGoToNextFile(zfile) != UNZ_END_OF_LIST_OF_FILE);
unzClose(zfile);
是Linux/Unix下最常见的压缩格式。
单个压缩包内只能包含单个文件。
对gz的支持是自带,不用额外安装包。
根本和打开文件文件没区别。
似乎读不到Meda data中的原始文件名。
import gzip
...
with gzip.open(SourceFile,'r') as gzMy:
lines=gzMy.readlines()
# do what ever with lines.
可以用GzipCompressorInputStream.getMetaData().getFilename()
读取原始文件名(没存就为空)。
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-compressartifactId>
<version>1.24.0version>
dependency>
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
...
try(BufferedReader br = new BufferedReader(new InputStreamReader(new GzipCompressorInputStream(Files.newInputStream(Paths.get(SourceFile))), Charset.forName("GBK"))))
{
String aLine;
while((aLine=br.readLine()) != null) {
// do what ever with every Line.
}
}
可以从*gzip.Reader.Name
中读取原始文件名(没存就为空)。
import (
...
"compress/gzip"
...
)
...
fz, err := gzip.NewReader(SourceFile)
if err != nil {
panic(err)
}
defer func(fz *gzip.Reader) {
err := fz.Close()
if err != nil {
panic(err)
}
}(fz)
OneReader := bufio.NewReader(fz)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}
也是官方的例子,似乎没有读一行的方式。
不过改改也能用是吧(下面是张图片,代码请参考官方文档)。
新版Delphi可以这样:
uses
System.ZLib,
...
var
line:String;
...
begin
LInput := TFileStream.Create(SourceFile, fmOpenRead);
LUnGZ := TZDecompressionStream.Create(LInput,15 + 16);
var asr:=TStreamReader.Create(LUnGZ);
while not asr.EndOfStream do
begin
line:String;
line:=asr.ReadLine;
// do what ever with every Line.
end;
FreeAndNil(asr);
FreeAndNil(LUnGZ);
FreeAndNil(LInput);
end;
使用zlib
。下面的例子用的是zlib1.3(August 18, 2023)
如果是从实际的gz文件来处理,似乎可以这样。
PS:严谨些需要判断读到的内容长度是否为bufferSize-1(没到换行符呢),自己再处理一下。
gzFile gzf = gzopen(SourceFile, "r");
while (gzgets(gzf, Buffer, bufferSize)!=NULL)
{
//do what ever with Buffer as char*
}
gzclose(gzf);
但如果来自流,比如下面的InputStream
,那么代码就有点长了。
⚠️下面的代码没有提供readline
这种方便的方式。
需要自己把解开的outBuffer
复制到自己的缓存中,再判断一下\n
的位置啥的,自己组成字符串。
如果还需要自由的fseek
移动位置,那么还是全部写入内存流当中再操作比较好。
供参考(改改才能用):
z_stream zlibStream;
memset(&zlibStream, 0, sizeof(zlibStream));
zlibStream.next_in = nullptr;
zlibStream.avail_in = 0;
zlibStream.next_out = nullptr;
zlibStream.avail_out = 0;
zlibStream.zalloc = Z_NULL;
zlibStream.zfree = Z_NULL;
zlibStream.opaque = Z_NULL;
if (inflateInit2(&zlibStream, 16 + MAX_WBITS) != Z_OK) {
// show error
if (error_list_.end() == error_list_.find(path))
{
error_list_[path] = 0x00;
}
error_list_[path] |= 0x04;
continue;
}
char* inBuffer = new char[bufferSize];
char* outBuffer = new char[bufferSize];
int zlibResult;
do {
InputStream.read(inBuffer, bufferSize);
zlibStream.next_in = reinterpret_cast<Bytef*>(inBuffer);
zlibStream.avail_in = (uInt)InputStream.gcount();
do {
zlibStream.next_out = reinterpret_cast<Bytef*>(outBuffer);
zlibStream.avail_out = bufferSize;
zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
if (zlibResult == Z_STREAM_ERROR) {
// show error
inflateEnd(&zlibStream);
if (error_list_.end() == error_list_.find(path))
{
error_list_[path] = 0x00;
}
error_list_[path] |= 0x04;
continue;
}
//Do something with decompressed data, write to some file?
//OutputStream.write(outBuffer, bufferSize - zlibStream.avail_out);
} while (zlibStream.avail_out == 0);
} while (InputStream.good() || zlibResult == Z_OK);
delete[] inBuffer;
delete[] outBuffer;
inflateEnd(&zlibStream);
OriginalFile.flush();
OriginalFile.close();
是Linux/Unix下最常见的打包格式。
单个压缩包内可以包含多个文件。
打包通常和压缩同时使用,比如:.tar.gz
,.tgz
,.tar.xz
。
对tar的支持是自带,不用额外安装包。
下面例子是.tar.gz
,.tgz
的,其它的格式类似。
Python不用依次串起来解gz和解tar。只需要指定open时的参数。
可以从TarInfo.name
得到包中每个文件的名字。
import tarfile
...
with tarfile.open(SourceFile,'r:gz') as tarMy:
for oneItem in tarMy.getmembers():
with tarMy.extractfile(oneItem) as filein:
lines = filein.readlines()
# do what ever with lines.
同上例子是单独的.tar
:
可以从org.apache.tools.tar.TarEntry.getName()
得到包中每个文件的名字。
<dependency>
<groupId>org.apache.antgroupId>
<artifactId>antartifactId>
<version>1.10.14version>
dependency>
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarInputStream;
...
try (TarInputStream in = new TarInputStream(Files.newInputStream(new File(SourceFile).toPath()),"UTF8")) {
TarEntry entry;
while ((entry = in.getNextEntry()) != null) {
if (entry.isFile()) {
try(BufferedReader br = new BufferedReader(new InputStreamReader(in, Charset.forName("GBK"))))
{
String aLine;
while((aLine=br.readLine()) != null) {
// do what ever with every Line.
}
}
}
}
}
同上例子是单独的.tar
文件。
如果是tar.gz
则和解gz串起来就好了。
类似:OneTAR := tar.NewReader(GZReader)
这样。
可以从*tar.Header.FileInfo().Name()
得到包中每个文件的名字。
import (
...
"archive/tar"
...
)
...
OneTAR := tar.NewReader(SourceFile)
for {
h, err := OneTAR.Next()
if err == io.EOF {
break
} else if err != nil {
panic(err)
}
if !h.FileInfo().IsDir() {
OneReader := bufio.NewReader(OneTAR)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}
}
}
请参考官方文档。
新版Delphi可以这样:
需要LibTar(Tar Library),也许还有其它实现方式,不太清楚。
同上例子是单独的.tar
文件。
如果是tar.gz
则和解gz串起来就好了。
uses
LibTar,
...
var
line:String;
DirRec:TTarDirRec;
...
begin
LInput := TFileStream.Create(SourceFile, fmOpenRead);
TarStream:= TTarArchive.Create(LInput);
TarStream.Reset;
while TarStream.FindNext(DirRec) do
begin
LOutput := TMemoryStream.Create();
TarStream.ReadFile(LOutput);
var asr:=TStreamReader.Create(LOutput);
while not asr.EndOfStream do
begin
line:String;
line:=asr.ReadLine;
// do what ever with every Line.
end;
FreeAndNil(asr);
FreeAndNil(LOutput);
end;
FreeAndNil(TarStream);
FreeAndNil(LInput);
end;
是Windows下主流的压缩格式。
单个压缩包内可以包含多个文件。
压缩比较高,最新的是RAR5格式。
常见的软件:🔗WinRAR。
需要pip install rarfile
。
用法和zip基本没区别,实测没成功,需要调用unrar.exe什么的。
此项暂时保留(参考zipfile的方式真的几乎一样)。
用com.github.junrar
不能处理RAR5格式.
用net.sf.sevenzipjbinding
可以处理RAR5,但直接用只能read数据块。
用这种方法不仅可以读RAR,还可以解压非常多的格式,包括7Z,ZIP,TAR,GZ,ARJ,CAB,WIM,etc……💡
如果仅仅需要解压成文件,那么单这一种方式就可以处理常见的所有压缩格式啦。
下面取得文件名那部分和解压RAR无关,主要是gz格式有取不到文件名的情况(no metadata)。
修改pom中的依赖为sevenzipjbinding-all-platforms
,支持更多的平台(MAC,ARM……)💡。
<dependency>
<groupId>net.sf.sevenzipjbindinggroupId>
<artifactId>sevenzipjbindingartifactId>
<version>16.02-2.01version>
dependency>
<dependency>
<groupId>net.sf.sevenzipjbindinggroupId>
<artifactId>sevenzipjbinding-windows-amd64artifactId>
<version>16.02-2.01version>
dependency>
<dependency>
<groupId>net.sf.sevenzipjbindinggroupId>
<artifactId>sevenzipjbinding-linux-amd64artifactId>
<version>16.02-2.01version>
dependency>
代码中的SevenZip.openInArchive(null...
为啥用null,是为了自动检测格式。
顺便吐槽匿名函数,不熟悉看不懂在干嘛。
实际上是在处理ISequentialOutStream->write(byte[] data)
。
import actp.tnu.api.CrossLog;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
...
public static void Uncompress(File inputFile, String targetFileDir) throws Exception {
File newdir = new File(targetFileDir);
if (!newdir.exists() && !newdir.mkdirs()) {
throw new Exception("Create Dir failed! : " + targetFileDir);
}
try (RandomAccessFile randomAccessFile = new RandomAccessFile(inputFile, "r");
IInArchive inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))) {
ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface();
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (!item.isFolder()) {
ExtractOperationResult result = item.extractSlow(data -> {
try {
String fileName = GetNameAndPathOK(inputFile, targetFileDir, item);
try (FileOutputStream fos = new FileOutputStream(targetFileDir + File.separator + fileName, true)) {
fos.write(data);
//change sth here, if you want to read line
}
} catch (Exception e) {
throw new SevenZipException(e.getMessage());
}
return data.length;
});
if (result != ExtractOperationResult.OK) {
//error
}
}
}
}
}
private static String GetNameAndPathOK(File inputFile, String targetFileDir, ISimpleInArchiveItem item) throws Exception {
String fileName = item.getPath();
if (fileName == null || fileName.isEmpty()) {
fileName = inputFile.getName().substring(0, inputFile.getName().lastIndexOf("."));
}
if (fileName.indexOf(File.separator) > 0) {
String path = targetFileDir + File.separator + fileName.substring(0, fileName.lastIndexOf(File.separator));
File newdir1 = new File(path);
if (!newdir1.exists() && !newdir1.mkdirs()) {
throw new Exception("Create Dir failed! : " + path);
}
}
return fileName;
}
使用github.com/nwaples/rardecode
。
也有其它的方式处理RAR,比如archiver.NewRar().Unarchive(Source,Dest)
是从文件解压到文件,但不能直接读压缩包的内容。
import (
...
"github.com/nwaples/rardecode"
...
)
...
RARReader, err := rardecode.OpenReader(SourceFile, "")
if err != nil {
panic(err)
}
defer func(RARReader *rardecode.ReadCloser) {
err := RARReader.Close()
if err != nil {
panic(err)
}
}(RARReader)
for {
f, err := RARReader.Next()
if err != nil {
break
}
if !f.IsDir {
OneReader := bufio.NewReader(RARReader)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}
}
}
是Windows下新晋的压缩格式,扩展名.7z
。
单个压缩包内可以包含多个文件。
压缩比较高,处理速度较快。
常用的软件:🔗7Zip。不过我比较喜欢基于它的:🔗NanaZip。
💡Nana = なな = 七。
需要安装包:pip install py7zr
。
import py7zr
...
if not py7zr.is_7zfile(SourceFile):
raise Exception(SourceFile+" is not in 7Z format!")
with py7zr.SevenZipFile(SourceFile) as sevenMy:
for oneItem in sevenMy.files:
filein = sevenMy.read([oneItem.filename])
lines = filein[oneItem.filename].readlines()
# do what ever with lines.
用org.apache.commons.compress
。
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-compressartifactId>
<version>1.24.0version>
dependency>
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
...
try (SevenZFile z7In = new SevenZFile(SourceFile)){
SevenZArchiveEntry entry;
while ((entry = z7In.getNextEntry()) != null) {
if (!entry.isDirectory()) {
try(BufferedReader br = new BufferedReader(new InputStreamReader(z7In.getInputStream(entry), Charset.forName("GBK"))))
{
String aLine;
while((aLine=br.readLine()) != null) {
// do what ever with every Line.
}
}
}
}
}
使用github.com/bodgit/sevenzip
。
import (
...
"github.com/bodgit/sevenzip"
...
)
...
sevenReader, err := sevenzip.OpenReader(SourceFile)
if err != nil {
panic(err)
}
defer func(sevenReader *sevenzip.ReadCloser) {
err := sevenReader.Close()
if err != nil {
panic(err)
}
}(sevenReader)
for _, f := range sevenReader.File {
if !f.FileInfo().IsDir() {
inFile, err := f.Open()
if err != nil {
panic(err)
}
OneReader := bufio.NewReader(inFile)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}
}
}
是Linux/Unix下古老的压缩格式,扩展名.z
。
单个压缩包内只能包含单个文件。
需要pip install unlzw3
。
import unlzw3
...
unCompress = BytesIO(unlzw3.unlzw(Path(fileNameFull).read_bytes()))
lines=unCompress.readlines()
# do what ever with lines.
用org.apache.commons.compress
。
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-compressartifactId>
<version>1.24.0version>
dependency>
import org.apache.commons.compress.compressors.z.ZCompressorInputStream;
...
try(BufferedReader br = new BufferedReader(new InputStreamReader(new ZCompressorInputStream(Files.newInputStream(Paths.get(SourceFile))), Charset.forName("GBK"))))
{
String aLine;
while((aLine=br.readLine()) != null) {
// do what ever with every Line.
}
}
用github.com/hotei/dcompress
。
import (
...
"github.com/hotei/dcompress"
...
)
...
fi, err := os.Open(filepath.Join(SourcePath, SourceFile))
if err != nil {
panic(err)
}
defer fi.Close()
dcompress.Verbose = true
fz, err := dcompress.NewReader(OneIn)
if err != nil {
panic(err)
}
OneReader := bufio.NewReader(fz)
for {
line, _, err := OneReader.ReadLine()
if err == io.EOF {
MyEof = true
break
}
if err != nil {
panic(err)
}
// do what ever with every Line.
}