よちよちpython

独習 python/Qpython/Pydroid3/termux

【stat とdatetime】ローカルやWeb上のファイル最終更新日時を取得する

Web上からファイルをダウンロード保存するプログラムを書くようなとき、「以前にダウンロードしたファイルと同じなら、ダウンロードしない」ようにしといた方がよいのではと考えた時のメモ。

なにしろプログラムを作る際にエラーばかり吐く。書き直す度にダウンロードしていては時間や通信コスト、サーバーへの負担等になる。極力重複は避けたい。
色んなやり方があるのでしょうけど、ファイルの最終更新日時を比較すれば出来ると思った。最終更新日時を「タイムスタンプ」とも言そうですが、その取得方法をみていきます。



【実行環境】
Android
Termux
Python3.9



目次




statコマンド


UNIX系OSにはstatというコマンドが用意されている(Windowsはよく調べてません)。
ファイル名をコマンドライン引数にすると、そのファイルの最終更新日時が表示されるようだ。試してみよう。

$ touch test.txt ←空のファイル作成
$ stat test.txt    ←最終更新日時の取得
  File: test.txt
  Size: 0               Blocks: 8          IO Block: 4096   regular empty file
Device: 1ch/28d Inode: 2859832     Links: 1
Access: (0660/-rw-rw----)  Uid: (    0/    root)   Gid: ( 9997/everybody)
Access: 2021-01-13 18:46:24.912620155 +0900
Modify: 2021-01-13 18:46:24.912620155 +0900
Change: 2021-01-13 18:46:24.912620155 +0900
 Birth: -

コマンド一発で出来て簡単。
取得したものには「Access」「Modify」「Change」と三種類の日時が載っている。全て同じ日時になっている。
ファイルを書き換えると下の2つの日時が変わる。書き換えて確認する。

$ echo hello > test.txt   ←「hello」をファイルに上書き
$ stat test.txt   ←最終更新日時の取得
File: test.txt
  Size: 6               Blocks: 16         IO Block: 4096   regular file
Device: 1ch/28d Inode: 2859832     Links: 1
Access: (0660/-rw-rw----)  Uid: (    0/    root)   Gid: ( 9997/everybody)
Access: 2021-01-13 18:46:24.912620155 +0900
Modify: 2021-01-13 18:48:01.982620118 +0900
Change: 2021-01-13 18:48:01.982620118 +0900
 Birth: -

書き換える前と、ModifyとChangeが変わっている。



WindowsコマンドプロンプトPowerShellで行う際は


WindowsのコマンドはUNIXコマンドと違うので、上記の方法は使えない。参考のサイトをリンクしておきます。
コマンドプロンプトでファイルの更新日時をミリ秒単位まで取得する - Qiita



Pythonでローカルファイルの最終更新日時を取得


Pythonでローカルに保存したファイルの最終更新日時を取得するには次のようにする。

>> import os
>> os.stat("test.txt")

os.stat_result(st_mode=33200, st_ino=2859832, st_dev=28, st_nlink=1, st_uid=0, st_gid=9997, st_size=6, st_atime=1610531184, st_mtime=1610531281, st_ctime=1610531281)

ワーっと出てきますが、最後の3つ、「st_atime」「st_mtime」「st_ctime」がそれぞれ「Access」「Modify」「Change」に対応している。
しかしそれらは、「1610531184」等と日時ではなく数字になっています。この数字を「エポック秒 = UNIX時刻」という。1970年1月1日0時0分0秒を基準として何秒経過したかを表すのだとか。

st_mtimeなど個別の属性の取り出し


上でワーっと出てきた出力のうち、st_mtimeだけを取り出してみます。これはファイルのヘッダー最終更新日時を表す。ファイルを書き換えるとこのデータも書き換えられる。

>> os.stat("test.txt").st_mtime

1610531281.98262

小数点のついたエポック秒が出てきた。

エポック秒の型を調べる


>> type(os.stat("test.txt").st_mtime)

float

float型の数値のようです。
数値で言われても日時が分かりにくいので、次ではそれが分かるように変換します。



エポック秒を日時に変換 datetime.fromtimestamp()


標準ライブラリのdatetimeを使い、float型のエポック秒→datetime型の日時 に変換します。

datetime.fromtimestamp(エポック秒)を使います。

>> from datetime import datetime

>> datetime.fromtimestamp(os.stat("test.txt").st_mtime)

datetime.datetime(2021, 1, 13, 18, 48, 1, 982620)

日時っぽいものに変換、出力されました。

datetime.fromtimestamp()の型を確認


>> print(type(datetime.fromtimestamp(os.stat("test.txt").st_mtime)))

<class 'datetime.datetime'>

「float型のエポック秒」が、「datetime.datetime型」に変換されたようです。


エポック秒を引き算する


特に意味はないが、
ファイルを更新した日時「(Modify) st_time」から、ファイルの作成日時「(Access) st_atime」を引き算すると変更までの時間差が出せるかと思いますので、やってみます。
長いので変数に置き換えてやります。

>> ts_a = os.stat("test.txt").st_atime

>> ts_m = os.stat("test.txt").st_mtime

>> print(ts_m - ts_a)

97.0699999332428

ファイルを作った97秒後に変換したみたいです。



Web上のファイルの最終更新日時を取得


Web上のファイルの最終更新日時も取得出来るようです。
NHKサイトの新型コロナのデータファイルを使って実験してみます。
元のサイト
新型コロナウイルス データで見る感染状況一覧|NHK特設サイト

都道府県ごとの感染者数の推移 東京都」のCSVデータファイル
https://www3.nhk.or.jp/n-data/opendata/coronavirus/nhk_news_covid19_prefectures_daily_data.csv



ここからは、外部ライブラリのrequestsを使いますのでインストールしておきます。

$ pip install requests



では、上にリンクした「都道府県ごとの感染者数の推移 東京都」CSVデータファイルの最終更新日時を取得してみます。

>> import requests

>> url = "https://www3.nhk.or.jp/n-data/opendata/coronavirus/nhk_news_covid19_prefectures_daily_data.csv"

>> res = requests.head(url)

>> res.headers["Last-Modified"]

'Tue, 12 Jan 2021 16:10:09 GMT'

Web上のCSVファイルの最終更新日時らしいものが取得されました。

下から2行目のres.headers["Last-Modified"]のカッコは[]を使います。

res.headers("Last-Modified")と書いてしまうと

TypeError: 'CaseInsensitiveDict' object is not callable

エラーが出ますので注意。

res.headers["Last-Modified"]で更新日時が取得できるのはなぜか。下記の方法で確認します。

>> res.headers

{'Content-Type': 'application/x-excel', 'Content-Length': '585438', 'Server': 'openresty', 'Last-Modified': 'Tue, 12 Jan 2021 16:10:09 GMT', 'ETag': '"be97eb0e7b8410186a20961e53573d42"', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept', 'Cache-Control': 'max-age=59', 'Date': 'Wed, 13 Jan 2021 11:01:01 GMT', 'Connection': 'keep-alive'}

「requests.head(url)」でWeb上のurlのヘッダー情報を取得し、その属性「headers」を取得した物が上記です。
辞書型のデータ。そのキー「"Last-Modified"」を取得したのでバリューの日時データが返された。



型の確認もしておきます。

>> print(type(res.headers["Last-Modified"]))

<class 'str'>

文字型strです。これをエポック秒のfloat型に出来れば、ローカルファイルとの比較が出来そうです。



str型の日時データをdatetime型に変換する


datetime.datetime.strptime(文字型の日時, 表示フォーマット)で変換できます。

>> datetime.strptime(res.headers['Last-Modified'], "%a, %d %b %Y %H:%M:%S GMT")

datetime.datetime(2021, 1, 12, 16, 10, 9)

datetime型の日時に変換できました。



2つのdatetime型の日時を比較する


上の方で「ローカルファイルtest.txtの更新日時エポック秒をdatetime型日時に変換」しました。おさらい

>> datetime.fromtimestamp(os.stat("test.txt").st_mtime)

datetime.datetime(2021, 1, 13, 18, 48, 1, 982620)

これと、今さっきNHKCSVファイルのstr型日時をdatetime型に変換した物とで比較してみます。
表示のフォーマットが違うようですが無視します。

仮定として、ローカルファイルtest.txtNHKからダウンロードしたものだとします。
もし更新日時でローカルの方が最近なら(大きければ)、つまりWeb上のファイル更新日時より後にダウンロードされたものであれば、ローカルのは最新データと言えます。
TrueFalseが返るように書きます。

>> import os
>> import requests
>> from datetime import datetime

# ローカルファイルの最終更新日時
>> local_st = datetime.fromtimestamp(os.stat("test.txt").st_mtime)

# datetime.datetime(2021, 1, 13, 18, 48, 1, 982620)

# Web上ファイルの最終更新日時
>> web_st = datetime.strptime(res.headers["Last-Modified"], "%a, %d %b %Y %H:%M:%S GMT")

# datetime.datetime(2021, 1, 12, 16, 10, 9)


# 比較
>> print(web_st < local_st)

# True

最終更新日時は、

  • ローカル : 2021/1/13
  • Web上 : 2021/1/12

になっている。ローカルの方が大きいのでTrueが返されました。最新データを入手済みということになります。
これで「最新ならダウンロードしない」という風な制御が出来そうです。

datetime型同士であれば、表示フォーマットは関係なく比較できるっぽい。



参考




おわりに


ブラウザにはダウンロードの重複をさける一時的なキャッシュと呼ばれるファイルを保存する仕組みがあるようです。キャッシュはファイルのダウンロードだけでなく、計算の重複を避ける仕組みもあるとか。常日頃ポンコツプログラムを書いてはエラーの連続です。大した計算の必要がないプログラムのときはこれで良くても、時間の掛かる機械学習のようなプログラムの場合には大変。こういうの考えるひと頭いいなぁ。



以上です。