先日、大きなサイズのXMLファイルを解析して変換し、小さなサイズのXMLファイルを出力するというプログラムをC#で作りました。
XMLの解析や作成はLINQを使えばかなり簡単なのでその部分のロジックはまったく問題なくできたのですが、出力するファイル名の付け方を間違ってしまうというミスをしてしまいました。
もう少し詳しく書くと、A.xmlというXMLファイルを10個のファイルに分割したときに、分割後のファイルの名前をA_yyyyMMddhhmissfff.xmlとしたところ、ファイル名が重複してしまったというミスです。
ここでyyyyMMddhhmissfffというのは、日時を入れるという意味でして、たとえば2012年6月18日12時34分56秒789ミリ秒に処理をした場合にはA_20120618123456789.xmlという名前を付けることになります。
なんでこれがダメだったのかはもう一目瞭然ですが、XMLを作るのに要した時間が1ミリ秒以下だった場合に名前が同じXMLができてしまうというミスです。初歩的すぎる...。
これはこれで手抜きをすると痛い目にあうというよい教訓になったわけですが、せっかくなのでファイル名を自動生成する方法についてまとめたいと思います。
1. Path.GetTempFileName()を使う
一時ファイルを作成する方法を調べた時に一番最初に出てきたのがこのPath.GetTempFileName()というメソッドでした。
(メリット)
-
- メソッドを呼び出すだけでファイル名が取れて楽
(デメリット)
-
- ファイルの出力先フォルダやファイルの拡張子が選択できない
- IOExceptionを出す場合がある
試しに使ってみたところ、以下の仕様で動作しているようでした。
出力先 | %TEMP% |
ファイル名 | tmp????.tmp |
その他 | メソッドを呼び出したタイミングでサイズが0Byteのファイルが作られる |
ファイル名の????には0000-FFFFまでの文字列が順番に入るようです。これがFFFFまでいってしまうと、次の呼び出しがIOExceptionになりますので、つまりは65535回しかこのメソッドは呼べないことになります。もちろん使い終わったファイルは消せばよいのですが、そもそも「一意なファイル名が欲しい」だけなのに、使い終わったあとの始末までやることを強制されるのってなんか違う気が...。いや、やることはやるんですが、本末転倒な気がしてなりません。
とか書いてたらちゃんとMSDNに書いてました。
一意な名前を持つ 0 バイトの一時ファイルをディスク上に作成し、そのファイルの完全パスを返します。
Path.GetTempFileName Method (System.IO) | Microsoft Docs
このメソッドは、.TMP という拡張子を持つ一時ファイルを作成します。
以前の一時ファイルを削除せずに、GetTempFileName メソッドを使用して 65535 個を超える数のファイルを作成した場合、IOException が発生します。
GetTempFileName メソッドは、一意な一時ファイル名が使用できない場合に、IOException を発生させます。このエラーを解決するには、すべての不必要な一時ファイルを削除します。
Path.GetTempFileName Method (System.IO) | Microsoft Docs
このあたりの不便さ(出力先や拡張子が選べない)は、staticなメソッドを呼び出すだけで使えるという気軽さとトレードオフした結果だと思うのでここはしょうがないのかなと。不便とはいえ、たとえば出力した後に名前を変えて移動すれば解決できる程度の問題ですのクリティカルな問題点ではありません。
ただ、そういう不便さは許せる一方で、使うためには決まりごとがいくつか生じてしまうことやエラーハンドリングが必要なこと、そして「ファイル名が欲しいだけなのに後始末まで約束させられる」というのは個人的にはいただけないなーと思います。
いまのところ「めんどくさいから使わね」という結論に...。
2. データのユニークキーを流用する
作成するファイルに格納されるデータのキー情報(個人や商品のIDなど)がある場合には、それをファイル名に入れることでユニークなファイル名が作成できます。
(メリット)
-
- データの中とファイル名が結びついているので分かりやすい
(デメリット)
-
- ファイルの中にキー情報が無いとそもそも使えない
- 同一キーに対して複数のファイルが必要になった場合など、これ単体では使えないケースがある
アイディアとしては悪くないのですが、基本的にこれだけではユニークなファイル名は生成できません。
それは他のほとんどのアイディアも同じなんですが...。
3. 連番を利用する
ファイル名に処理をした順番に連番を振るという方法です。
ベタといえばベタなんですが、ハンドリングしやすいですしとても確実な方法です。
さらに処理した順番もわかるので個人的にはすごく好きな方法です。
int cnt = 1; string before = DateTime.Now.ToString("yyyyMMdd"); foraech (xxxxxxx){ // ここのループ条件は適当に string now = DateTime.Now.ToString("yyyyMMdd"); if (before != now) cnt = 1; string.Format("A_{0}_{1:000}.xml", DateTime.Now.ToString("yyyyMMdd"), cnt++); before = now; }
ここでは日付ごとに連番を振っていますが、このあたりは好き好きで。
(メリット)
-
- ファイルを作成した順番がわかる
(デメリット)
-
- 連番だけではユニークであることを保証できない
上の例だと日付と組み合わせて使っていますが、こんな感じで連番を振るにしても他の方法との併用が望ましいです。
4. 乱数/ハッシュを利用する
ここまで引っ張ってしまいましたが、プログラマーが一意なデータを得ようと最初に思いつくのはおそらく乱数orハッシュだと思います。好みはあるでしょうけど。
乱数だとこんな感じ。
foraech (xxxxxxx){ // ここのループ条件は適当に string.Format("A_{0:0000000000}.xml", new Random().Next()); }
ハッシュはいいサンプルが思いつかないので、参考サイトを紹介するにとどめておきます。ファイルがそれほど大きくなければ、ファイルの中身全部でハッシュ値を生成するのがよいのかなと。
(メリット)
-
- 扱いが簡単な割に効果は大きい
(デメリット)
-
- そもそも一意であることを完全に保証する仕組みではない
- 名前からファイルの中身を判断するのが難しい
確率的には被る可能性はほぼゼロなんですが、ゼロではないだけで可能性としてはありえます。
なので仕組みとしてそもそも不完全なのと、あとはファイル名が無作為過ぎてファイルの中と結びつかないのは個人的にはあまり好きではないです。なので私自身はあまり使いたい方法ではありません。
5. PIDを利用する
プロセスごとに振られたIDを使えばユニークになるんじゃね?という安直な方法です。
int cnt = 1; int pid = Process.GetCurrentProcess().Id; foraech (xxxxxxx){ // ここのループ条件は適当に string.Format("A_{0}_{1:000}.xml", pid , cnt++); }
(メリット)
-
- 呼び出すだけで簡単
(デメリット)
-
- 常駐型プロセスだとIDは変わらない
とりあえずサービスとして動かしている場合などはプロセスIDは常に変わらないのでこれだけはキー情報になりません。日付や連番と組み合わせて使うのがよいのかなと。というか、それだったら別にプロセスIDはいらないか...。
GUIDを利用する
GUID(Global Unique IDentifier)、日本語だとグローバル一意識別子というそうですが、WikipediaによるとUUID(Universally Unique IDentifier)のMS版実装を指すそうです。ただ、MS版と言いつつも多くのサービス、ソフトウェアで使われているために概ね一般的な言葉ととらえてもよさそうです。
MSDNを読んで気付いたのですが、これクラスじゃなく構造体なんですね。ただC#の場合は構造体もメソッドをもてるので、値型か参照型かとか配置される場所がヒープかスタックかくらいしか違いはなくて扱いは変わんないので
GUID は、一意な識別子が必要とされるコンピュータおよびネットワーク全体で使用できる 128 ビットの整数 (16 バイト) です。このような識別子は、重複する確率がかなり低くなっています。
Guid Struct (System) | Microsoft Docs
これも使い方は簡単で、NewGuidメソッドで初期化してからToString()するだけです。
Guid guidValue = Guid.NewGuid(); Console.WriteLine(guidValue.ToString()); // -が付いてる Console.WriteLine(guidValue.ToString("N")); // -が付いてない Console.WriteLine(guidValue.ToString("B")); // -が付いていて、{}で囲まれている Console.WriteLine(guidValue.ToString("P")); // -が付いていて、()で囲まれている
で、結果はこんな感じです。
新しいGUIDが欲しい場合は再度NewGuidメソッドを呼べばOKです。
(参考) GUID値を生成するには?
(メリット)
-
- 呼び出すだけでほぼ一意な文字列が取れて便利
(デメリット)
-
- 任意過ぎて名前に規則性がなく扱いにくいケースがある
- 確実にファイル名がかぶらないわけではない
かなり便利ですし、名前が被る確率もほぼないので悪くないのですが、これも乱数などと同じで規則性が無さすぎてファイル名としてはとても扱いにくいです。
あとこれもここまで紹介したとおり、これだけで完全に重複を防ぐ方法ではないので、他の方法と同じく単体で使うのではなく要素として使うべきかなと。
6. 結論
結局、今回私が選んだのは「日付(yyyyMMdd)」+「ユニークな情報を付与」+「連番」という組み合わせでした。
public string GetUniqueFileName(string id, ref int num){ while (true){ string fileName = string.Format("XMLファイル_{0}_{1}_{2:00000}.xml", DateTime.Now.ToString("yyyyMMdd"), id, num++); if (File.Exists(fileName)) { if (num > 99999) throw new IOException("連番でか過ぎワロタ"); continue; } return fileName; } }
連番を5桁(99999まで)にしているのは、現実的に作成される上限とファイル名が極力短くしたいというところで折り合いをつけて決めただけでここはケースバイケースで変えてよいです。
あとソースにもあるとおり、既にその名前のファイルがあった場合には「連番」をインクリメントして再度名前を変えるという対応も合わせて行ったので、これでファイル被ることはなさそうです。本当は抜け番があった場合にも対応しようかと思ったのですが、現実的にそこまでファイルは増えないのでここでは割愛しました。
ちなみに、今回はシングルスレッド前提の処理なのでこの程度でも十分ですが、マルチスレッドな環境だともうちょっと工夫や気配りが必要かもです。
# 面倒なので今回はそこまでは説明しません(詳しくないし)
もっと良い方法があるよ!という人はコメントで教えてくださいー。