読者です 読者をやめる 読者になる 読者になる

森理 麟(moririring)のプログラマブログ

ゲームプログラマ森理 麟がのプログラムの話題を中心に書くブログです。

C#でログファイルを出力する少し冴えたやり方

この内容は、C# Advent Calender 2012への参加記事です。森理はAdvent Calender初参加です。

毎日他の人の記事を読んでいますが、ishisakaさんのを読み始めた時は「被ったか?」と思って焦りました。

読み進めていったら大丈夫だったのでホッとしました。来年はネタかぶりしないかどうかタイトルだけでも分かると良いかなと思います。

昨日はkazukさん。明日はugaya40さんです。


VisualStudioでツールを作った時、ログファイルを作成することはありませんか?

その場合appachのログのようにtxt形式で1行ずつテキストを追加していくのが簡単だと思います。

実装もStreamWriterのWriteLineを使うだけで、1行ずつ出力出来ます。

...
WriteLine("開始");
WriteLine("ログデータ1");
WriteLine("2012/12/24 0:00:00");
WriteLine("ログデータ2");
WriteLine("2012/12/24 0:00:03");
WriteLine("終了");
...


ただ、出力がそれほど大量ではなくて、むしろ内容が重要なログの場合もう少し表現を凝りたくなります。

太字や色を使って目立たせたり、縦横に長い情報をセル形式で表示出来たりすれば見た目はずっと良くなります。

この場合txt形式ではなくhtml形式にすればタグで殆ど実現出来ます。(doc形式はちと大変)。。

htmlのログを作る場合もStreamWriterのWriteLineなどで実際にタグを書いていく事になると思います。

...
WriteLine("<table>");
WriteLine("<thead><tr>");
WriteLine("<th>ログ名</th>");
WriteLine("<th>最終更新時間</th>");
WriteLine("</tr></thead>");
WriteLine("<tbody><tr>");
WriteLine("<td>ログデータ1</td><td>2012/12/24 0:00:00</td>");
WriteLine("<td>ログデータ2</td><td>2012/12/24 0:00:03</td>");
WriteLine("</tr></tbody>");
...


しかし、タグを文字列として渡していくプログラムはちょっと面倒です。

XmlTextWriterを使えばXML形式はもう少し楽に出力出来ますが、これも数が増えてくると同様です。

理想はhtmlファイルに普通にタグを書いていって、それをそのままロードしてログファイルとしたくなります。

これが出来ればhtmlを変更することでユーザーがカスタマイズも可能です。

ただ実現にはプログラムで使っている変数をhtml側で取得出来る表現が必要になります。

...
<table>
<thead>
<tr>
<th>ログ名</th>
<th>最終更新時間</th>
</tr>
</thead>
<tbody>
  <tr>
    <td>
     ログデータ1 <!-- ←この部分はプログラムで動的に決まる -->
    </td>
    <td>
      2012/12/24 0:00:00 <!-- ←この部分はプログラムで動的に決まる -->
    </td>
    <td>
     ログデータ2 <!-- ←この部分はプログラムで動的に決まる -->
    </td>
    <td>
      2012/12/24 0:00:03 <!-- ←この部分はプログラムで動的に決まる -->
    </td>
  </tr>
</tbody>
...


またセル状のログを作る事もよくあると思いますが、その場合「ループ」も表現したくなります。

「ループ」や「変数」を表現出来るhtml。これは単純に考えてスクリプト言語がそれにあたります。

例えばphpを使えば、変数やループを定義出来るので、動的に決まる変数を渡す事も当たり前のように出来ます。

しかしphpにしてしまうとパーサーが必要ですし、カスタマイズ出来るユーザーも限られてきます。

...
<table>
<thead>
<tr>
<th>ログ名</th>
<th>最終更新時間</th>
</tr>
</thead>
<tbody>
<tr>
<!-- ↓phpならこう書いておけば動的に決まる部分に対応出来る -->
<?php 
foreach($data => $value) {
    print("<td>{$value}</td>");
}
?>
</tr>
</tbody>
...


phpほど色々やれなくても、ログファイルなので「変数」、「ループ」、「分岐」ぐらいが渡せればOKです。

そうなるとテンプレートエンジンが考えている用途に近いかなと思いました。例えばPHPならSmartyがあります。

さらに言うなら用途がシンプルなのでオリジナルテンプレートを作る手もあります。

折角なのでC#の形式のテンプレートが欲しい。そんなことを軽くTwitterでつぶやきました。すると神の一言が帰ってきました。


Razorエンジンなんじゃそら?

恥ずかしながらその存在自体全く知らなかったのですが、既にC#のテンプレートエンジンはありました。

しかも記述がもの凄く直感的です。Razorを知らなくても何となくカスタマイズ出来そうなぐらいシンプルです。

更にちょっと調べただけでRazorEngineというパーサーも見つけました。

...
<table>
<thead>
<tr>
<th>ログ名</th>
<th>最終更新時間</th>
</tr>
</thead>
<tbody>
<tr>
<!-- ↓Razorならこう書いておけば動的に決まる部分に対応出来る。PHPよりかなりシンプル! -->
@foreach(var item in items)
{
<td>@(item.Name)</td>
} 
</tr>
</tbody>
...


htmlにC#で変数渡しやループ、分岐表現が書けて、パーサはRazorEngineを使う。

これで直感的にレイアウトを作れて、カスタマイズ可能なログファイルフォーマットが作れそうです。

やってみると自分で定義したクラスをListにして渡すところだけ少し手こずりましたが、結果的には実装出来ました。

RazorやRazorEngineの日本語の情報がまだ少なめではあります。


以下に自分で定義したクラスを渡すコードを書きます。

//ログとして渡したいクラスをcsで定義
public class LogData
{
    public string LogName { set; get; }
    public string LogTime { set; get; }
};
public class LogDatas
{
    public List<LogData> Datas;
};
<!-- ログ定義ファイルとなるlog.cshtml。(ASP.NETではRazorで書かれたファイルの拡張子はcshtml) -->

@inherits RazorEngine.Templating.TemplateBase<HogeNamaspace.LogDatas>

<table border='1'>
<thead>
<tr>
<th>ログ名</th>
<th>最終更新時間</th>
</tr>
</thead>
<tbody>
  @foreach (var log in @Model.Datas)
  {
  <tr>
    <td>
      @(log.LogName)
    </td>
    <td>
      @(log.LogTime)
    </td>
  </tr>
  }
</tbody>
</table>
//gLogDatasはプログラム中で様々な値が入る。
LogDatas gLogDatas; 
...
//log.cshtmlで書かれたファイルのテキストを全ロード
var template = File.ReadAllText("log.cshtml");
//パーサにかけてテキストを変換、動的に決まるLogDatasはココで代入
var result = Razor.Parse(template, new LogDatas(){ Datas = gLogDatas });
//変換されたテキストをログファイルとして生成
using (var sw = new StreamWriter("log.html"))
{
    sw.Write(result);
}
...

これで実際以下のhtmlが出力されます。

<!-- Razor.Parseで生成された文字列をlog.htmlにして書き出し -->
...
<table>
<thead>
<tr>
<th>ログ名</th>
<th>最終更新時間</th>
</tr>
</thead>
<tbody>
  <tr>
    <td>
     ログデータ1
    </td>
    <td>
      2012/12/24 0:00:00
    </td>
    <td>
     ログデータ2
    </td>
    <td>
      2012/12/24 0:00:03
    </td>
  </tr>
</tbody>
...

これでログファイルを作る時、WriteLineで作っていくより、直感的にレイアウトを作る事が出来ます(C#分は実行するまで分かりませんが)。

実行ファイルを配布をする時にログ定義ファイルを同封すれば、ユーザー側でフォント色を変えるといったカスタマイズも出来ます。

バージョンアップした時などの同期で茨の道が待っている気がしないこともないですが、まあある程度分かっていれば回避も出来ます。

全部これでログを作る必要は全くないですが、状況に応じて選択肢に入れると面白いと思います。