[Python]タイムスタンプ変更アプリを作ってみた

##はじめに

windows,python3,tkinterを用いて、ファイルのタイムスタンプを変更するGUIアプリを作ってみた。

タイムスタンプとは、ファイルの作成日時・更新日時・アクセス日時を表すもので、ファイルのプロパティから確認できる。

タイムスタンプを変更する利点としては「ファイル作成日時の隠蔽」がある。日記などのように写真を連続投稿する場合、ファイル作成日時から撮影場所を推定される可能性があるため、撮影場所を知られたくないときにタイムスタンプの変更が有効な手段となる。

個人的な背景としては、父が山を登ったときに希少な植物を撮影することが多く、その写真を日記として投稿している。しかし希少であるため下手に場所を知られて他人に荒らされたくなく、できる限り写真から撮影場所を知られたくないようにしたい。そんな要望をきっかけに、勉強も兼ねて開発を始めた。

##アプリの概要

アプリの全体図を以下に示す。

![タイムスタンプ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/350483/2a6791d0-0aff-191f-54f1-d486ef2e18ac.png)

おおまかな処理内容としては以下の通り。

・「参照」ボタンをクリックするとファイルのパスをエクスプローラーから持ってくる
・「設定」ボタンをクリックすると入力したファイルのタイムスタンプを現在のプロパティに表示する
・修正後のプロパティを入力し「変更」ボタンをクリックすると、その日時にタイムスタンプを変更する
・進行状況に応じて説明文を画面上部に表示(エラーなどの告示もここで行う)


##ファイルパスの参照

アプリケーションでもよく使われる「ファイルの参照」だが、ファイルタイプ・初期ディレクトリのパスを設定すれば、tkinter.filedialog.askopenfilenameで実装できる。

tkinterを使う場合、「from tkinter import *」と入れる人が多いと思うが、これだけではtkinter.filedialogをimportできてない(filedialogとしても「定義されていない」とエラーが出る)。それぞれ別個にimportする必要があることに注意。

初期ディレクトリについては今回C//としているが、もっと深いディレクトリを指定することもできる。相対パスだとエラーの元になりがちなので、絶対パスが安定。

取得したファイルパスを表示したい場合、tkinter.Entry().insert()をすればよい。

参照部分のコードは以下のようになる(色々と定義してない変数があるが、後ほど全体コードを表記する)。

```python3
from tkinter import *
import tkinter.filedialog

def reference(self):
    fTyp = [("","*")]#ファイル拡張子の指定
    iDir = os.path.abspath('C//')#スタート地点
    filepath = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir)
    #入力フォームにパスを入れる
    file_input_form.insert(END,filepath)#file_input_formはtkinter.Entry()
    return
```

##タイムスタンプの表示

ファイルパスをEntry.get()で取得した後、os.path.getctimeなどでタイムスタンプを取得できる。

取得したタイムスタンプの時間はUNIX表記なので、日時表記に変えるためdatetime.datetime.fromtimestampを使う。また、小数6桁くらいまで表記されている(秒未満は必要ない)ので、math.floorで小数点以下を切り捨てる。

ラベル内容の変更は、Label['text']='文字列'でできる。

また、ファイルパスが正しく入力されていない場合、os.path.getctime(file_input_form.get())の時点でエラーを出すので、try-except構文を使えばエラーを出さずに入力部分の改善を促すことができる。

コードとしては以下の通り。

```python3
def set_path(self):
    try:
        #作成日時
        ct_u=os.path.getctime(file_input_form.get())#UNIX表記
        ct_d=datetime.datetime.fromtimestamp(math.floor(ct_u))#日時表記
        #更新日時
        mt_u=os.path.getmtime(file_input_form.get())
        mt_d=datetime.datetime.fromtimestamp(math.floor(mt_u))
        #アクセス日時
        at_u=os.path.getatime(file_input_form.get())
        at_d=datetime.datetime.fromtimestamp(math.floor(at_u))

        #取得した日時をラベルに反映
        creation_stamp.current_setting_time['text']=creation_stamp.name+': 　'+str(ct_d)
        update_stamp.current_setting_time['text']=update_stamp.name+': 　'+str(mt_d)
        access_stamp.current_setting_time['text']=access_stamp.name+': '+str(at_d)

        #説明部分の更新
        whole_description['text']='修正後の日時(例:2000-01-01 11:22:33)を入力し、「変更」をクリックしてください'
        return
    except:
        whole_description['text']='ファイルパスが正しくありません、もう一度入力し直してください'
```

##タイムスタンプの変更

文字列から日付への変換はdatetime.datetime.strptimeでできる。

アプリの肝となるタイムスタンプの変更だが、ファイル元の安全性を考えて、コピーしたファイルを用いる。ただ、容量が大きいとコピーに手間取るので、フラグを使ってコピーをするかしないか区別できるようにしてもいいと思う。

コピーしたファイルの名前については、s=file_before.find('.')、file_after = file_before[0:s]+'_修正'+file_before[s:]のように、find関数と文字列操作を活用して[ファイル名_修正.拡張子]とする。find関数を活用することで、拡張子の文字数に関わらず正常に動く(file_after = file_before[:-4]+'_修正'+file_before[-4:]とすると、文字数3の拡張子にしか使えなくなる)。

コピー自体については、file_copy = shutil.copy(コピー元のファイルパス, コピー後のファイルパス)で簡単にできる。

最後、datetime.datetime(年,月,日,時,分,秒)を用いてタイムスタンプに用いる日付を作成し、win32_setctime.setctime(file_copy, ctime)(作成日時)、os.utime(file_copy, (atime, mtime))(アクセス日時・更新日時)で変更できる。作成日時の変更のほうがやや複雑らしい。

参考：[【Python】ファイルのタイムスタンプを変更するコード例【作成日時・更新日時・アクセス日時を変更する】](https://srbrnote.work/archives/4054)(シラベルノート)


```python3
class timestamp():
　　#__init__は後述

    def get_timestamp(self):
        t_after=self.set_input_form.get()
        t_after=datetime.datetime.strptime(t_after, '%Y-%m-%d %H:%M:%S')
        return t_after

def set_timestamp(self):
    try:
        #入力から修正後の日時を取得する
        ct_after=creation_stamp.get_timestamp()
        mt_after=update_stamp.get_timestamp()
        at_after=access_stamp.get_timestamp()

        #ファイルパスを決める(元ファイルのコピーを使う)
        file_before = file_input_form.get()
        #拡張子(.)の位置を調べる
        s=file_before.find('.')
        file_after = file_before[0:s]+'_修正'+file_before[s:]
        #コピー
        file_copy = shutil.copy(file_before, file_after)

        #作成日時、更新日時、アクセス日時を決める。
        ctime = datetime.datetime(ct_after.year, ct_after.month, ct_after.day, ct_after.hour, ct_after.minute, ct_after.second).timestamp()
        mtime = datetime.datetime(mt_after.year, mt_after.month, mt_after.day, mt_after.hour, mt_after.minute, mt_after.second).timestamp()
        atime = datetime.datetime(at_after.year, at_after.month, at_after.day, at_after.hour, at_after.minute, at_after.second).timestamp()

        #タイムスタンプを変更
        win32_setctime.setctime(file_copy, ctime)
        os.utime(file_copy, (atime, mtime))

        whole_description['text']='修正に成功しました'
        return
    
    except:
        whole_description['text']='エラーが発生しました、最初からやり直してください'
        return
```

##コード全体

アプリ全体のコードについて以下に示す。

windows10,python3.6.4,jupyter notebookにて動作確認済み。

```python3
import os
from tkinter import *
import tkinter.filedialog
import datetime
import math
import win32_setctime
import shutil

class timestamp():
    def __init__(self,name,posx,posy):
        self.name=name
        #間隔の調整
        while(len(name)<5):
            name+='　'
            
        self.current_setting_time=Label(text=name+': 情報なし')#現在の設定時刻
        self.current_setting_time.place(x=posx,y=posy)
        
        self.set_input_form=Entry(width=25)#入力フォーム
        self.set_input_form.place(x=posx+250,y=posy)
        
    def get_timestamp(self):
        t_after=self.set_input_form.get()
        t_after=datetime.datetime.strptime(t_after, '%Y-%m-%d %H:%M:%S')
        return t_after
        
def reference(self):
    fTyp = [("","*")]#ファイル拡張子の指定
    iDir = os.path.abspath('C//')#スタート地点
    filepath = tkinter.filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir)
    #入力フォームにパスを入れる
    file_input_form.insert(END,filepath)
    return

def set_path(self):
    try:
        #作成日時
        ct_u=os.path.getctime(file_input_form.get())#UNIX表記
        ct_d=datetime.datetime.fromtimestamp(math.floor(ct_u))#日時表記
        #更新日時
        mt_u=os.path.getmtime(file_input_form.get())
        mt_d=datetime.datetime.fromtimestamp(math.floor(mt_u))
        #アクセス日時
        at_u=os.path.getatime(file_input_form.get())
        at_d=datetime.datetime.fromtimestamp(math.floor(at_u))

        #取得した日時をラベルに反映
        creation_stamp.current_setting_time['text']=creation_stamp.name+': 　'+str(ct_d)
        update_stamp.current_setting_time['text']=update_stamp.name+': 　'+str(mt_d)
        access_stamp.current_setting_time['text']=access_stamp.name+': '+str(at_d)

        #説明部分の更新
        whole_description['text']='修正後の日時(例:2000-01-01 11:22:33)を入力し、「変更」をクリックしてください'
        return
    except:
        whole_description['text']='ファイルパスが正しくありません、もう一度入力し直してください'

def set_timestamp(self):
    if whole_description['text']!='修正後の日時(例:2000-01-01 11:22:33)を入力し、「変更」をクリックしてください':
        whole_description['text']='もう一度はじめからやり直してください'
        return
    
    try:
        #入力から修正後の日時を取得する
        ct_after=creation_stamp.get_timestamp()
        mt_after=update_stamp.get_timestamp()
        at_after=access_stamp.get_timestamp()

        #ファイルパスを決める(元ファイルのコピーを使う)
        file_before = file_input_form.get()
        #拡張子(.)の位置を調べる
        s=file_before.find('.')
        file_after = file_before[0:s]+'_修正'+file_before[s:]
        #コピー
        file_copy = shutil.copy(file_before, file_after)

        #作成日時、更新日時、アクセス日時を決める。
        ctime = datetime.datetime(ct_after.year, ct_after.month, ct_after.day, ct_after.hour, ct_after.minute, ct_after.second).timestamp()
        mtime = datetime.datetime(mt_after.year, mt_after.month, mt_after.day, mt_after.hour, mt_after.minute, mt_after.second).timestamp()
        atime = datetime.datetime(at_after.year, at_after.month, at_after.day, at_after.hour, at_after.minute, at_after.second).timestamp()

        #タイムスタンプを変更
        win32_setctime.setctime(file_copy, ctime)
        os.utime(file_copy, (atime, mtime))

        whole_description['text']='修正に成功しました'
        return
    
    except:
        whole_description['text']='エラーが発生しました、最初からやり直してください'
        return
        
win=Tk()
win.title('Change_timestamp')
win.geometry('500x300')

whole_description=Label(text='画像のパスを入力し、「設定」をクリックしてください')
whole_description.pack(fill=X)#ラベルの長さを全体に広げる(packによって中央寄せ)

#ファイルpath入力フォーム
file_input_form=Entry(width=40)
file_input_form.place(x=90,y=50)
#参照ボタン
reference_button=Button(text='参照')
reference_button.bind("<Button-1>",reference)
reference_button.place(x=340,y=50)

#画像設定ボタン
image_set_button=Button(text='設定')
image_set_button.bind("<Button-1>",set_path)
image_set_button.place(x=380,y=50)

current_property=Label(text='現在のプロパティ')
current_property.place(x=40,y=110)
changed_property=Label(text='修正後のプロパティ(入力)')
changed_property.place(x=290,y=110)

#作成日時
creation_stamp=timestamp('作成日時',40,140)
#更新日時
update_stamp=timestamp('更新日時',40,170)
#アクセス日時
access_stamp=timestamp('アクセス日時',40,200)

#変更ボタン
change_button=Button(text='変更')
change_button.bind("<Button-1>",set_timestamp)
change_button.place(x=230,y=250)


win.mainloop()
```

##まとめ

今回のアプリ開発を通して、datetimeライブラリ、日付と文字列の相互変換、ファイル操作について軽く学ぶことができた。

特に、エクスプローラーからファイルパスを参照してくるプログラムは色んな所で使われるはずなので、学んでおいて損はないはず。

pythonの基本を学んだ人のステップアップとして、一度作ってみてはいかがかと思う。