C#でファイルのハッシュ(SHA-256)を求めて、同じファイルの二重登録を防ぐ

C#・.NET

同じファイルを二度登録してしまうのを防ぎたい

領収書の画像や請求書のPDFなど、ファイルを添付する画面では、同じファイルを二度登録してしまうことがあります。

同じ領収書の画像を2回添付していて、経理の確認で二重計上に気づいた

ファイル名で比べても、名前を変えたコピーは見分けられません。中身を全部比べるのは、ファイルが増えるほど重くなります。そこで使うのがハッシュです。

ハッシュ(SHA-256)は、ファイルの中身から計算した64文字の「指紋」のような値です。中身が1バイトでも違えばまったく別の値になり、中身が同じなら名前が違っても同じ値になります。

本記事では、C#でファイルのSHA-256を求め、同じファイルの二重登録を防ぐ方法を紹介します。自作の立替精算システムで、領収書の画像の二重添付を防ぐのに使っている仕組みです。

立替精算システムで使ったところ

立替精算システムの領収書の確認の画面。領収書の画像と、画像サイズの確認、Windows OCRで読み取った金額・日付の候補が並んでいる

領収書を添付したときの確認画面です。画像のサイズ(画素数)を調べ、Windows OCRで金額や日付の候補を読み取っています。

私が自作した立替精算システムでは、領収書の画像を添付するときに、画像の種類・大きさを調べるのと一緒にSHA-256を計算して、データベースに保存しています。同じ明細にすでに同じハッシュの画像があれば、「同じ画像が既に添付されています。」と表示して、添付しません。

C#でファイルのSHA-256を求めて重複を調べるサンプルコード

動作確認は Windows 11・.NET 10 です(.NET 8 以降で動きます)。Visual Studio でコンソールアプリを作り、Program.cs に貼り付けて実行してください。.NET 10 なら、ファイルに保存して dotnet run ファイル名.cs でも実行できます。

using System.Security.Cryptography;

// 試しのファイルを 3 つ作る(b は a のコピーで名前だけ違う、c は 1 バイトだけ違う)
File.WriteAllBytes("ryoushuusho_a.jpg", [0xFF, 0xD8, 0x01, 0x02, 0x03, 0xFF, 0xD9]);
File.Copy("ryoushuusho_a.jpg", "ryoushuusho_b.jpg", overwrite: true);
File.WriteAllBytes("ryoushuusho_c.jpg", [0xFF, 0xD8, 0x01, 0x02, 0x04, 0xFF, 0xD9]);

// 添付済みのハッシュ(立替精算では、明細ごとにデータベースから読む)
var tenpuZumi = new HashSet<string>();

foreach (var fileNm in new[] { "ryoushuusho_a.jpg", "ryoushuusho_b.jpg", "ryoushuusho_c.jpg" })
{
    var hash = await GetSha256Async(fileNm);
    Console.WriteLine($"{fileNm}:{hash}");

    // HashSet.Add は、すでに同じ値があると false を返す
    if (!tenpuZumi.Add(hash))
    {
        Console.WriteLine("  → 同じ画像が既に添付されています。");
    }
    else
    {
        Console.WriteLine("  → 添付しました。");
    }
}


/// <summary>
/// ファイルの SHA-256 を、大文字の 16 進数 64 文字で返す。
/// ストリームで読むので、大きなファイルでも全部をメモリに読み込まない。
/// </summary>
static async Task<string> GetSha256Async(string path)
{
    await using var stream = File.OpenRead(path);
    var hash = await SHA256.HashDataAsync(stream);
    return Convert.ToHexString(hash);
}

実行結果

ryoushuusho_a.jpg:ED812B5630335B55EF9A3502826B18FABE1C5D095D3890AF99375D8812014154
  → 添付しました。
ryoushuusho_b.jpg:ED812B5630335B55EF9A3502826B18FABE1C5D095D3890AF99375D8812014154
  → 同じ画像が既に添付されています。
ryoushuusho_c.jpg:B6DEBCFE12A53796D0BAA9135F9E4A13D2CFEBC89BA93C33D7C0A6CB941B558D
  → 添付しました。

ryoushuusho_b.jpg は a のコピーで名前だけが違うため、同じハッシュになり、二重登録として止まっています。c は1バイトだけ違うので、まったく別のハッシュになっています。

コードの説明

  • SHA256.HashDataAsync(stream) は、ファイルを読みながらハッシュを計算します(.NET 7以降)。大きなファイルでも、全部をメモリに読み込みません。
  • すでに byte[] で持っている場合は、SHA256.HashData(data) で計算できます。立替精算システムでは、アップロードされた画像を byte[] で受け取っているので、こちらを使っています。
  • Convert.ToHexString は、32バイトのハッシュを16進数の64文字の文字列にします。データベースに保存したり、比べたりしやすくなります。
  • HashSet<string> の Add は、同じ値がすでにあると false を返すので、重複のチェックがそのまま書けます。

データベースで重複を調べる

立替精算システムでは、ハッシュを添付ファイルの表に保存しておき、添付するときに同じ明細の中で同じハッシュが無いかを調べています(Entity Framework Coreを使っています)。

var hash = Convert.ToHexString(SHA256.HashData(data));

// 同じ明細に、同じ画像がすでにあるか(削除した画像は除く)
if (await db.Tenpu.AnyAsync(
        x => x.MeisaiId == meisaiId && x.HashSha256 == hash && !x.SakujoFlg,
        cancellationToken))
{
    return "同じ画像が既に添付されています。";
}

削除した画像は除いて調べているので、間違えて消した画像を添付し直すことはできます。

注意点

  • ハッシュで分かるのは「中身がまったく同じファイル」だけです。同じ領収書を撮り直した画像や、縮小した画像は別のハッシュになるため、見分けられません。そうした重複は、金額や日付などほかの情報で確かめる必要があります。
  • MD5やSHA-1もハッシュですが、同じ値になる別のデータを作れることが分かっています。新しく作るならSHA-256を使ってください。
  • パスワードの保存には、SHA-256をそのまま使わず、専用の仕組み(ASP.NET Core Identityの PasswordHasher など)を使ってください。立替精算システムでも、ログインのパスワードはそちらで保存しています。

最後に

以上が、C#でファイルのハッシュ(SHA-256)を求めて、同じファイルの二重登録を防ぐ方法です。

ハッシュは1行で計算できます。保存しておけば、ファイルが何万件に増えても、64文字を比べるだけで同じファイルかどうかが分かります。

この仕組みを使っている立替精算システムは、こちらで紹介しています。

Pythonで文字列などをハッシュ化する方法は、こちらで紹介しています。

コメント

タイトルとURLをコピーしました