yotiky Tech Blog

とあるエンジニアの備忘録

C# - ZIP を操作する

目次

検証環境

  • .NET Core 3.1
  • LINQ Pad 6

ZIP を操作する

.NET Core で ZIP を操作するには、ZipFileZipArchiveZipArchiveEntry などを使う。

ZipFile はパスを必要としディスクへの読み書きに使う。

ZipArchive は初期化に Stream を必要とするため、メモリ上で操作するのに向いている。

ZipArchiveEntry は ZIP に含まれるファイルもしくは空のフォルダの情報を表す。

以降のサンプルコードでは次の変数を使用する。 また実行および出力結果は LINQPad で行っている。

// 圧縮する対象フォルダ
var targetDir = @"C:\ZipTest\ParentDir";
// 圧縮先
var zipPath = @"C:\ZipTest\test.zip";
// 展開先
var extractPath = @"C:\ZipTest\test";

targetDir 配下の構成は以下の通りになっている。

f:id:yotiky:20210309044430p:plain

圧縮する

ZipFile.CreateFromDirectory(targetDir, zipPath);

引数でエンコードを指定しない場合、 フォルダ名およびファイル名が ASCII 文字のみで構成されているとシステムのデフォルトコードページが使用される。 ASCII 文字以外を含む場合は、UTF-8 が使用される。

docs.microsoft.com

展開する

ZipFile.ExtractToDirectory(zipPath, extractPath);

引数でエンコードを指定した場合も指定しない場合も、ファイルヘッダーにフラグが設定されている場合、UTF-8 を使用してデコードされる。

ファイルヘッダーにフラグが設定されていない場合は、引数でエンコードを指定すると指定したエンコードでデコードされる。エンコードを指定しないとシステムのデフォルトコードページが使用される。

docs.microsoft.com

一般的に Windows では Shift-JISLinuxMac OS などでは UTF-8 が使用されるため、外部のツールなどで圧縮を行った場合は注意が必要である。

ヘッダーを直接読めれば UTF-8 かそれ以外かの判定ができる?かも?)

ZIP 内のファイルを列挙する

using (var archive = ZipFile.OpenRead(zipPath))
{
    foreach (var entry in archive.Entries)
    {
        entry.FullName.Dump();
    }
}

出力結果は次の通り。

f:id:yotiky:20210309044826p:plain

空のフォルダはフォルダ単体で ZipArchive.Entries プロパティに含まれるが、ファイルを配下に持つフォルダはフォルダ単体では含まれない。フォルダは末尾に / が付く。

一部だけ展開する

using (var archive = ZipFile.OpenRead(zipPath))
{
    var fileEntry = archive.GetEntry("SubDir/SubDirMemo1.txt");
    fileEntry.ExtractToFile(Path.Combine(extractPath, "SubDirMemo1_copy.txt"), true);

    var dirEntry = archive.GetEntry("EmptyDir/");
    dirEntry.ExtractToFile(Path.Combine(extractPath, "EmptyDir_copy"), true);
}

ファイルを持つフォルダはエントリーの取得ができないため、フォルダを指定して展開することはできない。また、空のフォルダのエントリーは取得できてもファイルではないので展開できない。空のファイルとして書き出される。

f:id:yotiky:20210309072204p:plain

ファイルを削除する

using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
{
    var fileEntry = archive.GetEntry("SubDir/SubDirMemo2.txt");
    fileEntry.Delete();
}

f:id:yotiky:20210309072556p:plain f:id:yotiky:20210309072452p:plain

ファイルを追加する

using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
{
    archive.CreateEntryFromFile(Path.Combine(targetDir, "Memo1.txt"), "AddedDir/Memo1_copy.txt");
}

f:id:yotiky:20210309072556p:plain f:id:yotiky:20210309072813p:plain

展開せずにテキストファイルの内容を取得する

using (var archive = ZipFile.OpenRead(zipPath))
{
    var fileEntry = archive.GetEntry("Memo1.txt");
    using (var reader = new StreamReader(fileEntry.Open()))
    {
        reader.ReadToEnd().Dump();
    }
}

サンプルコード全文

// 圧縮する対象フォルダ
var targetDir = @"C:\ZipTest\ParentDir";
// 圧縮先
var zipPath = @"C:\ZipTest\test.zip";
// 展開先
var extractPath = @"C:\ZipTest\test";

if (File.Exists(zipPath))
    File.Delete(zipPath);
if (Directory.Exists(extractPath))
    Directory.Delete(extractPath, true);

// 圧縮
ZipFile.CreateFromDirectory(targetDir, zipPath);
// 展開
ZipFile.ExtractToDirectory(zipPath, extractPath);

// ファイルの列挙
using (var archive = ZipFile.OpenRead(zipPath))
{
    foreach (var entry in archive.Entries)
    {
        // 空のフォルダは含まれるが、ファイルを持つフォルダはフォルダ単体としては含まれない
        entry.FullName.Dump();
    }
}

// 一部だけ展開する
using (var archive = ZipFile.OpenRead(zipPath))
{
    var fileEntry = archive.GetEntry("SubDir/SubDirMemo1.txt");
    fileEntry.ExtractToFile(Path.Combine(extractPath, "SubDirMemo1_copy.txt"), true);
    // 空のフォルダは取得できてもファイルではないので展開できない(空のファイルとして書き出される)
    var dirEntry = archive.GetEntry("EmptyDir/");
    dirEntry.ExtractToFile(Path.Combine(extractPath, "EmptyDir_copy"), true);
}

using (var archive = ZipFile.OpenRead(zipPath))
{
    archive.Entries.Select(x => x.FullName).Dump("元のZIP");
}
    
// ファイルを削除する
using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
{
    var fileEntry = archive.GetEntry("SubDir/SubDirMemo2.txt");
    //fileEntry.Delete();

    archive.Entries.Select(x => x.FullName).Dump("削除後のZIP");
}

// ファイルを追加する
using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
{
    archive.CreateEntryFromFile(Path.Combine(targetDir, "Memo1.txt"), "AddedDir/Memo1_copy.txt");
    archive.Entries.Select(x => x.FullName).Dump("追加後のZIP");
}

// 展開せずにテキストファイルの内容を取得する
using (var archive = ZipFile.OpenRead(zipPath))
{
    var fileEntry = archive.GetEntry("Memo1.txt");
    using (var reader = new StreamReader(fileEntry.Open()))
    {
        reader.ReadToEnd().Dump();
    }
}

ZipArchive

Form に ZIP ファイルを添付して送られてきた場合や、Azure Blob Storage から ZIP ファイルをメモリにダウンロードした場合など、ZIP ファイルのデータを Stream として取得できる場合は ZipAcrhive を直接使う。

using (var stream = form.Files[0].OpenReadStream())
using (var zip = new ZipArchive(stream, ZipArchiveMode.Read, false, Encoding.GetEncoding("Shift_JIS")))
{
}
using (var stream = new MemoryStream())
{
    await blobClient.DownloadToAsync(stream);
    using (var zip = new ZipArchive(stream, ZipArchiveMode.Read, true, Encoding.GetEncoding("Shift_JIS")))
    {
    }
}

第3引数の leaveOpen は、ZipArchiveDispose した後も Stream を開いたままにするかどうかのフラグ。

参考