Python Tips: Mac の .DS_Store を除外して zip ファイルを作成したい

Python で、 MacOS で自動作成される .DS_Store ファイルを除外した形で zip ファイルを作成する方法をご紹介します。

まずは .DS_Store とは何ぞやというそもそものところを確認した上で具体的な方法を見ていきましょう。

.DS_Store とは

MacOS を使っている方にはおなじみですが、 .DS_Store ファイルとは、フォルダのメタ情報を格納したファイルです。 MacOS に特有のもので、 GUI 関連の情報を格納しているようです。具体的には、アイコンの位置、背景画像などの情報が含まれるようです。

.DS_Store は Desktop Services Store の略で DS Store だそうです。

英語の Wikipedia の説明を一部抜粋します。

In the Apple macOS operating system, .DS_Store is a file that stores custom attributes of its containing folder, such as the position of icons or the choice of a background image. The name is an abbreviation of Desktop Services Store, reflecting its purpose. It is created and maintained by the Finder application in every folder, and has functions similar to the file desktop.ini in Microsoft Windows.

意訳:

Apple の OS macOS において、 .DS_Store はフォルダのカスタム属性を格納するファイルです。アイコンの位置や背景画像などの情報を格納しています。名前は Desktop Services Store の省略形です。 Finder アプリが各フォルダに対して作成・管理しており、 Microsoft Windows の desktop.ini に似た機能を持っています。

使用するもの

この .DS_Store ファイルを除外した形でディレクトリの zip ファイルを作成する方法ですが、いくつかのアプローチが考えられます。

今回は標準ライブラリのみを使った方法のうち、 zip ファイルを扱うためのライブラリ zipfile と Python 3.4 で導入された pathlib.Path を使う方法をご紹介します。

  • zipfile
  • pathlib.Path

コード

早速ですが、実際のコードを見てみましょう。

import zipfile
from pathlib import Path
from typing import Iterable, List

settings = {
    'verbose': False,
}

def compress_dir(zip_out: Path, directory: Path, skipped_names: Iterable[str]):
    '''指定されたディレクトリを指定された名前で zip ファイルに圧縮する
    '''
    if zip_out.exists():
        message = 'Specified zip file already exists: "{}".'.format(zip_out)
        raise CompressionError(message)

    with zipfile.ZipFile(str(zip_out.absolute()), 'w') as z:
        if settings['verbose']:
            print('Zip file has been initialized: "{}".'.format(z.filename))
        for f in iter_all_files(directory):
            if f.name in skipped_names:
                if settings['verbose']:
                    print('Skipped: "{}".'.format(f))
                continue

            z.write(f)
            if settings['verbose']:
                print('Added: "{}".'.format(f))

def iter_all_files(directory: Path) -> List[Path]:
    '''指定されたディレクトリの下にあるすべてのファイル( Path )を下層のものも含めて返すイテレータを返す
    '''
    if not directory.is_dir():
        message = 'Specified directory is not found: "{}".'.format(directory)
        raise CompressionError(message)

    return directory.glob('**/*.*')

class CompressionError(Exception):
    '''zip 圧縮プロセスで発生するエラーを表すカスタム例外
    '''
    pass

この関数 compress_dir() は、指定されたディレクトリ diretory を zip ファイル zip_out として圧縮する関数です。第 3 引数 skipped_names で指定された名前のファイルはスキップします。

これを次のように使用すると、 .DS_Store ファイルを除外した形でディレクトリ「圧縮対象ディレクトリ」を zip 圧縮することができます。

zip_file = Path('サンプル.zip')
directory = Path('圧縮対象ディレクトリ')
skipped_names = ('.DS_Store', )

try:
    compress_dir(zip_file, directory, skipped_names)
except CompressionError as e:
    # TODO 文脈によってより適切な例外処理に差し替える
    raise e

以下にポイントをいくつか取り上げてみます。

zipfile.ZipFile は Python 3.2 以降で context manager として使えるようになっているので、 with ... as ... 構文で使用しています。

カスタム例外の CompressionError は、次の 2 つのケースで上がるようになっています。

  • zip ファイルがすでに存在するとき
  • 指定されたディレクトリが存在しないとき

完全なスクリプトのサンプル

この compress_dir() を使ってスクリプトを作った場合のサンプルを GitHub Gist に置いたので、興味のある方はご覧になってみてください。 argparse を使って、コマンドラインの引数、オプション引数を受け付けるようにしています。

以上、 Python で Mac の .DS_Store を除外して zip ファイルを作成する方法についてでした。

この記事に興味のある方は次の記事も参考になるかもしれません。

zip ファイルの zip ではなくて Python の組み込み関数の zip() の方に興味のある方は次の記事などが参考になるかもしれません。