もっと詳しく

先日、Zip圧縮された大量のファイルから、特定のフォルダに格納されているファイルのみ解凍する必要に迫られました。

そういえば、今までも大量のZipファイルを解凍したことが何度かあったような・・・

そこで、これを機に .NET 標準機能だけで Zipファイルの解凍、圧縮を行うクラスを作ったので、紹介したいと思います。

もちろん、指定した条件に合致するファイルのみ解凍することも可能です。

興味のある方は、是非ご一読ください。

ZipFile、ZipArchiveクラスについて

.NET Framework 4.5 以上と、.NetCore から、標準で Zipファイルを扱うためのクラスである、ZipFile、ZipArchive が用意されました。

下図は、この記事で登場するクラスの関係を表した図です。

ZipFile クラスは、ZipFileをオープンするためのクラスです。

単にフォルダを丸ごと圧縮するとか、Zipファイルを単純に解凍するだけなら、このクラスだけで事が足ります。

ZipArchiveクラスは、指定したファイルを書庫に追加する時に使います。

ZipArchiveEntryクラスは、書庫内のエントリ情報(ファイル名、書庫内のフォルダ階層、圧縮サイズ、最終更新日時など)を保持していますが、この情報を使って任意のファイルの解凍や、書庫内のエントリからの削除が行えます。

エントリについて

エントリとは、書庫内に格納されたファイルパスのことです。

ルートからファイルに到達するまでのフォルダ名、ファイル名は、スラッシュ( ‘/’ )で区切るところが、通常のファイルパスとの違いです。

例えば下図を例にすると、Window1.xaml.cs を指定したい場合、ルートが Deliverables となり、Source の下にWindow1.xaml.cs があるので、次のように記述します。

 Deliverables/Source/Window1.xaml.cs

同様に、Suites.Utils.dll を指定する場合は、次のようになります。

 Deliverables/Source/bin/Suites.Utils.dll

ZipFile、ZipArchiveクラスの使い方の具体例

ZipFile、ZipArchive クラスを使うには、あらかじめ プログラム先頭に下記の1行を追加しておいてください。

using System.IO.Compression;

これで準備は完了です。

それでは、一通りに使い方について簡単なソースコードを紹介していきます。

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

ZipFile.CreateFromDirectory(ソースディレクトリ,書庫のパス ,[圧縮レベル],[ベースディレクトリの追加]);

ZipFile.CreateFromDirectory(@"d:\mylib",@"d:\mylib.zip");

圧縮レベルは Fastest、Optimal、SmallestSize、NoCompression の4種類が選べます。

省略した場合は、Optimal が選択されます。

速度優先で圧縮 CompressionLevel.Fastest
速度とサイズのバランスを取って圧縮 CompressionLevel.Optimal
サイズ優先で圧縮 CompressionLevel.SmallestSize
無圧縮 CompressionLevel.NoCompression 

ベースディレクトリの追加をtrue にした場合と false にした場合の違いは以下の通りです。

フォルダ丸ごと解凍

ZipFile.ExtractToDirectory(書庫のパス, 解凍先フォルダ[,上書きモード]);

ZipFile.ExtractToDirectory(@"d:\mylib.zip", @"d:\temp", true);

書庫内のエントリ(フォルダ、フォルダ名)を取り出す

ZipFile.OpenRead(書庫のパス) で書庫をオープン後、 Entries でエントリを取得

ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
foreach (ZipArchiveEntry entry in zip.Entries)
{
    Console.Write(entry.FullName); 
}

ZipArchiveEntry には、以下のプロパティが指定できます。

Name ファイル名
FullName エントリ
Length 元のサイズ
CompressedLength 圧縮サイズ
LastWriteTime 最終更新日時
ExternalAttributes OSおよびアプリケーション固有のファイル属性
Crc32 32ビット巡回冗長検査

例えば、tempフォルダの中に存在するフォルダのみ解凍したい場合を考えてみます。

通常は、ループの中でFullName を参照し、temp が含まれていれば、そのエントリを解凍するような処理を考えますが、temp の中に 空のフォルダ(今回の例では dummy)のエントリがあると、それも取得されてしまいます。

後述する ExtractToFile に空フォルダのエントリ渡すとエラーになってしまうので、空フォルダのエントリか否かを識別したくなります。

幸いなことに、空フォルダのエントリだと Name プロパティ は空文字になるため、Name != “” の条件を付け加えることで識別可能です。

#tempフォルダ配下のファイルを解凍するサンプル
ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
foreach (ZipArchiveEntry entry in zip.Entries)
{
    if(entry.Name != "" && entry.FullName.Contains("/temp/")
    {
        Console.Write(entry.FullName);
    } 
}

指定したファイルのみ解凍

ZipFile.OpenRead(書庫のパス) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntryのExtractToFile(出力ファイル名,[上書きモード]) で取り出します。

ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
entry.ExtractToFile(@"d:\Window1.xaml.cs",true);

書庫から指定したファイルを削除

ZipFile.Open(書庫のパス, ZipArchiveMode.Update) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntry の Delete() で削除します。

ZipArchive zip = ZipFile.Open(@"d:\mylib.zip", ZipArchiveMode.Update);
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
entry.Delete();

書庫にファイルを追加

ZipFile.Open(書庫のパス, ZipArchiveMode.Update) で書庫をオープン後、CreateEntryFromFile(追加したいファイル名,追加先のエントリ,[圧縮レベル]) でファイルを追加します。

ZipArchive zip = ZipFile.Open(@"d:\mylib.zip", ZipArchiveMode.Update);
zip.CreateEntryFromFile(@"d:\Window1.xaml.cs", "Deliverables/Source");

解凍せず指定したファイルの中身を取得

ZipFile.OpenRead(書庫のパス) で書庫をオープン後、GetEntry(エントリ) で対象ファイルを特定、ZipArchiveEntryのOpen()で生成した Stream を使って中身を取得します。

ZipArchive zip = ZipFile.OpenRead(@"d:\mylib.zip");
ZipArchiveEntry entry = zip.GetEntry("Deliverables/Source/Window1.xaml.cs");
StreamReader sr = new StreamReader(entry.Open(), Encoding.UTF8);
var str = sr.ReadToEnd();
ConsoleWrite(str);

自作クラスのソースコード

いままでの内容を元に、使いやすくしたのが下記のZipクラスになります。

リファレンス

指定フォルダの圧縮 void Compress(
 string folder,
 string zipFile,
bool isOverwrite = false,
CompressionLevel compressionLevel = CompressionLevel.Optimal
)
書庫の解凍 void Extract(
string zipFile,
string outputFolder = “”,
bool isOverwrite = false
)
書庫の解凍(条件指定) int Extract(
string zipFile,
string outputFolder,
Func func,
bool isOverwrite = false
)
書庫内のファイル名取得 string[] Files(
string zipFile,
string format = “{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}”
)
エントリ一覧取得 IEnumerable Entries(string zipFile)
指定ファイルの解凍 bool TakeOut(
string zipFile,
string entryName,
string outuptName = “”,
bool isOverwrite = false
)
指定エントリの削除 bool Delete(
string zipFile,
string entryName
)
書庫へのファイル追加 void Append(
string zipFile,
string fileName,
string entryPath = “”,
CompressionLevel compressionLevel = CompressionLevel.Optimal
)
解凍せず中身を参照 string Read(
string zipFile,
string entryPath,
string encoderName = “UTF-8”
)

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.IO.Compression;
using MathNet.Numerics.Distributions;
using System.DirectoryServices.ActiveDirectory;
using System.Printing;

namespace CommonClass
{
    public class Zip
    {

        /// <summary>
        /// フォルダの書庫を作成する。
        /// </summary>
        /// <param name="folder"></param>
        /// <param name="zipFile"></param>
        /// <param name="compressionLevel"></param>
        public void Compress(string folder, string zipFile, bool isOverwrite = false, CompressionLevel compressionLevel = CompressionLevel.Optimal)
        {
            if (isOverwrite && File.Exists(zipFile))
            {
                File.Delete(zipFile);
            }
            ZipFile.CreateFromDirectory(folder, zipFile, compressionLevel, true);
        }

        /// <summary>
        /// 書庫を解凍する
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="outputFolder"></param>
        /// <param name="isOverwrite"></param>
        public void Extract(string zipFile, string outputFolder = "", bool isOverwrite = false)
        {
            //outputFolder が指定されていない場合、書庫が存在する場所を出力フォルダに指定する
            outputFolder = (outputFolder == "") ? System.IO.Path.GetDirectoryName(zipFile)! : outputFolder;

            //解凍
            ZipFile.ExtractToDirectory(zipFile, outputFolder, isOverwrite);
        }

        /// <summary>
        /// 指定した条件に一致した場合のみ解凍し、戻り値として解凍したファイル数を返す
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="outputFolder"></param>
        /// <param name="func"></param>
        /// <param name="isOverwrite"></param>
        public int Extract(string zipFile, string outputFolder, Func<ZipArchiveEntry, bool> func, bool isOverwrite = false)
        {
            var cnt = 0;

            foreach (var entry in Entries(zipFile))
            {
                if (entry.Name != "" && func(entry))
                {
                    var dir = System.IO.Path.Combine(outputFolder, System.IO.Path.GetDirectoryName(entry.FullName)?.Replace("/", "\\"));
                    if (!Directory.Exists(dir))
                    {
                        Directory.CreateDirectory(dir);
                    }
                    //1ファイルのみ解凍
                    TakeOut(zipFile, entry.FullName, dir, isOverwrite: isOverwrite);
                    cnt++;
                }
            }
            return cnt;

        }

        /// <summary>
        /// 書庫内のファイル一覧を取得する。
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="format"></param>
        /// <returns></returns>
        public string[] Files(string zipFile, string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}")
        {
            List<string> list = new List<string>();

            using (ZipArchive zip = ZipFile.OpenRead(zipFile))
            {
                //書庫内のファイルとディレクトリを列挙
                foreach (ZipArchiveEntry entry in zip.Entries)
                {
                    //entry が ファイルでなければ読み飛ばす
                    if (entry.Name == "") continue;

                    //entry が ファイルならリストに追加
                    list.Add(string.Format(
                        format,
                        entry.Name,                 //ファイル名
                        entry.FullName,             //フルパス
                        entry.Length,               //サイズ               
                        entry.CompressedLength,     //圧縮サイズ
                        entry.LastWriteTime,        //最終更新日時
                        entry.ExternalAttributes,   //OSおよびアプリケーション固有のファイル属性
                        entry.Crc32                 //32ビット巡回冗長検査
                        ));
                }
                return list.ToArray();
            }
        }

        /// <summary>
        /// 書庫内のエントリ(ファイルとディレクトリ)の取得
        /// </summary>
        /// <param name="zipFile"></param>
        /// <returns></returns>
        public IEnumerable<ZipArchiveEntry> Entries(string zipFile)
        {
            using (ZipArchive zip = ZipFile.OpenRead(zipFile))
            {
                return zip.Entries;
            }
        }

        /// <summary>
        /// 書庫から指定したファイルのみ解凍する。
        /// entryName には書庫内のフォルダ階層上のフルパス(\文字で区切られたフォルダ+ファイル名)を指定する。
        /// outputFileを省略すると、書庫と同じ場所に、 entryName で指定したファイル名を使って出力する。
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="entryName"></param>
        /// <param name="outuptName"></param>
        /// <param name="isOverwrite"></param>
        /// <returns></returns>
        public bool TakeOut(string zipFile, string entryName, string outuptName = "", bool isOverwrite = false)
        {
            using (ZipArchive zip = ZipFile.OpenRead(zipFile))
            {
                //書庫内のエントリを取得
                ZipArchiveEntry? entry = zip.GetEntry(entryName.Replace("\\", "/"));

                //エントリが見つかれば解凍
                if (entry != null)
                {
                    //出力ファイル名が空白なら、書庫と同じフォルダにエントリ名で出力するよう outuptName を設定
                    if (outuptName == "")
                    {
                        outuptName = System.IO.Path.Combine(
                                        System.IO.Path.GetDirectoryName(zipFile),
                                        System.IO.Path.GetFileName(entryName)
                                     );
                    }
                    //出力先にフォルダ名が指定されたら、そのフォルダにエントリ名で出力するよう outuptName を設定
                    if (System.IO.Directory.Exists(outuptName))
                    {
                        outuptName = System.IO.Path.Combine(outuptName, System.IO.Path.GetFileName(entryName));
                    }

                    //書庫から指定されたエントリ名のファイルを解凍し、outputName に出力
                    entry.ExtractToFile(outuptName, isOverwrite);
                    return true;
                }

                return false;
            }
        }

        /// <summary>
        /// 書庫から指定したファイルを削除する。
        /// entryName には書庫内のフォルダ階層上のフルパス(\文字で区切られたフォルダ+ファイル名)を指定する。
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="entryName"></param>
        /// <returns></returns>
        public bool Delete(string zipFile, string entryName)
        {
            using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
            {
                //書庫内のエントリを取得
                ZipArchiveEntry? entry = zip.GetEntry(entryName);

                //エントリが見つかれば解凍
                if (entry != null)
                {
                    entry.Delete();
                    return true;
                }

                return false;
            }
        }

        /// <summary>
        /// 書庫にファイルを追加する
        /// entryPath には書庫内のフォルダ階層上のフォルダ(\文字で区切られたフォルダ)を指定する。
        /// entryPath を省略すると、書庫内のルート(階層の一番上)に、fileName で指定されたファイル名で追加される。
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="fileName"></param>
        /// <param name="entryPath"></param>
        /// <param name="compressionLevel"></param>
        public void Append(string zipFile, string fileName, string entryPath = "", CompressionLevel compressionLevel = CompressionLevel.Optimal)
        {
            using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
            {
                //書庫内のバスが指定されていなければファイル名からエントリを作成
                if (entryPath == "")
                {
                    var drive = System.IO.Path.GetPathRoot(fileName) ?? "";
                    entryPath = (drive == "") ? fileName.Replace("\\", "/") : fileName.Substring(drive.Length);
                }
                //書庫内のパスが指定されていれば、それにファイル名を付けてエントリを作成
                else
                {
                    entryPath = System.IO.Path.Combine(entryPath, System.IO.Path.GetFileName(fileName));
                }

                //書庫にファイルを追加(書庫が無ければ作成)
                zip.CreateEntryFromFile(fileName, entryPath, compressionLevel);
            }
        }

        /// <summary>
        /// 解凍せず指定したファイルの中身を取得する
        /// </summary>
        /// <param name="zipFile"></param>
        /// <param name="entryPath"></param>
        /// <param name="encoderName"></param>
        /// <returns></returns>
        public string Read(string zipFile, string entryPath, string encoderName = "UTF-8")
        {
            using (ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Update))
            {
                // エントリを取得する
                ZipArchiveEntry? entry = zip.GetEntry(entryPath.Replace("\\", "/"));

                if (entry != null)
                {
                    //エントリから内容を取得する
                    using (StreamReader sr = new StreamReader(entry.Open(), Encoding.GetEncoding(encoderName)))
                    {
                        return sr.ReadToEnd();
                    }
                }
            }
            return "";
        }
    }
}

まとめ

今回は必要に駆られて C#でZipファイルを扱うためのクラスを作成したので、ZipFile、ZipArchive の全体像、基本的な使い方、そしてこれらを使った自作の便利クラスの紹介を行いました。

フリーの圧縮解凍ツールがあるので、プログラムで圧縮解凍を行うケースは少ないかもしれませんが、圧縮するファイルがあちこにのフォルダにまたがっていたり、大量のZIPファイルから特定の条件を満たすファイルのみ解凍したいような場合は、手作業だと非常に面倒くさくなります。

そんな時、この記事を参考にして頂ければ幸いです。