naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Pythonでディレクトリのサイズを取得する

プログラミング関連 osモジュール Python 約10日前
2017年8月6日22:57
今回はPythonで、カレントのディレクトリ・ファイルのサイズを取得し、サイズが大きい順に表示してみます。

まず、このようにディレクトリやファイルがたくさんあり...


実行すると、以下のように表示されます。


ソースコード



"""ファイル・ディレクトリのサイズを取得するモジュール."""
import os


SUFFIXES = {
    1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
    1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
}


def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.

    http://diveintopython3-ja.rdy.jp/your-first-python-program.html

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''
    if size < 0:
        raise ValueError('number must be non-negative')

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)

    raise ValueError('number too large')


def get_size(path):
    """pathのサイズを返す(ディレクトリ対応版).

    基準となるパスを受け取り、中のディレクトリ・ファイルを全て足したサイズを返す

    引数:
        path: 基準となるファイル・ディレクトリのパス

    """
    if os.path.isfile(path):
        return os.path.getsize(path)
    else:
        for dirpath, dirnames, filenames in os.walk(path):

            # 現在のディレクトリ内の全てのファイルパス
            files_path = [os.path.join(dirpath, file) for file in filenames]
            # それらのサイズを測り、sumで合計を出す
            files_size = sum([os.path.getsize(path)
                              for path in files_path if os.path.exists(path)])

            # 現在のディレクトリ内の全てのディレクトリパス
            dirs_path = [os.path.join(dirpath, dr) for dr in dirnames]
            # get_size(この関数)にディレクトリのパスを渡していき、改めてサイズを測り、合計を出す
            dirs_size = sum([get_size(path)
                             for path in dirs_path if os.path.exists(path)])

            return files_size + dirs_size


def main():
    """メイン."""
    all_size = []
    # カレント以下のファイル・ディレクトリを全て取得し、サイズを測る
    with os.scandir(os.getcwd()) as it:
        for entry in it:
            size = get_size(entry.path)
            all_size.append((entry.path, size))

    # サイズ順でソート
    for name, size in sorted(all_size, key=lambda x: x[1], reverse=True):
        print(name, approximate_size(size))


if __name__ == '__main__':
    main()



まず説明しておくこととして、os.listdirは単純にファイル・ディレクトリのパスを返しますが、3.5で追加されたos.scandirはそれに加えファイル属性も返してくれます。
今回は特にファイル属性の参照はしないのですが、便利そうなのでscandirを使います。
# os.listdir()
os.scandir()


os.listdirとは違い、文字列のパスではなくos.DirEntryというオブジェクトをyieldで返してきます。
entry.nameで名前が、entry.pathでパスにアクセスできます。
for entry in os.scandir():
    print(entry.name, entry.path)  # ファイル・ディレクトリ名とパスを表示



listdirもそうですが、scandirも引数を指定しなければカレントディレクトリを対象にします。
引数がない場合のデフォルトは「'.'」となるのですが、これだと相対パスでの表示になってしまいます。
# os.scandir('.')と同義
for entry in os.scandir():
    print(entry.path)


パスは以下のような感じになります。
./__pycache__
./main.py
./__init__.py


フルパスで表示されるようにしたいと思いました。現在のカレントディレクトリの絶対パスを文字列で得る必要があります。
今回はos.getcwd()でカレントディレクトリの絶対パスを得ています。
for entry in os.scandir(os.getcwd()):
    print(entry.path)


こちらだと、フルパスになります。
/home/narito/test2/__pycache__
/home/narito/test2/main.py
/home/narito/test2/__init__.py



中身のソースコードを解説していきます。
ディレクトリなんかはサイズを取得するのに1手間いるので、パスをgetsize関数に渡し、サイズを取得します。
サイズを取得したら、(パス, サイズ)というタプルをリストに格納します。
    all_size = []
    # カレント以下のファイル・ディレクトリを全て取得し、サイズを測る
    with os.scandir(os.getcwd()) as it:
        for entry in it:
            size = get_size(entry.path)
            all_size.append((entry.path, size))


リストに全て格納したら、サイズ順にソートしてprintで表示します。
approximate_sizeは、ファイルサイズを人に見やすい形に変換する関数です。
これが大まかな流れです。
    # サイズ順でソート
    for name, size in sorted(all_size, key=lambda x: x[1], reverse=True):
        print(name, approximate_size(size))



これがそのapproximate_size関数です。
http://diveintopython3-ja.rdy.jp/your-first-python-program.htmlに載っているコードで、それをそのまま使ってみました。
def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.

    http://diveintopython3-ja.rdy.jp/your-first-python-program.html

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''
    if size < 0:
        raise ValueError('number must be non-negative')

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)

    raise ValueError('number too large')



こちらが、ファイル・ディレクトリサイズを取得するget_size関数です。
def get_size(path):
    """pathのサイズを返す(ディレクトリ対応版).

    基準となるパスを受け取り、中のディレクトリ・ファイルを全て足したサイズを返す

    引数:
        path: 基準となるファイル・ディレクトリのパス

    """
    if os.path.isfile(path):
        return os.path.getsize(path)
    else:
        for dirpath, dirnames, filenames in os.walk(path):

            # 現在のディレクトリ内の全てのファイルパス
            files_path = [os.path.join(dirpath, file) for file in filenames]
            # それらのサイズを測り、sumで合計を出す
            files_size = sum([os.path.getsize(path)
                              for path in files_path if os.path.exists(path)])

            # 現在のディレクトリ内の全てのディレクトリパス
            dirs_path = [os.path.join(dirpath, dr) for dr in dirnames]
            # get_size(この関数)にディレクトリのパスを渡していき、改めてサイズを測り、合計を出す
            dirs_size = sum([get_size(path)
                             for path in dirs_path if os.path.exists(path)])

            return files_size + dirs_size


もらったパスがファイルならば、os.path.getsize()でそのままファイルサイズを返します。
if os.path.isfile(path):
    return os.path.getsize(path)


ディレクトリならば、少し処理をします。os.walkを使い、パス内のファイル・ディレクトリを再帰的に取得していきます。
https://torina.top/detail/267/で、os.walkの簡単な使い方を紹介しています。
else:
    for dirpath, dirnames, filenames in os.walk(path):


まず、以下でディレクトリ内の全てのファイルのパスを集めます。
[path1, path2, path3, ...]のように、ファイルのフルパスが詰まったリストが作成されます。
# 現在のディレクトリ内の全てのファイルパス
files_path = [os.path.join(dirpath, file) for file in filenames]



上で作ったリストからファイルパスを一つづつ取り出し、os.path.getsizeでサイズを取得します。
この際に、os.path.exists(path)を満たすものだけサイズを取得します。これは主に、壊れたシンボリックリンクがあった場合に無視するための記述です。
[1000, 300, 10, 400, ...]のようなサイズが詰まったリストが出来上がり、それに対してsumで合計を取得します。
# それらのサイズを測り、sumで合計を出す
files_size = sum([os.path.getsize(path) for path in files_path if os.path.exists(path)])



次はディレクトリです。ファイルと流れは同じです。
# 現在のディレクトリ内の全てのディレクトリパス
dirs_path = [os.path.join(dirpath, dr) for dr in dirnames]


ファイルの合計サイズの取得と違うのは、os.path.getsizeではなく、getsize関数(この関数)を呼び出していることです。
こうすることでディレクトリのサイズを再帰的に調べていきます。
# get_size(この関数)にディレクトリのパスを渡していき、改めてサイズを測り、合計を出す
dirs_size = sum([get_size(path) for path in dirs_path if os.path.exists(path)])


最後はファイルサイズとディレクトリのサイズを足すだけ!
return files_size + dirs_size



こういったスクリプトはすぐに使えるようにしておくと便利です。
例えばLinuxならば、/usr/bin などにpysize のようなファイル名で保存し、chmod 755 等で実行権限を与えておきます。
以下のように、Shebangもファイル先頭に書いておきましょう。(書き方はいくつかあります)
#!/usr/bin/python3.6

こうすることで、いつでも「pysize」 で呼び出すことができます。