[C#]シリアライズしたバイナリデータを圧縮解凍する方法

.Net Freamework 2.0から、System.IO.Compressionが追加されていて、DeflateStream(rfc1951), GZipStream(rfc1952)が使用できる。
シリアライズはBinaryFormatterを使えばいい。簡単に書こうとすると、以下のよーな感じになる。

/// <summary>
/// オブジェクトを圧縮して書き込み
/// </summary>
/// <param name="filePath"></param>
/// <param name="obj"></param>
public void WriteWithCompress(string filePath, object obj)
{
    using (Stream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
    {
        using (DeflateStream ds = new DeflateStream(stream, CompressionMode.Compress, true))
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.Serialize(ds, obj);
        }
    }
}

このソースを.Net Freamework 2.0(VisualStudio2005)で動作させてみると、出力サイズが大して圧縮されていないことに気づくと思う。ところが、.Net Freamework 3.0(VisualStudio2008)で動作させるとサイズは半分以下になる。
この違いは、恐らく2.0のバグが3.0で改善されたとか、そんなところじゃないかと思われる。根拠はないが。
じゃあ2.0では使い物にならないのかというと、そういうわけでもない。
MemoryStreamを使用して、逐一バッファに保持すればいい。

/// <summary>
/// オブジェクトを圧縮して書き込み
/// </summary>
/// <param name="filePath"></param>
/// <param name="obj"></param>
public void WriteWithCompress(string filePath, object obj)
{
    byte[] buffer;
    using (MemoryStream ms = new MemoryStream())
    {
        IFormatter formatter = new BinaryFormatter();
        formatter.Serialize(ms, obj);
        buffer = ms.ToArray();
    }
    using (MemoryStream ms = new MemoryStream())
    {
        using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Compress, true))
        {
            ds.Write(buffer, 0, buffer.Length);
        }
        buffer = ms.ToArray();
    }
    using (Stream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
    {
        stream.Write(buffer, 0, buffer.Length);
    }
}

これで2.0, 3.0で同様の結果が得られる。
読み込みについては、バージョン関係なく以下のように。

/// <summary>
/// 圧縮されたオブジェクトを解凍して読み込み
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public object ReadWithDecompress(string filePath)
{
    object obj;
    using (Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        using (DeflateStream ds = new DeflateStream(stream, CompressionMode.Decompress))
        {
            IFormatter formatter = new BinaryFormatter();
            obj = formatter.Deserialize(ds);
        }
    }
    return obj;
}

ちなみに、GZipStreamを使いたい場合は、DeflateStreamをGZipStreamに書き換えるだけでOKだ。

[C#]シリアライズしたオブジェクトをバイナリ形式で出力する方法

まずはシリアライズ。方法は、[Serializable]を付けるだけ。
例として、文字列をバイト配列で保持するシリアライズクラスを適当に書いてみた。

using System;
using System.Collections.Generic;
using System.Text;

namespace SandBox
{
    [Serializable]
    public class SerializedByteArray
    {
        private byte[] data;

        public SerializedByteArray(string str)
        {
            this.data = Encoding.Unicode.GetBytes(str);
        }

        public string Text
        {
            get { return Encoding.Unicode.GetString(data); }
        }
        /// <summary>
        /// 保持しているバイト情報を引数で指定した数だけ倍にする
        /// </summary>
        /// <param name="size"></param>
        public void Increase(int size)
        {
            string str = this.Text;
            StringBuilder temp = new StringBuilder(str);
            for (int i = 0; i < size; i++)
            {
                temp.Append(str);
            }
            this.data = Encoding.Unicode.GetBytes(temp.ToString());
        }
    }
}

さて、このクラスをバイナリ形式でファイル入出力してみよう。
実はBinaryFormatterを使って簡単に出力できる。
使うのは、Serialize()メソッド。

/// <summary>
/// オブジェクト書き込み
/// </summary>
/// <param name="filePath"></param>
/// <param name="obj"></param>
public void Write(string filePath, object obj)
{
    using (Stream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
    {
        IFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, obj);
    }
}

読み込みも同様に簡単に書ける。
こちらは、Deserialize()メソッドで取り込み。

/// <summary>
/// オブジェクト読み込み
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public object Read(string filePath)
{
    object obj;
    using (Stream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        IFormatter formatter = new BinaryFormatter();
        obj = formatter.Deserialize(stream);
    }
    return obj;
}

手軽。

[java]CsvBeanReader, CsvBeanWriter, ParseDateを併用するには

前回、Beanによる読み込みとMapによる書き込みを取り上げましたが、元サイトのサンプルソースに手を加えての紹介、という趣旨だったので、Beanによる書き込みについては割愛しました。
なので、今回はBeanによる書き込みに焦点を当ててみましょう。


配布サイト:http://supercsv.sourceforge.net/
前回記事:[java]CSVの読み書きを快適に〜「Super CSV」ノススメ - Undead mode 忘備録


早速ですが、Bean内容を出力するmain実装をしてみると、↓のような感じになります。
ファイル名:WritingObjects.java

package write;

import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.supercsv.io.CsvBeanWriter;
import org.supercsv.io.ICsvBeanWriter;
import org.supercsv.prefs.CsvPreference;

import bean.UserBean;

/**
 * SuperCSVによるCSVファイル書き込み(Bean)
 * @author kazuki.kido
 */
public class WritingObjects {
    public static void main(String[] args) throws Exception {
        // ヘッダ情報定義
        String[] header = new String[]{"username", "password", "date", "zip", "town"};
        List<UserBean> list = new ArrayList<UserBean>();
        // 1行目データを作成
        UserBean bean1 = new UserBean();
        bean1.setUsername("Klaus");
        bean1.setPassword("qwexyKiks");
        bean1.setDate(new Date());
        bean1.setZip(1111);
        bean1.setTown("Tokyo");
        list.add(bean1);
        // 2行目データを作成
        UserBean bean2 = new UserBean();
        bean2.setUsername("Oufu");
        bean2.setPassword("bobilop");
        bean2.setDate(new Date());
        bean2.setZip(4555);
        bean2.setTown("Tokyo");
        list.add(bean2);
        ICsvBeanWriter writer = new CsvBeanWriter(new FileWriter("./data/boo.csv"), CsvPreference.EXCEL_PREFERENCE);
        try {
            // ファイルへ出力
            writer.writeHeader(header);
            // エラーログ取得用バッファ
            StringBuilder errorLog = new StringBuilder();
            // ファイルへ書き出し
            for(UserBean bean : list){
                writer.write(bean, header, UserBean.processors, errorLog);
                if(errorLog.length() > 0){
                    System.err.println(errorLog);
                    break;
                }
            }
        } finally {
            writer.close();
        }
    }
}

UserBeanについては前回作成したものを流用します。が、実はこのままだとエラーが発生してしまいます。エラーが発生する原因は、UserBean.javaの以下の部分。

ファイル名:UserBean.java

    /** 各要素フォーマット定義 */
    public static final CellProcessor[] processors = new CellProcessor[] {
            new Unique(new StrMinMax(4, 20)),    // username
            new StrMinMax(7, 35),                // password
            new ParseDate("dd/MM/yyyy"),         // date
            new Optional(new ParseInt()),        // zip
            null                                 // town
    };

date要素の定義にParseDateを使用していますが、このクラスは「CSVから取得した文字列 ⇒ 引数のフォーマットとして解釈し、Date型に変換して保持する」ことを指示しています。
しかし、CsvBeanWrite.write(Object, String, CsvPreference, StringBuilder)で必要となるのは「Date型⇒引数のフォーマットで変換し、文字列として出力する」指示です。探してみても、この2つを満たすハイブリットなクラスは用意されていません。
ないんじゃ仕方ないですよね〜というわけで、作ってみました。

ファイル名:ParseDateEx.java

package cell;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.supercsv.cellprocessor.ParseDate;
import org.supercsv.cellprocessor.ift.DateCellProcessor;
import org.supercsv.exception.SuperCSVException;
import org.supercsv.util.CSVContext;

/**
 * write時の処理を考慮したParseDate拡張
 * @author kazuki.kido
 */
public class ParseDateEx extends ParseDate{
    /** 日付フォーマット */
    DateFormat formatter;
    /**
     * コンストラクタ
     * @param s
     */
    public ParseDateEx(String s) {
        super(s);
        formatter = new SimpleDateFormat(s);
    }
    /**
     * コンストラクタ
     * @param s
     * @param datecellprocessor
     */
    public ParseDateEx(String s, DateCellProcessor datecellprocessor) {
        super(s, datecellprocessor);
        formatter = new SimpleDateFormat(s);
    }
    /**
     * フォーマットチェック処理
     * @param obj
     * @param csvcontext
     * @throws SuperCSVException
     */
    public Object execute(Object obj, CSVContext csvcontext) throws SuperCSVException {
        // 出力時処理
        if(obj instanceof Date){
            return formatter.format((Date)obj);
        }
        // 入力時処理
        return super.execute(obj, csvcontext);
    }
}

ParseDateを継承し、executeメソッドのみ小細工しています。他メソッドは親メソッドを呼んでいるだけです。executeメソッドの引数objにDate型が渡された場合、ファイル出力時にメソッドが呼ばれているということになるので、formatterに基づくString型の値を返します。他の型が渡された場合は親メソッドに処理を引き継ぎます。

早速これをUserBeanに適用してみます。

ファイル名:UserBean.java

package bean;

import java.util.Date;

import org.supercsv.cellprocessor.Optional;
import org.supercsv.cellprocessor.ParseInt;
import org.supercsv.cellprocessor.constraint.StrMinMax;
import org.supercsv.cellprocessor.constraint.Unique;
import org.supercsv.cellprocessor.ift.CellProcessor;

import cell.ParseDateEx;

/**
 * CSVファイルの要素定義(Bean)
 * @author kazuki.kido
 */
public class UserBean {
    
    /** 各要素フォーマット定義 */
    public static final CellProcessor[] processors = new CellProcessor[] {
            new Unique(new StrMinMax(4, 20)),    // username
            new StrMinMax(7, 35),                // password
            new ParseDateEx("dd/MM/yyyy"),       // date
            new Optional(new ParseInt()),        // zip
            null                                 // town
    };
    
    /* 各要素の Getter/Setter 定義 */
    
    private String username, password, town;
    private Date date;
    private int zip;

    public String getPassword() { return password; }
    public Date getDate() { return date; }
    public String getTown() { return town; }
    public String getUsername() { return username; }
    public int getZip() { return zip; }
    public void setPassword(String password) { this.password = password; }
    public void setDate(Date date) { this.date = date; }
    public void setTown(String town) { this.town = town; }
    public void setUsername(String username) { this.username = username; }
    public void setZip(int zip) { this.zip = zip; }
}

ParseDate ⇒ ParseDateExに変わったのと、importが変更になっただけですね。
これで準備OKです。
実行してみると、以下のような内容が出力されます。

ファイル名:boo.csv

username,password,date,zip,town
Klaus,qwexyKiks,30/09/2007,1111,Tokyo
Oufu,bobilop,30/09/2007,4555,Tokyo

ちゃんと日付も意図した形式で出力されました。


尚、今回のプロジェクト構成は以下の通りです。

[java]Super CSV による File Write

では続いてファイル書き込みもやってみましょう。
ファイル名:WritingMaps.java

package write;

import java.io.FileWriter;
import java.util.HashMap;

import org.supercsv.io.CsvMapWriter;
import org.supercsv.io.ICsvMapWriter;
import org.supercsv.prefs.CsvPreference;

/**
 * SuperCSVによるCSVファイル書き込み
 * @author kazuki.kido
 */
class WritingMaps {
    public static void main(String[] args) throws Exception {
        ICsvMapWriter writer = new CsvMapWriter(new FileWriter("./data/boo.csv"), CsvPreference.EXCEL_PREFERENCE);
        try {
            final String[] header = new String[] { "name", "city", "zip" };
            // 1行目データを作成
            final HashMap<String, ? super Object> data1 = new HashMap<String, Object>();
            data1.put(header[0], "Karl");
            data1.put(header[1], "Tent city");
            data1.put(header[2], 5565);
            // 2行目データを作成
            final HashMap<String, ? super Object> data2 = new HashMap<String, Object>();
            data2.put(header[0], "Banjo");
            data2.put(header[1], "River side");
            data2.put(header[2], 5551);
            // ファイルへ出力
            writer.writeHeader(header);
            writer.write(data1, header);
            writer.write(data2, header);
        } finally {
            writer.close();
        }
    }
}

こちらはBeanではなく、Mapを用いています。当然、ICsvBeanWriter, CsvBeanWriterとUserBeanを使えば、Beanによる書き込みも可能です。
[java]CsvBeanReader, CsvBeanWriter, ParseDateを併用するには - Undead mode 忘備録

ファイルへの出力結果は次の通り。
ファイル名:boo.csv

name,city,zip
Karl,Tent city,5565
Banjo,River side,5551

よさげです。

[java]Super CSV による File Read

Super CSVは、CSVファイルをオブジェクトのような感覚で扱うことができる、オープンソースCSVファイル高速アクセスライブラリです。言ってみればO/RマッピングCSVファイル版ですね。
http://supercsv.sourceforge.net/
これが非常に便利そうなのに、何故かサイトに載っているサンプルコードが微妙に不親切なので、簡単に書き換えてみました。


読み込むCSVファイルの内容は次のような感じです。
ファイル名:foo.csv

username, password,   date,        zip,  town
Klaus,    qwexyKiks,  17/1/2007,   1111, New York
Oufu,     bobilop,    10/10/2007,  4555, New York

さて、まずはBeanの定義。このクラスでは、CSVファイルの列要素を定義します。
ファイル名:UserBean.java

package bean;

import java.util.Date;

import org.supercsv.cellprocessor.Optional;
import org.supercsv.cellprocessor.ParseDate;
import org.supercsv.cellprocessor.ParseInt;
import org.supercsv.cellprocessor.constraint.StrMinMax;
import org.supercsv.cellprocessor.constraint.Unique;
import org.supercsv.cellprocessor.ift.CellProcessor;

/**
 * CSVファイルの要素定義(Bean)
 * @author kazuki.kido
 */
public class UserBean {
    
    /** 各要素フォーマット定義 */
    public static final CellProcessor[] processors = new CellProcessor[] {
            new Unique(new StrMinMax(4, 20)),    // username
            new StrMinMax(7, 35),                // password
            new ParseDate("dd/MM/yyyy"),         // date
            new Optional(new ParseInt()),        // zip
            null                                 // town
    };
    
    /* 各要素の Getter/Setter 定義 */
    
    private String username, password, town;
    private Date date;
    private int zip;

    public String getPassword() { return password; }
    public Date getDate() { return date; }
    public String getTown() { return town; }
    public String getUsername() { return username; }
    public int getZip() { return zip; }
    public void setPassword(String password) { this.password = password; }
    public void setDate(Date date) { this.date = date; }
    public void setTown(String town) { this.town = town; }
    public void setUsername(String username) { this.username = username; }
    public void setZip(int zip) { this.zip = zip; }
}

で、実際に読み込み処理を行うmain実装が↓のようなかんじになります。読み込んだ内容をコンソール出力します。
ファイル名:ReadingObjects.java

package read;

import java.io.FileReader;

import org.supercsv.io.CsvBeanReader;
import org.supercsv.io.ICsvBeanReader;
import org.supercsv.prefs.CsvPreference;

import bean.UserBean;

/**
 * SuperCSVによるCSVファイル読み込み
 * @author kazuki.kido
 */
public class ReadingObjects {
    
    public static void main(String[] args) throws Exception{
        ICsvBeanReader inFile = new CsvBeanReader(
            new FileReader("./data/foo.csv"), CsvPreference.EXCEL_PREFERENCE);
        try {
            final String[] header = inFile.getCSVHeader(true);
            UserBean user = null;
            while((user = inFile.read(UserBean.class, header,
                            UserBean.processors)) != null){
                // 取得要素をコンソールへ出力
                System.out.print("ユーザ名:" + user.getUsername());
                System.out.print(" パスワード:" + user.getPassword());
                System.out.print(" 日付:" + user.getDate());
                System.out.print(" 郵便番号:" + user.getZip());
                System.out.println(" 住所:" + user.getTown());
            }
        } finally {
            inFile.close();
        }
    }
}

で、コンソール出力結果がこんなかんじになります。

ユーザ名:Klaus パスワード:qwexyKiks 日付:Wed Jan 17 00:00:00 JST 2007 郵便番号:1111 住所:New York
ユーザ名:Oufu パスワード:bobilop 日付:Wed Oct 10 00:00:00 JST 2007 郵便番号:4555 住所:New York

Beanを介してCSVファイルの内容を簡単に取得できます。定義に沿ってDate型なんかに自動変換してくれるので、とても楽です。


ちなみにプロジェクト構成は以下の通り。

[java]Super CSV で区切り文字や改行文字を任意に指定する

「で、区切り文字とかどうなってんの?」


はい、ちゃんと用意されています。CsvPreferenceというクラスで指定できます。
次のように第2引数で指定します。

ICsvBeanReader inFile = new CsvBeanReader(new FileReader("./data/foo.csv"), CsvPreference.EXCEL_PREFERENCE);

ICsvMapWriter writer = new CsvMapWriter(new FileWriter("./data/boo.csv"), CsvPreference.EXCEL_PREFERENCE);


CsvPreference.EXCEL_PREFERENCEを使用していますが、予め用意されている定義は全部で4種類。

定数名称 クォート文字 区切り文字(デリミタ) 改行文字
STANDARD_PREFERENCE " , \r\n
EXCEL_PREFERENCE " , \n
EXCEL_NORTH_EUROPE_PREFERENCE " ; \n
NO_COMMENT_PREFERENCE " , \n

何気にEXCEL_PREFERENCEとNO_COMMENT_PREFERENCEは、定義内容が全く同じです。

さて、当然これ以外のパターンについても任意で指定することが可能です。たとえば、クォート文字を'B'、区切り文字を'?'、改行文字列を"\r\n"としたい場合、

CsvPreference preference = new CsvPreference('B', '?', "\r\n");
ICsvBeanReader inFile = new CsvBeanReader(new FileReader("./data/foo.csv"), preference);

ICsvMapWriter writer = new CsvMapWriter(new FileWriter("./data/boo.csv"), preference);

といった要領でCsvPreferenceをインスタンス化し、使用します。

[java]Jakarta IO の底力

単純なファイル読み込みを行う場合、FileInputStream, InputFileReader, BufferReader
のコンボを使うよりも、LineIteratorを使う方が高速ですよ、という話。
Jakarta : 266ms
Standard : 343ms
Channel : 3375ms
filesize : 1,331,881 bytes
Java 1.6.0.2

/**
 * jakarta.ioを用いたファイル読込み
 * @param filePath
 * @return ファイル内容
 */
public List<String> jakartaReader(String filePath, String encode) throws IOException{
    List<String> fileData = new ArrayList<String>();
    LineIterator iterator = FileUtils.lineIterator(new File(filePath), encode);
    try{
        while(iterator.hasNext()){
            fileData.add(iterator.nextLine());
        }
    } finally {
        LineIterator.closeQuietly(iterator);
    }
    return fileData;
}
/**
 * 一般的なファイル読込み
 * @param filePath ファイルパス
 * @return ファイル内容
 */
public List<String> standardReader(String filePath, String encode) throws IOException{
    List<String> fileData = new ArrayList<String>();
    FileInputStream fs = null;
    InputStreamReader sr = null;
    BufferedReader br = null;
    try{
        fs = new FileInputStream(filePath);
        sr = new InputStreamReader(fs, encode);
        br = new BufferedReader(sr);
        String line = null;
        while((line = br.readLine()) != null){
            fileData.add(line);
        }
    }finally{
        if(br != null) try { br.close(); } catch (IOException e){}
        if(sr != null) try { sr.close(); } catch (IOException e){}
        if(fs != null) try { fs.close(); } catch (IOException e){}
    }
    return fileData;
}
/**
 * channel経由のファイル読込み
 * @param filePath
 * @return ファイル内容
 */
public List<String> channelReader(String filePath, String encode) throws IOException{
    List<String> fileData = new ArrayList<String>();
    int bufSize = 65535;
    FileInputStream fs = null;
    try{
        fs = new FileInputStream(filePath);
        FileChannel channel = fs.getChannel();
        long size = channel.size();
        if(size < bufSize) bufSize = (int)size;
        ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        byte[] bytes = new byte[bufSize];
        boolean chCR = false;
        int lastSize = (int)(size % bufSize);
        read : for(long pos = size; pos > 0; pos -= bufSize){
            if(pos <= lastSize) bytes = new byte[lastSize];
            buffer.get(bytes);
            String temp = new String(bytes, encode);
            int start = 0;
            for(int i=0; i<temp.length(); i++){
                char c = temp.charAt(i);
                if(c == '\r'){ // CR
                    fileData.add(temp.substring(start, i));
                    chCR = true;
                } else if(c == '\n'){ // LF
                    start = i + 1;
                    if(chCR){
                        chCR = false;
                    } else {
                        fileData.add(temp.substring(start, i));
                    }
                } else if(c == '\0'){ // ファイル終了
                    fileData.add(temp.substring(start, i));
                    break read;
                } else {
                    chCR = false;
                }
            }
            size -= bufSize;
        }
    } finally {
        if(fs != null) try{ fs.close(); } catch (IOException e){}
    }
    return fileData;
}