SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

Python3でバックアップコピー(変更があり、かつ、新しいファイルのみ)/Pythonサンプル

目次

Python3でバックアップコピー(変更があり、かつ、新しいファイルのみ)

WindowsフォルダのバックアップツールをPythonで作ります。

要件としては。

  1. フォルダを指定して下位フォルダを再帰的にチェックする
  2. 更新有無をファイルのハッシュ値で検査し一致したものは対象にしない。
  3. ハッシュ値不一致でもタイムスタンプが古い場合は対象にしない。
  4. 上記の2つの差異条件にマッチしたものだけコピーする。

という感じです。

僕の作業フォルダにはOffice(Excel・Word他)ファイルなどが1000個くらいあって、細かくフォルダ分けされていたりするのですが、そこを30秒くらいで差分バックアップするのを目標にします。

使用例のソース

処理の実態は「class_dirtools.py」にまとめて、使うときは以下だけです。

from class_dirtools import MyDirTools


dt = MyDirTools()
dt.copy_difference_only(fromdir="C:\\tmp\\test2", todir="C:\\tmp\\test1")

これで「fromdir」以下の全ファイルをチェックして差分(変更があり、更新日が新しいもの)だけを「todir」以下にコピーします。

処理本体のソース

本体の「class_dirtools.py」です。

import pathlib
import hashlib
import shutil
import datetime

'''
差分のみバックアップ
ファイルのハッシュ値で比較した差分+タイムスタンプが新しい場合のみコピーする。
それ以外のファイルコピーは行わない。

'''


class MyDirTools:

    def __get_dir_dict(self, start_dir):
        files = list(pathlib.Path(start_dir).glob("**\\*"))
        ret = {}  # fileオブジェクトを格納する辞書
        ret_hash = {}  # file毎のハッシュ値を格納する辞書
        ret_updatetime = {}  # file毎の更新日付を格納する辞書
        for file in files:
            if(file.is_file()):
                ret.setdefault(file.name, file)
                update_time = datetime.datetime.fromtimestamp(
                    file.stat().st_mtime)
                ret_updatetime.setdefault(file.name, update_time)
                with file.open(mode='rb') as f:
                    ret_hash.setdefault(
                        file.name, hashlib.md5(
                            f.read()).hexdigest())
        return ret, ret_hash, ret_updatetime

    def copy_difference_only(
            self,
            fromdir,
            todir):
        fromfiledic, fromhashdic, fromupdatedic = self.__get_dir_dict(fromdir)
        tofiledic, tohashdic, toupdatedic = self.__get_dir_dict(todir)
        match_count = 0
        unmatch_count = 0
        copyed_count = 0
        for file in fromfiledic.items():
            fname = file[0]
            if((fname in tohashdic) and (fromhashdic[fname] == tohashdic[fname])):
                match_count += 1
            else:
                unmatch_count += 1
                if(fname not in tofiledic):
                    # copy2はディレクトリが存在しないとエラーになるので、先にフォルダ作成をする
                    tmp_path_str = todir
                    tmp_path_from = str(file[1]).replace(fromdir, "")
                    tmp_path_sub = tmp_path_from.replace("\\" + fname, "")
                    to_path_str = tmp_path_str + tmp_path_sub
                    topath = pathlib.Path(to_path_str)
                    topath.mkdir(parents=True, exist_ok=True)
                    # フォルダを作成したあとにコピーする
                    shutil.copy2(str(fromfiledic[fname]), to_path_str)
                    print("コピー:" + fname)
                    copyed_count += 1
                else:
                    tmp_path_str = str(tofiledic[fname])
                    # タイムスタンプが新しい場合のみコピーする
                    if(fromupdatedic[fname] > toupdatedic[fname]):
                        shutil.copy2(str(fromfiledic[fname]), tmp_path_str)
                        print("コピー:" + fname)
                        copyed_count += 1
        print(
            "一致:" +
            str(match_count) +
            "件、不一致:" +
            str(unmatch_count) +
            "件、コピー:" +
            str(copyed_count) +
            "件")

そんな複雑なことはしてないですが、以下で補足します。

ソースのポイント補足説明

補足します。

pathlib/hashlibを使ってます

基本、フォルダ・ファイル処理は「pathlib」をつかってやってます。

docs.python.org

ファイルのハッシュ値の計算には「hashlib」を使ってます。

docs.python.org

今回は「md5」を使って

hashlib.md5( f.read()).hexdigest()

のようにハッシュ値を計算しています。

MD5を選択した理由は、単に昔からあって僕がなじんでいるからです。

ここは、ちゃんとハッシュが計算できて、速いなら何でもいいと思います。

file.is_file()のチェック

今回のアイディアのポイントは、Pathlib.Pathで取得したファイルオブジェクトと計算済のハッシュ値や更新日付の情報などを、格納したPythonの辞書を前処理として作成し、実際の更新判定やコピー処理は辞書のループ内でやるところです。

最初は「glob("**\\*")」で取得できるものすべてを対象としてましたが、中にディレクトリも含まれるので、先にはじいておかないと、あと処理のつまらない所でエラーになって、チェック処理がゴチャゴチャしてしまいました。

なので「file.is_file()」でTrue(つまりファイルのみ)を辞書に格納しています。

shutil.copy2のエラー

コピー処理はライブラリの「shutil.copy2」を使ってます。

docs.python.org

shutil.copyだと、ファイルの作成時間や変更時間などをコピーしないので、タイムスタンプが変わってしまい、バックアップになりません。

なので、ファイルの作成時間や変更時間などもコピーする「shutil.copy2」が必要です。

とても便利なのですが、コピー先のフォルダがないとエラーで落ちます。

なので、コピー先にないファイルの場合に、フォルダがなければ先に作成しておく必要があります。

これは、pathlibのmkdirを使うと便利なので、文字列のパスをいったんpathlibオブジェクトに変換して、mkdirしています。

topath = pathlib.Path(to_path_str)
topath.mkdir(parents=True, exist_ok=True)

ここで「exist_ok=True」としておくと、すでに同じフォルダが存在していてもエラーにならないので、いちいち、新規作成する前に存在チェックがいりません。

エラーチェックは手抜きしてます

自分で使うツールなのと、ブログにのせるのにソースをできるだけシンプルに短くしたかったということもあって、エラーチェックの手は抜いてます。

正確に言えば、細かい処理をはしょって、骨組みだけ書いている感じです。

特に大きいのは、フォルダ指定のところです。

一応。

C:\\tmp\\test2

のようにコピー元とコピー先フォルダを指定するのですが、この時に

C:\\tmp\\test2\\

のように、最後に「\\」がつくと、うまく動きません。

他人に使ってもらうなら、エラーチェックをいれるなり、最後の「\\」があったら消すなりの処理が必要ですが、上記の理由ではしょってます。

そのへん、ご容赦ください。

ではでは。