Monday, February 24, 2014

Используем SharpCompress для работы с ZIP-архивами

Здравствуйте, уважаемые читатели моего блога. В этой статье поговорим об еще одной библиотеке для архивирования – SharpCompress. На момент написания статьи была доступна версия для скачивания 0.10.3 бета. Небольшое предисловие, почему я решил описать данную библиотеку в своем блоге. Где-то полгода назад меня перевели на проект, где использовалась SharpCompress, причем версии 0.8- (к сожалению, не помню номер билда, поэтому поставлю прочерк, так как это не играет особой роли). Эта библиотека была тогда в альфа-версии. Это говорит о том, что библиотека могла измениться в любую минуту. Но это не смутило архитектора данного творения, и он решил добавить эту библиотеку в проект. Дело в том, что в альфа-версии не было множества необходимого функционала  в библиотеке, поэтому, кроме нее, использовался класс для работы с архивами с DevExpress. Какую оценку дать решению такого горе-архитектора, зависит только от Вашей фантазии. А поскольку мне с ребятами по команде пришлось много времени "колдовать" с этой библиотекой, я решил поделится своими наблюдениями с Вами. После соответствующих выводов Вам решать, использовать ли эту библиотеку в своих проектах.
Библиотека популярна и пользуется спросом, о чем говорит количество скачиваний, которых на момент написания статьи было чуть больше 6 тысяч. Неплохо, как для бета-версии. Для того чтобы протестировать эту библиотеку, мы, как и для предыдущих статей о библиотеках для архивирования файлов на языке C#, создадим класс-обертку, через который будем выполнять всю необходимую логику. Класс назван SharpCompressHelper, и его реализация приведена ниже.
public class SharpCompressHelper
{
       public void Compress(string fileName, string sourceDirectory,
             ArchiveType type = ArchiveType.Zip,
             CompressionType compressionType = CompressionType.Deflate)
       {
             using (Stream stream = File.OpenWrite(fileName))
             {
                    using (var writer = WriterFactory.Open(stream, type, compressionType))
                    {
                           writer.WriteAll(sourceDirectory, "*", SearchOption.AllDirectories);
                    }
             }
       }

       public void CompressWithZipArchive(string fileName, string sourceDirectory,
        CompressionType compressionType = CompressionType.Deflate,
        CompressionLevel compressionLevel = CompressionLevel.Default)
       {
             using (var archive = ZipArchive.Create())
             {
            archive.DeflateCompressionLevel = compressionLevel;
                    archive.AddAllFromDirectory(sourceDirectory);
                    archive.SaveTo(fileName, compressionType);
             }     
       }

       public void UnzipWithZipArchive(string fileName, string sourceDirectory)
       {
             using (var archive = ZipArchive.Open(fileName))
             {
                    foreach (var entry in archive.Entries.Where(x => !x.IsDirectory))
                    {
                           entry.WriteToDirectory(sourceDirectory, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite);
                    }
             }
       }

       public void UnzipReaderFactory(string fileName, string stDestPath)
       {
             using (var stream = File.OpenRead(fileName))
             using (var reader = ReaderFactory.Open(stream))
             {
                    while (reader.MoveToNextEntry())
                    {
                           if (!reader.Entry.IsDirectory)
                           {
                                  reader.WriteEntryToDirectory(stDestPath, ExtractOptions.ExtractFullPath | ExtractOptions.Overwrite);
                           }
                    }
             }
       }

    public void Unzip(string stZipPath, string stDestPath)
    {
        using (var archive = ArchiveFactory.Open(stZipPath))
        {
            const ExtractOptions extractOptions = ExtractOptions.ExtractFullPath |
                                                    ExtractOptions.Overwrite;

            var entries = archive.Entries.Where(e => !e.IsDirectory).Select(s => s);
            foreach (var arcEntry in entries)
                arcEntry.WriteToDirectory(stDestPath, extractOptions);
        }
    }

    public void UnzipSevenZipArchive(string stZipPath, string stDestPath)
    {
        using (var archive = SevenZipArchive.Open(stZipPath))
        {
            var reader = archive.ExtractAllEntries();
            while (reader.MoveToNextEntry())
            {
                if(!reader.Entry.IsDirectory)
                {
                    var fileName = Path.Combine(stDestPath,Path.GetFileName(reader.Entry.FilePath));
                    var stream = new MemoryStream();
                    reader.WriteEntryTo(stream);
                    File.WriteAllBytes(fileName, stream.ToArray());
                }
            }
        }
    }
}
Класс для измерения тестов приведен ниже.
public static class Profiler
{
    public static double MeasureAction(Action action)
    {
        var st = new Stopwatch();
           
        st.Start();

        action();

        st.Stop();

        return st.Elapsed.TotalMilliseconds;
    }
}
Исходный код использования библиотеки Ionic.Zip приведен ниже. Но в отличие от первых своих статей об использовании популярных библиотек для работы с архивами, как SharpZipLib и DotNetZip, работу с библиотекой SharpCompress я опишу подробно, так как полагаю, некоторыми моментами с этой библиотеки Вы будете удивлены. Для начала приведем код способов архивирования с этой библиотекой, которые Вы можете использовать.
var directory = CreateEmptyDirectory();
var zipHelper = new SharpCompressHelper();
var resultFile = Path.Combine(Directory.GetCurrentDirectory(), "result.txt");
if (File.Exists(resultFile))
    File.Delete(resultFile);

var sourceDirecory = @"D:\CSharp\IocContainersDemo";
//var sourceDirecory = @"D:\books\Introduction to F#";
var size = GetDirectorySize(sourceDirecory);
File.AppendAllText(resultFile, string.Format("Directory size {0} bytes {1}", size, Environment.NewLine));

const string sharpCommpressPacking1 = "SharpCompressPacking1.zip";
const string sharpCommpressPacking2 = "SharpCompressPacking2.zip";

var fileName = Path.Combine(directory, sharpCommpressPacking1);
var result = Profiler.MeasureAction(() => zipHelper.Compress(fileName, sourceDirecory));
File.AppendAllText(resultFile, string.Format("Zip directory with WriterFactory {0} msec, Size: {1}{2}",
    result,
    new FileInfo(fileName).Length,
    Environment.NewLine));

var fileName1 = Path.Combine(directory, sharpCommpressPacking2);
var result1 = Profiler.MeasureAction(() => zipHelper.CompressWithZipArchive(fileName1, sourceDirecory));
File.AppendAllText(resultFile, string.Format("Zip directory with ZipArchive {0} msec, Size: {1}{2}",
    result1,
    new FileInfo(fileName1).Length,
    Environment.NewLine));
Код должен быть несложным. Сначала создаем пустую директорию для тестов. Пожалуй, приведу вспомогательные функции, которые используются для приведенного выше примера.
private static void DeleteTempDirectory()
        {
            var directory = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
            if (Directory.Exists(directory))
                Directory.Delete(directory, true);
        }

        private static string CreateEmptyDirectory()
        {
            var directory = Path.Combine(Directory.GetCurrentDirectory(), "TestArchives");
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);
            else
            {
                var files = Directory.GetFiles(directory);
                foreach (var file in files)
                {
                    File.Delete(file);
                }
            }
            return directory;
        }

        static long GetDirectorySize(string path)
        {
            var a = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);

            return a.Select(name => new FileInfo(name)).Select(info => info.Length).Sum();
        }  
Задача этих функций - это создание пустой директории для теста, если такой нет, а если есть то почистить все от старых данных, а также удаление созданной папки и подсчет места на диске (размер папки). Вот те две функции, которые тестировались для создания архива с некой выбранной папки.
public void Compress(string fileName, string sourceDirectory,
       ArchiveType type = ArchiveType.Zip,
       CompressionType compressionType = CompressionType.Deflate)
{
       using (Stream stream = File.OpenWrite(fileName))
       {
             using (var writer = WriterFactory.Open(stream, type, compressionType))
             {
                    writer.WriteAll(sourceDirectory, "*", SearchOption.AllDirectories);
             }
       }
}

public void CompressWithZipArchive(string fileName, string sourceDirectory,
    CompressionType compressionType = CompressionType.Deflate,
    CompressionLevel compressionLevel = CompressionLevel.Default)
{
       using (var archive = ZipArchive.Create())
       {
        archive.DeflateCompressionLevel = compressionLevel;
             archive.AddAllFromDirectory(sourceDirectory);
             archive.SaveTo(fileName, compressionType);
       }     
}
Рассмотрим производительность этих функций для двух разных папок, одна из которых имеет размер 38087531 байта, а вторая, соответственно, - 463441844 байт. Замер архивирования этих двух папок приведен ниже.
Интересное замечание: если Вы захотите проверить в дебаг-режиме, то результат от истинного может отличаться в почти в два раза. Также такой баг наблюдается, если среда CLR не закешировала данные: во время первого запуска Ваши данные могут отличаться. Для этого теста нужно собрать как минимум в релизе, не запускать со студии и брать средний результат после нескольких тестовых запусков. Тогда результаты будут давать более-менее реальную картину.
Данные для двух результатов практически не отличаются. Большая вероятность того, что два этих разных класса используют общий метод для сжатия.
Сейчас рассмотрим, какие степени сжатия доступны для данной библиотеки, и как это отражается на производительности. Для того чтобы это протестировать, я написал такой код:
var compressFiles = new List<Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>>(new[]
{
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test1.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.None),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test2.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.BestSpeed),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test3.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level2),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test4.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level3),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test5.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level4),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test6.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level5),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test7.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Default),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test8.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level7),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test9.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.Level8),
    new Tuple<string, SharpCompress.Compressor.Deflate.CompressionLevel>(Path.Combine(directory, "Test10.zip"), SharpCompress.Compressor.Deflate.CompressionLevel.BestCompression),
});

foreach (var compressFile in compressFiles)
{
    //Avoid closure variable
    var temp = compressFile;
    var res = Profiler.MeasureAction(() => zipHelper.CompressWithZipArchive(temp.Item1,
                                                                                    sourceDirecory,
                                                                                    compressionLevel: temp.Item2));
    File.AppendAllText(resultFile, string.Format("Zip directory with ZipArchive {0} msec, Size: {1}{2}",
        res,
        new FileInfo(temp.Item1).Length,
        Environment.NewLine));
}
После запуска приложения я получил вот такой результат.
Directory size 38087531 bytes
Zip directory with ZipArchive 3650,1355 msec, Size: 16590528
Zip directory with ZipArchive 3574,2207 msec, Size: 16590528
Zip directory with ZipArchive 3646,066 msec, Size: 16590528
Zip directory with ZipArchive 3584,6116 msec, Size: 16590528
Zip directory with ZipArchive 3574,7011 msec, Size: 16590528
Zip directory with ZipArchive 3589,4719 msec, Size: 16590528
Zip directory with ZipArchive 3589,8365 msec, Size: 16590528
Zip directory with ZipArchive 3613,0922 msec, Size: 16590528
Zip directory with ZipArchive 3582,0287 msec, Size: 16590528
Zip directory with ZipArchive 3593,8234 msec, Size: 16590528
Приведу код, который использовался для теста.
public void CompressWithZipArchive(string fileName, string sourceDirectory,
    CompressionType compressionType = CompressionType.Deflate,
    CompressionLevel compressionLevel = CompressionLevel.Default)
{
       using (var archive = ZipArchive.Create())
       {
        archive.DeflateCompressionLevel = compressionLevel;
             archive.AddAllFromDirectory(sourceDirectory);
             archive.SaveTo(fileName, compressionType);
       }     
}
Получается, что этот код не работает. Как переписать функцию, чтобы учитывалась степень сжатия, я так и не нашел. Буду очень благодарен, если подскажете, где я ошибся. Это сразу огромный минус для данной библиотеки. Вот функция, которая использовалась для теста:
private static void UnzipFromSevnZipArchive(string directory, SharpCompressHelper zipHelper, string fileName,
                                            string resultFile)
{
    var outputDirectory1 = Path.Combine(directory, "SevenZip1");
    if (!Directory.Exists(outputDirectory1))
        Directory.CreateDirectory(outputDirectory1);
    Directory.CreateDirectory(outputDirectory1);
    try
    {
        var outputResult = Profiler.MeasureAction(() => zipHelper.Unzip(fileName, outputDirectory1));
        File.AppendAllText(resultFile, string.Format("Unzip file to directory (ArchiveFactory) {0} msec, FileName: {1}{2}",
                                                        outputResult,
                                                        fileName,
                                                        Environment.NewLine));
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
        Console.ReadLine();
    }
    var outputDirectory2 = Path.Combine(directory, "SevenZip2");
    if (!Directory.Exists(outputDirectory2))
        Directory.CreateDirectory(outputDirectory2);
    try
    {
        var outputResult = Profiler.MeasureAction(() => zipHelper.UnzipSevenZipArchive(fileName, outputDirectory2));
        File.AppendAllText(resultFile, string.Format("Unzip file to directory (SevenZipArchive) {0} msec, FileName: {1}{2}",
                                                        outputResult,
                                                        fileName,
                                                        Environment.NewLine));
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
        Console.ReadLine();
    }
}
Файл для теста был взят вначале 340418 байт, что равно 333 КБ. И посмотрите, какие тесты показал запуск.
Unzip file to directory (ArchiveFactory) 3223,2127 msec, FileName: D:\CSharp\CompositeOnlineLibrary.7z
Unzip file to directory (SevenZipArchive) 469,1841 msec, FileName: D:\CSharp\CompositeOnlineLibrary.7z
Разница просто не поддается логике. И чем больше размер, тем хуже. Например, первый вариант у меня постоянно падал на файле в 17 Мб. Дело в том, что подобный пример, как в первом тесте, использовался в промышленной эксплуатации. Вот сами функции:
public void Unzip(string stZipPath, string stDestPath)
{
    using (var archive = ArchiveFactory.Open(stZipPath))
    {
        const ExtractOptions extractOptions = ExtractOptions.ExtractFullPath |
                                                ExtractOptions.Overwrite;

        var entries = archive.Entries.Where(e => !e.IsDirectory).Select(s => s);
        foreach (var arcEntry in entries)
            arcEntry.WriteToDirectory(stDestPath, extractOptions);
    }
}

public void UnzipSevenZipArchive(string stZipPath, string stDestPath)
{
    using (var archive = SevenZipArchive.Open(stZipPath))
    {
        var reader = archive.ExtractAllEntries();
        while (reader.MoveToNextEntry())
        {
            if(!reader.Entry.IsDirectory)
            {
                var fileName = Path.Combine(stDestPath,Path.GetFileName(reader.Entry.FilePath));
                var stream = new MemoryStream();
                reader.WriteEntryTo(stream);
                File.WriteAllBytes(fileName, stream.ToArray());
            }
        }
    }
}
Если приведенные примеры не отбили у Вас желание использовать эту библиотеку, то, пожалуй, стоит упомянуть об еще двух важных ошибках, обнаруженных в данной библиотеке. Она покрывает работу с 7z только частично, при этом время от времени падает. Вторая ошибка находится в предыдущей версии библиотеки. Она не работала с архивами, если Вы в существующий архив добавили свой файл. Третье, что субъективно не импонирует в этой библиотеке, - то, что некоторые примеры реально не работают. Задумка неплохая, но на данном этапе у нее ужасная реализация. Если для использования библиотеки DotNetZip необходимы минимальные знания C#, то с этой библиотекой нужно быть гуру, чтобы понять, почему она падает там, где этого быть не должно. Если Вы используете данную библиотеку, мне остается Вам пожелать только удачи. Для личной же будущей практики у меня стало меньше одной библиотекой, которую можно использовать в языке C#.

2 comments:

  1. Лучше поздно чем никогда, но я отвечу на вопрос об отсутствии учёта степени сжатия. Всё дело в том, что у метода Deflate нет понятия уровень сжатия, поэтому и реализовать его невозможно.

    ReplyDelete
    Replies
    1. Спасибо большое за информацию. На момент написания статьи (еще в далеком 2014), я искал ответ на этот вопрос. Все источники твердили что:
      There are ten different compression levels for DEFLATE (0 no compression & fastest, 9 best compression & slowest).
      Поэтому так и описал в статье как это дошло до меня. Сама идея статьи была проверить топ библиотеки для роботы с архивами, так как на продакшене начались проблемы с производительностью. Вот и решил для себя накидать пару тестов для замера производительности той или иной либы, которые в конечном итоге сделал у виде набора статей. В любом случае спасибо за информацию.

      Delete