見出し画像

Python 学習記録 ー 画像ファイルが含まれるフォルダーを csv ファイルに書き出すスクリプト、画像ファイル数付きで

何を作ったか

Python スクリプトを作りました。
スクリプトを実行すると、写真が入ったフォルダーのリストを csv ファイルに書き出します。
データ項目は2つ、フォルダーおよびフォルダーに含まれる画像ファイルの個数です。

なぜ作ったか

そろそろエゾビタキがやってくる。
先日、フィールドで虫を撮影しながら、「エゾビタキ、まだ来ないな」と、、、
いつ頃くるのでしたっけ?
夏の間、虫に夢中になりすぎたせいで、鳥に関するデータを思い出せません。

自分は、基本的に観察記録をつけませんが、種名でHDDの写真フォルダーを検索したら出てくるでしょう。
が、少ししかヒットしません。もっとあるはずなのに、なぜヒットしないのでしょうか。
外部HDD のインデックスが正しく作られていないのが原因かもしれません。インデックスを作り直してみたのですが、終わりそうにありませんでしたので、途中でやめました。

そうだ、Python に探してもらうことにしよう。

というわけです。


ここで紹介するのは、汎用性のあるものに作り替えたスクリプトです。
MacOS で動作します。
検索対象は、Pictures フォルダーです。

Code

# -*- coding: utf-8 -*-
# Pictures フォルダー内の画像ファイルの数をサブフォルダー毎に調べるスクリプト
# MacOS 用

import os
import re
import csv
import getpass
import unicodedata
from datetime import datetime

if __name__ == '__main__':

    # ログイン名の取得
    user = getpass.getuser()
    # 検索パスの文字列
    searchPath = '/Users/' + user + '/Pictures/'
    # 正規表現パターン、検索パス用
    repSearchPath = re.compile('^' + searchPath)
    # 正規表現パターン、画像ファイル用の拡張子のうち使っていそうなものだけ
    repFileExtension = re.compile(r'^.+\.(ORF|NEF|JPE?G|TIFF?|PNG|BMP|PI?CT)$', re.IGNORECASE)

    # データの入れ物を用意する。見出し用の文字を1個目のデータとして追加
    outData = [['検索パス:' + searchPath, '画像ファイル数']]

    # フォルダーとファイルに関する情報をゲットする
    fileList = os.walk(searchPath)

    # フォルダーとそれに含まれるファイルについての処理
    for folder, subFolders, files in fileList:
        # フォルダー名の濁音や半濁音が別の文字になるので、NFC で正規化
        myFolder = unicodedata.normalize('NFC', folder)
        # 検索フォルダーは、全てのフォルダーに共通でついてくるので取り除く
        mySubFolder = re.sub(repSearchPath, '', myFolder)
        # 検索フォルダー直下の場合は、フォルダー名に適当な説明を入れる
        if mySubFolder == '':
            mySubFolder = '〜 検索パス直下 〜'
        # ファイルのカウンターをリセットする
        file_count = 0

        for file in files:
            # 拡張子が、画像ファイルのものだったらカウントする。
            if repFileExtension.match(file):
                file_count += 1
        else:  # for ループを終了するときの処理
            # 画像ファイルがあったら、サブフォルダー名とファイル数を追加する
            if file_count > 0:
                outData.append([mySubFolder, file_count])

    # csv ファイルの名称に付ける日時を用意
    now = datetime.now().strftime('%Y%m%d%H%M%S')
    # csv ファイルに保存する
    with open('./PictureFolders_' + now + '.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerows(outData)
        print(f.read)


画像ファイルの数を調べるかのようなコメントを添えましたが、ファイル数のカウントはおまけで、フォルダーの一覧を取得するのが第一の目的です。

ディスクの名称は固定です。
外部ディスクはそのまま名前を入れれば問題ないのですが、内蔵ディスクの場合は、パスにユーザー名が入ります。公開するにあたってユーザー名固定では困ると思いました。そこで、スクリプトの中でユーザー名をゲットするようにしました。
きっと難しいことをやらないといけないんだろうなと不安に思いながらネットで調べたら、簡単でした。

user = getpass.getuser()

これでゲットしたログインユーザー名を、パスに加えればユーザーの違いは吸収できるというわけです。

searchPath = '/Users/' + user + '/Pictures/'

ここでは、Pictures フォルダーに固定していますが、これを変えれば好きな場所を検索できると思います。

あとの方でフォルダー名をゲットするのですが、フルパスの形です。ルートからPicture フォルダーまでの文字列が、全てのフォルダーに付いてしまいます。邪魔なので、取り除くことにしました。

repSearchPath = re.compile('^' + searchPath)

と、正規表現のパターンをコンパイルし、re.sub() を使って冗長な部分を空白に置き換えます。つまり削除するということです。

mySubFolder = re.sub(repSearchPath, '', myFolder)

無駄に正規表現なんか使いました。
次の文は、拡張子の正規表現パターンです。対象の拡張子を変えるときは、ここをいじれば良いです。

repFileExtension = re.compile(r'^.+\.(ORF|NEF|JPE?G|TIFF?|PNG|BMP|PI?CT)$', re.IGNORECASE)

ちなみに、JPE?G とは、JPG または JPEG ということです。
re.IGNORECASE とすることで、jpg や jpeg にも対応します。

参考

フォルダーのリストを作るのに、os.walk() というのを使ってみました。

fileList = os.walk(searchPath)

再帰的だとかの難しい話なしで、階層の深いところまで潜って報告してくれますので楽です。

取れるデータは、全てのフォルダーと、それに含まれるサブフォルダーおよびファイルです。generator 形式のデータでした。なんですかね、generator って。
データの1個1個は、フォルダー、サブフォルダー、ファイルで構成されています。という捉え方で良いのでしょうか。
自分の用途では、サブフォルダーのデータは必要ありませんでした。

最初、これ↓を見て扱い方がわかりませんでした。人が書いたサンプルをパクってきたものですから、まったく理解不足です。

for folder, subFolders, files in fileList:

os.walk() でゲットしたデータのそれぞれが、一つの folder とそれに含まれる subFolders(複数のサブフォルダー)および files (複数のファイル)ということなのですが、、、
色々いじってみて、files に含まれる個々のファイルについて調べれば良いとわかりました。

for file in files:

さて、最後にデータを書き込む必要があります。

else:

for ループを終了するときの処理です。終了したときの処理ですか?
for 文と同じインデントで、else: と書けば良いのでした。
これはどこかで聞いて知っていましたので、すんなりと各フォルダーの最後の処理ができました。

はあ〜、何を言っているか、わかりませんかね。

また、os.walk() を使ってフォルダー名を拾ったときに、例えば
「ホシメハナアフ゛」
のように、カタカナと濁点がバラバラになる現象に悩まされました。
「ブ」が「フ゛」になってしまうのです。
すごく簡単にできそうなところに、恐ろしい罠がしかけられていました。

調べると、Unicode 文字の問題とのこと。NFC で正規化する必要があると知りました。

myFolder = unicodedata.normalize('NFC', folder)

これに気づいたのは、PyCharm でデバッグしているときでした。「ブ」だと信じていた文字が、デバッガーでは「フ゛」と表示されていました。また、OS上は、見た目が同じでも違う「ブ」が存在していたときもありました。「ブ」のような濁点がついたカナがあるとヒットしないのです。画面に表示された文字を見ると「ブ」に見えるのに、自分がタイプした「ブ」とは違う文字のようでした。
os.walk() を使うのをやめようかなとまで考えました。
でも、解決しました。解決策がみつかって良かったです。

ドキュメントを読んでもよくわかりませんね。良い説明がどこかにあると思います。
HFS+ という古いファイルシステムの場合に、そのようなことがあるという話もあって、実際HDDを確認したらHFS+でした。

ここまでできたら、あとは、csv ファイルに書き出すだけです。

話は戻りますが、データは、outData という名前の「リスト」にしました。
最初、次のようにしていました。

outData = []
outData.append(['検索パス:' + searchPath, '画像ファイル数'])

しかし、PyCharm はこれを「書き方が悪い」みたいに言うのです。そこで、直してもらいました。PyCharm に。

outData = [['検索パス:' + searchPath, '画像ファイル数']]

あ、こういうことね。

それから、PyCharm は、subfolder をTypo、つまりタイプミスであると指摘しました。sub-folder が正しいと思っているのでしょうか。この問題は、subFolder に変えてクリアしました。

ファイル

いつからか note の記事にファイルを添付できるようになりましたので、試しに添付してみます。無駄かもしれませんが。


試行錯誤の頃ー検索語を入力してフォルダー検索

このスクリプトを作り始めた頃、ダイアログを表示して検索語を入力したら、いろいろな条件で検索できて便利かもと思って作ったものがこちら↓です。
Tkinter というライブラリの簡単な使い方を知りました。

# -*- coding: utf-8 -*-
# フィルダー名を検索、完全一致

import os
import csv
from datetime import datetime
import unicodedata
import tkinter as tk
import tkinter.simpledialog as simpledialog

if __name__ == '__main__':

    # キーワード検索
    # フォルダーだけ

    tk.Tk().withdraw()
    keyword = simpledialog.askstring('検索語入力', '検索語を入力してください')

    outData = []

    # 同じフォルダーにあるフォルダーの一覧を再起的に取得

    file_list = os.walk('./')

    for folder, subfolders, files in file_list:
        for my_folder in subfolders:
            my_folder = unicodedata.normalize('NFC', my_folder)
            if my_folder == keyword:
                print(folder)
                outData.append([folder, my_folder])

# csv ファイルの名称に付ける日時を用意
    now = datetime.now().strftime('%Y%m%d%H%M%S')
# csv ファイルに保存する
    with open('./Folders_' + now + '.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerows(outData)
        print(f.read)

スクリプトを置いたフォルダーとそのサブフォルダーを検索します。
これを作った後で、いちいち検索語を入力するのが面倒に感じました。そこで、検索するのはやめてとりあえず全部拾うように変えました。
写真用の外部HDDは2台ありますので、スクリプトを置いたフォルダーだけを対象にする方法ですと、データが2個できてしまいます。それらを別々に扱うか、そうでなかったらファイルを統合する必要があります。
そこで、最終的には、一度に2台を検索して一つの csv ファイルにまとめて出力するように作りました。


自分用に作ったスクリプトは、外部HDD固定で作りました。外部HDDの名前は人それぞれでしょうから、そのままでは使えません。
Tkinter を使えば、ファイルを開くダイアログを表示して場所を選択することもできます。場所が決まっているのに、いちいちダイアログで選択するのは無駄に思うから、やっていないのです。
場所を変えたかったらスクリプトを書き換えれば良いのです。

MacOS で目的のファイルやフォルダーのパスを調べる方法ですが、対象を右クリックしてコンテキストメニュー?を表示した状態で、optionキーを押します。「コピー」だったところが、「…パス名をコピー」みたいな感じに変わりますので、それを選択します。パス名がクリップボードにコピーされますので、あとは適当なところにペーストするだけです。スクリプトにペーストした文字列は、そのままで正しく扱われます。

csv ファイルを LibreOffice Calc で開いて、単語で絞り込むなどして便利に使っています。

t.koba

この記事が気に入ったらサポートをしてみませんか?