Python でファイルのハッシュ値を計算する(MD5, SHA256)

ファイルのハッシュ値(チェックサム)を計算すると、ファイルの整合性確認や重複検出ができる。Python の hashlib モジュールを使う。

MD5 ハッシュを計算する

import hashlib

def md5_hash(filepath):
    hash_md5 = hashlib.md5()
    with open(filepath, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

print(md5_hash('data.txt'))  # 'd41d8cd98f00b204e9800998ecf8427e'

チャンク単位で読み込むことで、大容量ファイルでもメモリを節約できる。

SHA256 ハッシュを計算する

MD5 は衝突攻撃に対して脆弱なため、セキュリティが重要な場面では SHA256 を使う。

import hashlib

def sha256_hash(filepath):
    hash_sha256 = hashlib.sha256()
    with open(filepath, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            hash_sha256.update(chunk)
    return hash_sha256.hexdigest()

print(sha256_hash('data.txt'))

汎用的なハッシュ関数

アルゴリズムを引数で指定できる汎用関数を作ると便利だ。

import hashlib

def file_hash(filepath, algorithm='sha256'):
    h = hashlib.new(algorithm)
    with open(filepath, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            h.update(chunk)
    return h.hexdigest()

# 使用例
print(file_hash('data.txt', 'md5'))
print(file_hash('data.txt', 'sha256'))
print(file_hash('data.txt', 'sha512'))

利用可能なアルゴリズムは hashlib.algorithms_available で確認できる。

小さなファイルは一度に読む

数 MB 以下の小さなファイルなら、一度に読み込んでも問題ない。

import hashlib
from pathlib import Path

def quick_hash(filepath):
    data = Path(filepath).read_bytes()
    return hashlib.sha256(data).hexdigest()

ファイルの整合性を確認する

ダウンロードしたファイルのハッシュ値と公開されている値を比較する。

def verify_file(filepath, expected_hash, algorithm='sha256'):
    actual_hash = file_hash(filepath, algorithm)
    return actual_hash == expected_hash.lower()

# 使用例
expected = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
if verify_file('download.zip', expected):
    print('ファイルは正常です')
else:
    print('ファイルが破損または改ざんされています')

重複ファイルを検出する

ハッシュ値を使ってディレクトリ内の重複ファイルを見つける。

from collections import defaultdict
from pathlib import Path

def find_duplicates(directory):
    hash_map = defaultdict(list)
    
    for filepath in Path(directory).rglob('*'):
        if filepath.is_file():
            h = file_hash(filepath)
            hash_map[h].append(filepath)
    
    # 重複があるもの(同じハッシュが2つ以上)だけを返す
    return {h: files for h, files in hash_map.items() if len(files) > 1}

# 使用例
duplicates = find_duplicates('./documents')
for hash_val, files in duplicates.items():
    print(f'重複: {files}')

高速化:最初にサイズで絞り込む

大量のファイルを処理する場合、まずファイルサイズで絞り込むと高速化できる。

from collections import defaultdict
from pathlib import Path

def find_duplicates_fast(directory):
    # まずサイズでグループ化
    size_map = defaultdict(list)
    for filepath in Path(directory).rglob('*'):
        if filepath.is_file():
            size_map[filepath.stat().st_size].append(filepath)
    
    # サイズが同じファイルだけハッシュを計算
    hash_map = defaultdict(list)
    for size, files in size_map.items():
        if len(files) > 1:
            for filepath in files:
                h = file_hash(filepath)
                hash_map[h].append(filepath)
    
    return {h: files for h, files in hash_map.items() if len(files) > 1}

サイズが異なるファイルは絶対に重複しないため、ハッシュ計算を省略できる。