Pythonでファイル・ディレクトリをコピーするshutil.copy, copytree
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_ok
をTrue
とするとエラーにならない。コピー先に同名のファイルがある場合はコピー元のファイルで上書きされる。
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
引数ignore
にshutil.ignore_patterns()
を指定すると、特定のファイルやディレクトリをコピー対象から除外できる。
- shutil.ignore_patterns() --- 高水準のファイル操作 — Python 3.11.4 ドキュメント
- shutil - copytree の例 --- 高水準のファイル操作 — Python 3.11.4 ドキュメント
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)
はa
かb
のいずれかにマッチする。
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