C# Tips | ファイル・ディレクトリ操作:ファイル圧縮(ZIP)

C# C#
スポンサーリンク
スポンサーリンク

はじめに 「ZIP を扱える」と業務ツールの格が一段上がる

業務でよくあるシーンです。

複数ファイルを ZIP にまとめてメールで送りたい。
バックアップを ZIP で固めて日付ごとに保存したい。
外部システムから渡された ZIP を展開して中身を処理したい。

こういうときに、C# から ZIP をサクッと扱えると、一気に「業務ユーティリティ感」が増します。
しかも、.NET には標準で ZIP 用のクラスが用意されているので、実はそんなに難しくありません。

ここでは、プログラミング初心者向けに、

ZIP の基本(どのクラスを使うか)
フォルダを丸ごと ZIP にする
特定ファイルだけを選んで ZIP にする
ZIP を展開する(解凍)
実務ユーティリティとしての設計ポイント

を、例題付きでかみ砕いて説明していきます。


C# で ZIP を扱うための基本クラス

System.IO.Compression と ZipFile / ZipArchive

C#(.NET)で ZIP を扱うときに使う名前空間は、System.IO.Compression です。
ここに、ZIP 圧縮・解凍のためのクラスが用意されています。

一番よく使うのは ZipFile クラスです。
これは「フォルダを ZIP にする」「ZIP をフォルダに展開する」といった、
“ざっくり操作”を簡単にやるためのユーティリティ的なクラスです。

もう少し細かく制御したいときは、ZipArchive クラスを使います。
こちらは「ZIP の中のエントリ(ファイル)を 1 つずつ追加・削除・読み書きする」ためのクラスです。

まずは ZipFile から入り、必要になったら ZipArchive に進む、という順番が分かりやすいです。


フォルダを丸ごと ZIP に圧縮する

最も簡単な「フォルダ → ZIP」圧縮

「このフォルダをそのまま ZIP にしたい」というケースはとても多いです。
その場合、ZipFile.CreateFromDirectory を使うと一発でできます。

事前に、参照に System.IO.Compression.FileSystem を追加しておく必要があります(.NET Framework の場合)。
.NET 6 以降などでは、通常の using だけで使えることが多いです。

using System;
using System.IO.Compression;

class Program
{
    static void Main()
    {
        string sourceDirectory = @"C:\work\backup_source";
        string zipPath = @"C:\work\backup.zip";

        ZipFile.CreateFromDirectory(
            sourceDirectory,
            zipPath,
            CompressionLevel.Optimal,
            includeBaseDirectory: false);

        Console.WriteLine("圧縮完了");
    }
}
C#

ここでの引数の意味をしっかり押さえておきましょう。

sourceDirectory
圧縮元のフォルダです。このフォルダの中身が ZIP に入ります。

zipPath
出力する ZIP ファイルのパスです。既に存在している場合は上書きされます。

CompressionLevel
圧縮レベルです。Optimal(そこそこ圧縮)、Fastest(速さ優先)、NoCompression(圧縮しない)などがあります。
業務では、よほどの理由がなければ Optimal で問題ありません。

includeBaseDirectory
true にすると、「フォルダ自体」も ZIP の中に含めます。
false だと、「フォルダの中身だけ」が ZIP のルートに入ります。
どちらが正しいかは、相手が期待する構造によります。

例えば、C:\work\backup_source に a.txt と b.txt があるとします。

includeBaseDirectory: false の場合、ZIP の中身はこうなります。

a.txt
b.txt

includeBaseDirectory: true の場合はこうです。

backup_source\a.txt
backup_source\b.txt

外部システムに渡す ZIP の場合、「ルートに何があるべきか」を仕様で確認しておくのが大事です。


特定のファイルだけを選んで ZIP にする(ZipArchive)

「フォルダ全部」ではなく「選んだファイルだけ」圧縮したい

ZipFile.CreateFromDirectory は便利ですが、「この拡張子だけ」「このリストにあるファイルだけ」といった柔軟な指定はできません。
そういうときは、ZipArchive を使って、自分でエントリを追加していきます。

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;

public static class ZipUtil
{
    public static void CreateZipFromFiles(
        string zipPath,
        IEnumerable<string> filePaths,
        string? baseDirectory = null)
    {
        string? dir = Path.GetDirectoryName(zipPath);
        if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using var zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None);
        using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create);

        foreach (var filePath in filePaths)
        {
            if (!File.Exists(filePath))
            {
                continue;
            }

            string entryName = GetEntryName(filePath, baseDirectory);

            var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);

            using var entryStream = entry.Open();
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);

            fileStream.CopyTo(entryStream);
        }
    }

    private static string GetEntryName(string filePath, string? baseDirectory)
    {
        if (string.IsNullOrEmpty(baseDirectory))
        {
            return Path.GetFileName(filePath);
        }

        string fullBase = Path.GetFullPath(baseDirectory);
        string fullPath = Path.GetFullPath(filePath);

        if (fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase))
        {
            string relative = fullPath.Substring(fullBase.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            return relative.Replace(Path.DirectorySeparatorChar, '/');
        }

        return Path.GetFileName(filePath);
    }
}
C#

使い方の例です。

var files = Directory.GetFiles(
    @"C:\work\data",
    "*.csv",
    SearchOption.TopDirectoryOnly);

ZipUtil.CreateZipFromFiles(
    @"C:\work\csv_only.zip",
    files,
    baseDirectory: @"C:\work");
C#

ここでの重要ポイントを整理します。

ZIP の中の「パス(エントリ名)」を自分で決めている
CreateEntry(entryName, ...) の entryName が、ZIP 内でのファイルパスになります。
baseDirectory を指定しておくと、「そのフォルダからの相対パス」で ZIP に入れられます。

FileStream.CopyTo でストリーム同士をコピーしている
fileStream(元ファイル)から entryStream(ZIP 内のエントリ)へ、
CopyTo で一気にコピーしています。
これが「ファイルを ZIP に書き込む」基本パターンです。

存在しないファイルはスキップしている
業務ユーティリティでは、「リストにあるけど実際には無いファイル」が混ざることもあります。
その場合にどうするか(例外にするか、スキップするか)は設計ポイントです。
ここではシンプルにスキップしています。


ZIP を展開する(解凍)

フォルダに丸ごと展開する

ZIP を解凍するのも、ZipFile を使うと簡単です。

using System;
using System.IO;
using System.IO.Compression;

class Program
{
    static void Main()
    {
        string zipPath = @"C:\work\backup.zip";
        string extractDirectory = @"C:\work\restore";

        if (!Directory.Exists(extractDirectory))
        {
            Directory.CreateDirectory(extractDirectory);
        }

        ZipFile.ExtractToDirectory(
            zipPath,
            extractDirectory,
            overwriteFiles: true);

        Console.WriteLine("展開完了");
    }
}
C#

ExtractToDirectory の overwriteFiles を true にすると、
展開先に同名ファイルがあっても上書きします。
false にすると、既に存在するファイルがある場合に例外が出ます。

業務で使うときは、「既存ファイルを上書きしてよいか」を仕様で決めておく必要があります。

ZIP の中身を見ながら、必要なファイルだけ展開する

「ZIP の中にたくさんファイルがあるけれど、そのうち一部だけ使いたい」というケースもあります。
その場合は、ZipArchive を使って中身を列挙し、条件に合うものだけ取り出します。

using System;
using System.IO;
using System.IO.Compression;

public static class ZipExtractUtil
{
    public static void ExtractCsvOnly(string zipPath, string extractDirectory)
    {
        Directory.CreateDirectory(extractDirectory);

        using var zipStream = new FileStream(zipPath, FileMode.Open, FileAccess.Read, FileShare.Read);
        using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);

        foreach (var entry in archive.Entries)
        {
            if (!entry.FullName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            string destinationPath = Path.Combine(extractDirectory, entry.FullName);

            string? destDir = Path.GetDirectoryName(destinationPath);
            if (!string.IsNullOrEmpty(destDir) && !Directory.Exists(destDir))
            {
                Directory.CreateDirectory(destDir);
            }

            using var entryStream = entry.Open();
            using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None);

            entryStream.CopyTo(fileStream);
        }
    }
}
C#

使い方です。

ZipExtractUtil.ExtractCsvOnly(
    @"C:\work\mixed.zip",
    @"C:\work\csv_out");
C#

ここでのポイントは、「ZIP 内のパス(entry.FullName)をそのまま展開先のパスに反映している」ことです。
サブフォルダ構造もそのまま再現されます。


実務ユーティリティとしての設計ポイント

圧縮・解凍の「単位」を決める

業務で ZIP を扱うときは、「何を 1 単位として圧縮するか」を最初に決めておくと設計が楽になります。

例えば、次のような単位があります。

1 日分のログを 1 ZIP にする(logs_2025-01-28.zip)
1 件の取引データを構成する複数ファイルを 1 ZIP にする
バックアップ対象フォルダ全体を 1 ZIP にする

この「単位」が決まると、ファイル名のルールやフォルダ構成も自然に決まってきます。

エンコーディングとファイル名

ZIP 自体はバイナリ形式ですが、中に入っているファイルはテキストかもしれません。
圧縮・解凍のときに「中身のエンコーディング」は変わりません。
つまり、「中の CSV を Shift_JIS で書いたら、解凍しても Shift_JIS のまま」です。

一方で、「ZIP 内のファイル名の文字コード」が絡む問題もありますが、
.NET の ZipArchive / ZipFile を使っている限り、通常はあまり意識しなくて大丈夫です。
ただし、他言語・他環境との連携では、「日本語ファイル名が文字化けする」問題が起きることもあるので、
可能なら英数字だけのファイル名にしておくと安全です。

例外処理とログ

圧縮・解凍は、ファイル I/O の塊です。
次のような理由で例外が出ることがあります。

ZIP ファイルが壊れている
展開先に書き込み権限がない
ディスク容量が足りない

業務ユーティリティとしては、「どの ZIP で」「どのファイルで」「何が起きたか」をログに残しておくと、
運用時のトラブルシュートがかなり楽になります。


まとめ 「ZIP ユーティリティ」は“ファイルをまとめて扱う”ための基本ツール

ZIP 圧縮・解凍は、業務システムと外部世界をつなぐときの定番フォーマットです。
C# では、標準ライブラリだけでかなり実用的なことができます。

押さえておきたいポイントを整理すると、こうなります。

フォルダ丸ごとなら ZipFile.CreateFromDirectory が一番簡単。
個別ファイルを選んで圧縮したいときは ZipArchive を使い、CreateEntry と CopyTo の組み合わせで書き込む。
解凍は ZipFile.ExtractToDirectory で一括、ZipArchive で中身を見ながら部分展開もできる。
ZIP 内のパス(エントリ名)をどうするか(ルート構造、相対パス)は、相手の期待する構造から逆算して決める。
例外や失敗ケースを想定し、「どの ZIP をどう処理しようとして失敗したか」をログに残す。

ここまで理解できれば、「ファイルを ZIP で固めてやり取りする」という業務の定番パターンを、
自分の C# ユーティリティでしっかり支えられるようになります。

タイトルとURLをコピーしました