有一天,我从远程服务器收集一些日志,并不假思索地将这些文件压缩到一个文件中,而不是将目录添加到 tarball 中。我可以手动分离出一些日志文件,但其中一些已经被压缩。所以原始文件看起来像:
ex_access.log
ex_access.log.1.gz
ex_access.log.2.gz
ex_debug.log
ex_debug.log.1.gz
ex_debug.log.2.gz
ex_update.log
ex_update.log.1.gz
ex_update.log.2.gz
并被压缩为 exlogs.gz,正如您所期望的,解压后,它是一个包含所有原始文件的文件。有没有办法将原始gz文件分离出来,以便可以正常解压而不是打印出二进制文件:
^_<8B>^H^H<9B>C<E8>a^@
^Cex_access.log.1^@<C4><FD><U+076E>-Kr<9D> <DE><F7>S<9C>^W<E8><CE><F0><FF><88>y[<D5><EA>+<A1>^EHuU<A8>^K<B6><94><AA>L4E^R̤^Z^B<EA><E1><DB>}<AE>̳<B6><D6>I<C6><F8><9C><DB><C6>
<F1>@G`<E6><D6><FE><E0>3<C2><C3>ٰ̆|<E4><FC><BB>#<FD><EE><B8>~9<EA>+<A7>W+<FF><FB><FF><F6><9F><FE><97><FF><E3><97><FF><FD>^Z<E3><FF><F8><E5><FF><FE><CB><C7><FF>Iy<FC>?<8E><F9>?<F3>?<EF><B5><F7><F9><BF><FF>ß<FF>
[etc]
是的,我可以再次收集日志(因为我确实有保留原始日志完整的感觉),但是获得访问服务器的批准是一件痛苦的事情,如果可能的话,我想避免它。
编辑:我使用的命令是
gzip -c ex_* > exlogs.gz
答案1
将文件 gzip 压缩为单个文件时,gzip
会创建一个包含多个 gzip 流的文件,就好像您首先单独压缩文件然后将它们连接起来一样。
此行为在手册页。
-c --stdout --to-stdout
将输出写入标准输出;保持原始文件不变。如果有多个输入文件,则输出由一系列独立压缩的成员组成。
这意味着每个源文件都有一个单独的 gzip 标头(其中包含原始文件名)。所以原则上它们可以在减压时分离。
不幸的是,gzip
开发人员选择不支持这一点gunzip
:
如果您希望创建包含多个成员的单个存档文件,以便以后可以独立提取成员,请使用 tar 或 zip 等存档程序。 [...] gzip 被设计为 tar 的补充,而不是替代品。
取消连接文件并非易事,因为 gzip 标头和页脚都不包含压缩数据流的长度。这意味着,为了可靠地找到第二个流的开始,您需要解码整个 deflate 数据流,这是解压缩整个数据流的一半。
据我所知,目前还没有工具只能浏览数据流以找出它的结束位置,即使有该领域的一些研究支持对 gzip 压缩文件内容的准随机访问。
幸运的是,一些编程库可以用来单独解压缩 gzip 流,例如 Perl 的IO::Uncompress::Gunzip
,正如 Stéphane Chazelas 中提到的他的回答,或铁锈的flate2
。
最后,作为解决方案,我编写了该工具枪拉链分裂。它单独解压缩每个文件,也可以解连接文件。对于后者,它会解压缩每个文件,记录 gzip 流开始的偏移量,同时丢弃结果。这可以进一步优化,但即使对于千兆字节大小的文件,工作速度也相当快。
$ ./gunzip-split --help
gunzip-split 0.1.1
Uncompress concatenated gzip files back into separate files.
USAGE:
gunzip-split [OPTIONS] <FILE>
ARGS:
<FILE> concatenated gzip input file
OPTIONS:
-d, --decompress Decompressing all files (default)
-f, --force Overwrite existing files
-h, --help Print help information
-l, --list-only List all contained files instead of decompressing
-o, --output-directory <DIRECTORY> Output directory for deconcatenated files
-s, --split-only Split into multiple .gz files instead of decompressing
-V, --version Print version information
$ ./gunzip-split -s -o ./out/ combined.gz
file_1: OK.
file_2: OK.
$ ls ./out
file_1.gz file_2.gz
答案2
碰巧, ingzip -c file1 file2 > result
确实gzip
创建了两个独立的压缩流,每个文件一个,甚至还存储文件的文件名和修改时间。
它不允许您在解压时使用该信息,但您可以使用perl
'sIO::Uncompress::Gunzip
模块来执行此操作。例如:
#! /usr/bin/perl
use IO::Uncompress::Gunzip;
$z = IO::Uncompress::Gunzip->new("-");
do {
$h = $z->getHeaderInfo() or die "can't get headerinfo";
open $out, ">", $h->{Name} or die "can't open $h->{Name} for writing";
print $out $buf while $z->read($buf) > 0;
close $out;
utime(undef, $h->{Time}, $h->{Name}) or warn "can't update $h->{Name}'s mtime";
} while $z->nextStream;
并调用该脚本作为,它将恢复当前工作目录中的that-script < exlogs.gz
文件及其原始名称和修改时间(不包括未存储的亚秒部分)。gzip
答案3
这有点复杂,但在满足以下要求时有效:
- 这
merged.gz
是清晰的 ASCII 数据和 gzip 压缩文件的混合体 - 它来自像这样的操作
cat log0 log1.gz log2.gz log3 log4.gz > merged.gz
- 明文 ASCII 文件中的行仅来自可打印字符
- gzip 压缩文件的魔术字节完好无损(以十六进制表示
1F 8B
)
大多数程序应该可用,sponge
可以moreutils
通过手动写入临时文件来避免。
做了什么:
- 将具有专用可打印字符的行放入每个连续块的文件中。请注意,如果您连续合并两个清晰的 ASCII 文件,这不会将它们分开(在这种情况下使用日志的时间戳来分隔文件)并且原始文件名会丢失
- 将其他行放入中间
gz_only.gz
文件中 - 使用魔术字节来分隔文件
最后一点使用csplit
,它只能在还有换行符的情况下进行分割 - 因此这是在分割之前引入并在分割之后删除的。目前假设合并系统中的 gzip 压缩文件不超过 1000 个。
#!/bin/bash
#lines with printable characters go to separate files for each consecutive block
awk '{ if ($0 ~ /^[[:print:]]+$/) { print > "file_"i+0}
else {if (oldi==i) {i++}}}' merged.gz
#get lines with non-printables to other merged file
grep -av '^[[:print:]]$' merged.gz > gz_only.gz
#split into files and remember their count
#sed introduces newline before magic bytes
#csplit splits on occurrence of magic bytes and returns info on file lengths
nfiles=$( sed "s/$(printf '\x1f\x8b')/\n&/g" gz_only.gz |
csplit - -z "/$(printf '\x1f\x8b')/" '{*}' -b'%03d.gz' |
wc -l )
#first file is empty, due to introduced newline
rm -fv xx000.gz
#for all other remove newline
#note: the above grep introduced a newline to the last file
#if splitting is done for a file only concatenated from
#gz-files (no previous grep), the last file would have to
#be excluded from this operation.
for (( i=1 ; i<nfiles ; i++ )) ; do
name=xx$(printf '%03d.gz' $i)
head -c -1 $name | sponge $name
done
#retrieve original file name
for f in xx*gz ; do
#this is ready for simple filenames like the suggested logs,
#e.g. no " as file name character
mv $f "$(file $f | awk -F'"' '{print $2}').gz"
done
#unzip files
find -name '*gz' ! -name gz_only.gz ! -name merged.gz -exec gunzip {} +
我有点觉得使用 ASCII 和非 ASCII 的分离以及分割可能会更优雅perl
,但我不熟悉。