Pythonでファイル・ディレクトリをコピーするshutil.copy, copytree

Posted: | Tags: Python, ファイル処理

Pythonでファイルやディレクトリ(フォルダ)をコピーするにはshutil.copy(), shutil.copy2(), shutil.copytree()を使う。

コピーではなく移動や削除をしたい場合は以下の記事を参照。

本記事ではシンボリックリンクの扱いなどの細かい仕様には触れない。詳細は公式ドキュメントを参照。

本記事のサンプルコードでは以下のようにshutilモジュールをインポートしている。標準ライブラリに含まれているので追加のインストールは不要。

import shutil

ファイルをコピー: shutil.copy(), copy2()

ファイルをコピーするにはshutil.copy()shutil.copy2()を使う。

使い方はどちらも同じだが、shutil.copy()はメタデータ(作成日時・更新日時など)をコピーせず、shutil.copy2()はメタデータをできる限りコピーするという違いがある。

基本的な使い方

shutil.copy()の第一引数にコピー元のファイルのパス、第二引数にコピー先のディレクトリまたはファイルのパスを指定する。コピーによって作成されたファイルのパスが返される。

パスはパス文字列やpathlib.Pathなどのpath-like objectで指定する。パス文字列でのディレクトリ指定時の末尾の区切り文字(/)はあってもなくてもよい。

以下のファイル・ディレクトリ構造を例とする。

temp/
├── dir1/
├── dir2/
│   ├── file.txt
│   └── file2.txt
└── file.txt

第二引数に既存のディレクトリを指定すると、そのディレクトリにファイルがコピーされる。同名のファイルが既に存在している場合は上書きする。

new_path = shutil.copy('temp/file.txt', 'temp/dir1')
print(new_path)
# temp/dir1/file.txt

new_path = shutil.copy('temp/file.txt', 'temp/dir2')
print(new_path)
# temp/dir2/file.txt

第二引数に新規のパスを指定すると、そのファイル名でコピーされる。存在しない中間ディレクトリを含むとエラー。あらかじめ直上のディレクトリまで作成しておく必要がある。

new_path = shutil.copy('temp/file.txt', 'temp/dir1/file2.txt')
print(new_path)
# temp/dir1/file2.txt

# new_path = shutil.copy('temp/file.txt', 'temp/dir2/new_dir/file2.txt')
# FileNotFoundError: [Errno 2] No such file or directory: 'temp/dir2/new_dir/file2.txt'

なお、第二引数に新規のディレクトリを指定しようとして例えばtemp/dir3と指定すると、dir3というファイルとしてコピーされるので注意。temp/dir3/と指定するとエラー。

第二引数に既存のファイルを指定すると上書きする。

new_path = shutil.copy('temp/file.txt', 'temp/dir2/file2.txt')
print(new_path)
# temp/dir2/file2.txt

結果は以下の通り。第一引数にコピー元として指定したtemp/file.txtの内容が他のファイルにコピー・上書きされている。

temp/
├── dir1/
│   ├── file.txt
│   └── file2.txt
├── dir2/
│   ├── file.txt
│   └── file2.txt
└── file.txt

shutil.copy()とcopy2()の違い

shutil.copy2()の使い方はshutil.copy()と同じ。メタデータ(作成日時・更新日時など)の扱いのみが異なる。

shutil.copy()はメタデータをコピーしない。例えば、コピーしたファイルの更新日時はコピーした時刻となる。os.path.getmtime()で更新日時を取得すると、コピー元とコピー先で異なっていることが確認できる。

import os

new_path = shutil.copy('temp/file.txt', 'temp/file_copy.txt')
print(new_path)
# temp/file_copy.txt

print(os.path.getmtime('temp/file.txt') == os.path.getmtime('temp/file_copy.txt'))
# False

一方、shutil.copy2()はメタデータをできる限りコピーする。コピー元とコピー先の更新日時が同じ。

new_path = shutil.copy2('temp/file.txt', 'temp/file_copy2.txt')
print(new_path)
# temp/file_copy2.txt

print(os.path.getmtime('temp/file.txt') == os.path.getmtime('temp/file_copy2.txt'))
# True

なお、shutil.copy2()でもすべてのメタデータが保持されるわけではないので注意。

警告: 高水準のファイルコピー関数 (shutil.copy(), shutil.copy2()) でも、ファイルのメタデータの全てをコピーすることはできません。

POSIXプラットフォームでは、これはACLやファイルのオーナー、グループが失われることを意味しています。 Mac OSでは、リソースフォーク(resource fork)やその他のメタデータが利用されません。これは、リソースが失われ、ファイルタイプや生成者コード(creator code)が正しくなくなることを意味しています。 Windowsでは、ファイルオーナー、ACL、代替データストリームがコピーされません。 shutil --- 高水準のファイル操作 — Python 3.11.4 ドキュメント

ディレクトリ(フォルダ)をコピー: shutil.copytree()

基本的な使い方

ディレクトリを配下のファイルやサブディレクトリも含めて再帰的にコピーするにはshutil.copytree()を使う。

第一引数にコピー元のディレクトリのパス、第二引数にコピー先のディレクトリのパスを指定する。コピー先のディレクトリのパスが返される。

以下のファイル・ディレクトリ構造を例とする。

temp/
└── dir1/
    ├── file.txt
    └── subdir/
        └── file2.txt

第二引数に新規のパス(存在しないパス)を指定すると、中間ディレクトリも含めたディレクトリが生成され、中身がコピーされる。

new_path = shutil.copytree('temp/dir1', 'temp/dir2/new_dir')
print(new_path)
# temp/dir2/new_dir
temp/
├── dir1/
│   ├── file.txt
│   └── subdir/
│       └── file2.txt
└── dir2/
    └── new_dir/
        ├── file.txt
        └── subdir/
            └── file2.txt

第二引数に既存のディレクトリを指定するとデフォルトではエラーになる。次に説明するdirs_exist_ok引数を使う。

# new_path = shutil.copytree('temp/dir1', 'temp/dir2/new_dir')
# FileExistsError: [Errno 17] File exists: 'temp/dir2/new_dir'

既存のディレクトリにコピー: 引数dirs_exist_ok

上述のように、第二引数に既存のディレクトリを指定するとデフォルトではエラーになる。

引数dirs_exist_okTrueとするとエラーにならない。コピー先に同名のファイルがある場合はコピー元のファイルで上書きされる。

new_path = shutil.copytree('temp/dir1', 'temp/dir2/new_dir', dirs_exist_ok=True)
print(new_path)
# temp/dir2/new_dir

コピー関数を指定: 引数copy_function

コピーに使用する関数を引数copy_functionで指定できる。

デフォルトは上述のshutil.copy2()が使われており、メタデータができる限り保持される。メタデータをコピーしたくない場合はshutil.copy()を指定すればよい。

import os

new_path = shutil.copytree('temp/dir1', 'temp/dir_copy2')
print(new_path)
# temp/dir_copy2

print(os.path.getmtime('temp/dir1/file.txt') == os.path.getmtime('temp/dir_copy2/file.txt'))
# True

new_path = shutil.copytree('temp/dir1', 'temp/dir_copy', copy_function=shutil.copy)
print(new_path)
# temp/dir_copy

print(os.path.getmtime('temp/dir1/file.txt') == os.path.getmtime('temp/dir_copy/file.txt'))
# False

無視するファイル・ディレクトリを指定: 引数ignore

引数ignoreshutil.ignore_patterns()を指定すると、特定のファイルやディレクトリをコピー対象から除外できる。

shutil.ignore_patterns()にはglob形式のパターンを複数指定できる。ワイルドカード*などを使用可能。指定したパターンにマッチする名前のファイル・ディレクトリはコピーされない。

以下のファイル・ディレクトリ構造を例とする。

temp/
└── dir1/
    ├── .config
    ├── .dir/
    ├── file.txt
    └── subdir/
        ├── file.jpg
        └── file.txt

.から始まるファイルおよびディレクトリ、拡張子がjpgのファイルを無視してコピーする。

new_path = shutil.copytree(
    'temp/dir1', 'temp/dir2', ignore=shutil.ignore_patterns('.*', '*.jpg')
)
print(new_path)
# temp/dir2
temp/
├── dir1/
│   ├── .config
│   ├── .dir/
│   ├── file.txt
│   └── subdir/
│       ├── file.jpg
│       └── file.txt
└── dir2/
    ├── file.txt
    └── subdir/
        └── file.txt

複数のファイルを一括でコピー

実践的な例として、条件に応じて複数のファイルを一括でコピーする方法を説明する。

なお、ファイル・ディレクトリ数が多い場合にglob.glob()**を使うと時間がかかる可能性があるので、他の特殊文字で条件を絞れるのであればそちらを使ったほうがいい。

また、上述のshutil.copytree()の引数ignoreで指定できる条件であればそちらのほうが簡単。

ワイルドカードで条件指定

globモジュールを使うとワイルドカード*などの特殊文字を使ってファイルやディレクトリの一覧をリストで取得できる。詳しい使い方は以下の記事を参照。

以下のファイル・ディレクトリ構成を例とする。

temp/
└── dir1/
    ├── 123.jpg
    ├── 456.txt
    ├── abc.txt
    └── subdir/
        ├── 000.csv
        ├── 789.txt
        └── xyz.jpg

元のディレクトリ構造を保持しない

サブディレクトリ内も含めて、拡張子がtxtのファイルを抽出してコピーする。コピー先のディレクトリはあらかじめ生成しておく必要がある。

import shutil
import glob
import os

os.makedirs('temp/dir2')

for p in glob.glob('temp/dir1/**/*.txt', recursive=True):
    if os.path.isfile(p):
        shutil.copy(p, 'temp/dir2')
temp/
├── dir1/
│   ├── 123.jpg
│   ├── 456.txt
│   ├── abc.txt
│   └── subdir/
│       ├── 000.csv
│       ├── 789.txt
│       └── xyz.jpg
└── dir2/
    ├── 456.txt
    ├── 789.txt
    └── abc.txt

上の例では問題ないが、条件によってはディレクトリが抽出される可能性もあるのでos.path.isfile()を使ってファイルのみを対象としている。

ディレクトリをコピーしたい場合はos.path.isfile()shutil.copy()のかわりにos.path.isdir()shutil.copytree()を使えばよい。

元のディレクトリ構造を保持する

コピー元のディレクトリ構造を保持したい場合は、例えば以下のようにする。

glob.glob()の引数root_dirを指定して、os.path.dirname()によるディレクトリ名の抽出、os.path.join()によるパスの連結を利用する。os.makedirs()でディレクトリを生成するので、コピー先のディレクトリを生成しておく必要はない。

src_dir = 'temp/dir1'
dst_dir = 'temp/dir3'

for p in glob.glob('**/*.txt', recursive=True, root_dir=src_dir):
    if os.path.isfile(os.path.join(src_dir, p)):
        os.makedirs(os.path.join(dst_dir, os.path.dirname(p)), exist_ok=True)
        shutil.copy(os.path.join(src_dir, p), os.path.join(dst_dir, p))
temp/
├── dir1/
│   ├── 123.jpg
│   ├── 456.txt
│   ├── abc.txt
│   └── subdir/
│       ├── 000.csv
│       ├── 789.txt
│       └── xyz.jpg
├── dir2/
│   ├── 456.txt
│   ├── 789.txt
│   └── abc.txt
└── dir3/
    ├── 456.txt
    ├── abc.txt
    └── subdir/
        └── 789.txt

正規表現で条件指定

正規表現で条件を指定したい場合はreモジュールを使う。glob.glob()だけでは実現できない複雑な条件を指定できる。

基本的な考え方は上述のglob.glob()のみで抽出する場合と同じ。

以下のファイル・ディレクトリ構成を例とする。

temp/
└── dir1/
    ├── 123.jpg
    ├── 456.txt
    ├── abc.txt
    └── subdir/
        ├── 000.csv
        ├── 789.txt
        └── xyz.jpg

元のディレクトリ構造を保持しない

glob.glob()**と引数recursive=Trueを使ってすべてのファイル・ディレクトリを再帰的にリストアップして、re.search()などで正規表現による判定を行う。

サブディレクトリ内も含めて、ファイル名が数字のみで拡張子がtxtまたはcsvのファイルを抽出してコピーする。\dは数字、+は1回以上の繰り返し、(a|b)abのいずれかにマッチする。

import shutil
import glob
import os
import re

os.makedirs('temp/dir2')

for p in glob.glob('temp/dir1/**', recursive=True):
    if os.path.isfile(p) and re.search('\d+\.(txt|csv)', p):
        shutil.copy(p, 'temp/dir2')
temp/
├── dir1/
│   ├── 123.jpg
│   ├── 456.txt
│   ├── abc.txt
│   └── subdir/
│       ├── 000.csv
│       ├── 789.txt
│       └── xyz.jpg
└── dir2/
    ├── 000.csv
    ├── 456.txt
    └── 789.txt

元のディレクトリ構造を保持する

コピー元のディレクトリ構造を保持する場合は以下の通り。

src_dir = 'temp/dir1'
dst_dir = 'temp/dir3'

for p in glob.glob('**', recursive=True, root_dir=src_dir):
    if os.path.isfile(os.path.join(src_dir, p)) and re.search('\d+\.(txt|csv)', p):
        os.makedirs(os.path.join(dst_dir, os.path.dirname(p)), exist_ok=True)
        shutil.copy(os.path.join(src_dir, p), os.path.join(dst_dir, p))
temp/
├── dir1/
│   ├── 123.jpg
│   ├── 456.txt
│   ├── abc.txt
│   └── subdir/
│       ├── 000.csv
│       ├── 789.txt
│       └── xyz.jpg
├── dir2/
│   ├── 000.csv
│   ├── 456.txt
│   └── 789.txt
└── dir3/
    ├── 456.txt
    └── subdir/
        ├── 000.csv
        └── 789.txt

関連カテゴリー

関連記事