gzip圧縮ファイルのリンクを解除

gzip圧縮ファイルのリンクを解除

ある日、リモートサーバーからいくつかのログを収集していましたが、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に圧縮されます。これは、解凍後にすべての元のファイルを含む1つのファイルです。バイナリを印刷するのではなく、元の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ヘッダーやフッターには圧縮データストリームの長さが含まれていないため、ファイルのリンクを解除するのは簡単ではありません。これは、第2のストリームの先頭を確実に見つけるために、全体のストリームを解凍するのに必要なものの半分である収縮ストリーム全体を復号する必要があることを意味する。

私が知っている限り、データストリームを探索してそれが終わる場所を見つけることができるツールはありません。この分野のいくつかの研究では、gzipで圧縮されたファイルの内容への準ランダムアクセスをサポートしています。

IO::Uncompress::Gunzip幸いなことに、Stéphane Chazelasが述べたPerlなどのいくつかのプログラミングライブラリを使用して、gzipストリームの圧縮を独立して解くことができます。彼の答え、またはさびた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

偶然にも、inはファイルごとに1つずつ2つの独立した圧縮ストリームを生成し、gzip -c file1 file2 > resultファイルgzipのファイル名と変更時間まで保存します。

解凍時にはその情報を使用することはできませんが、perlモジュールIO::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圧縮ファイルのマジックバイトはそのまま残ります(16進数1F 8B

ほとんどのプログラムは機能する必要があり、一時ファイルを手動で作成するとこれを防ぐことspongeができます。moreutils

あなたは何をしましたか:

  1. 各連続ブロックに対して、ファイルに印刷可能な専用文字を含む行を挿入します。 2つの一般的なASCIIファイルを連続的にマージすると、ファイルは分割されず(この場合はログのタイムスタンプがファイルを分離するために使用されます)、元のファイル名が失われます。
  2. gz_only.gz中間ファイルに追加の行を入れる
  3. マジックバイトを使用したファイルの区切り

最後に、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が、慣れていません。

関連情報