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

C sharp 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_sourcea.txtb.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#

ExtractToDirectoryoverwriteFiles を 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 を使い、CreateEntryCopyTo の組み合わせで書き込む。
解凍は ZipFile.ExtractToDirectory で一括、ZipArchive で中身を見ながら部分展開もできる。
ZIP 内のパス(エントリ名)をどうするか(ルート構造、相対パス)は、相手の期待する構造から逆算して決める。
例外や失敗ケースを想定し、「どの ZIP をどう処理しようとして失敗したか」をログに残す。

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

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