先日業務で、ショートカット先の実態ファイルをコピーする作業がありました。サブディレクトリも複数あり、超絶面倒くさかったので、Pythonでショートカットの実態を再帰的にコピーするプログラムを作成したので、解説とともに紹介します。
現時点ではWindowsで実行することしか想定していません。次回macやlinuxでも動作するように改良していく予定です。
プログラム全体像
プログラムの流れとしては、一旦対象のディレクトリをサブディレクトリも含めて全てコピーします。コピーするときにショートカットの場合はショートカットとしてコピーし、その後ショートカットについては実態に置き換えるという流れです。
以下がプログラム全体です。
import os
from pathlib import Path
import shutil
import win32com.client
def get_real_path(path):
"""
ショートカットの実態のパスを返す
"""
wshell = win32com.client.Dispatch('WScript.Shell')
return wshell.CreateShortcut(path).Targetpath if path.endswith('.lnk') else path
def copy_tree(src_dir, target_dir, copy_shortcut_dir=True, copy_not_exist_shortcut=True):
"""
ショートカットを実態としてディレクトリ事コピーする。
"""
shutil.copytree(src_dir, target_dir, dirs_exist_ok=True)
# コピー先のショートカットを実態に置き換える
for cur_dir, dirs, files in os.walk(target_dir):
for f in files:
if not f.endswith('.lnk'):
continue
# ショートカット、実態の絶対パス取得
shortcut_abs_path = os.path.abspath(os.sep.join([cur_dir, f]))
entity_abs_path = get_real_path(shortcut_abs_path)
if os.path.isdir(entity_abs_path) and copy_shortcut_dir:
# ショートカットのフォルダを削除して、フォルダの実態をコピー
os.remove(shortcut_abs_path)
copy_tree(entity_abs_path, Path(shortcut_abs_path).parent / Path(entity_abs_path).name,
copy_shortcut_dir, delete_not_exist_shortcut)
elif os.path.isfile(entity_abs_path):
# ショートカットのファイルを削除して、ファイルの実態をコピー
os.remove(shortcut_abs_path)
shutil.copy(entity_abs_path, os.path.dirname(shortcut_abs_path))
elif not os.path.exists(entity_abs_path) and not copy_not_exist_shortcut:
# ショートカット先が存在しない場合はショートカットを削除
os.remove(shortcut_abs_path)
if __name__ == '__main__':
copy_tree('path/to/src', 'path/to/target')
プログラムの使い方
プログラムの使い方を説明します。
プログラムを適当なファイル名で保存して、例えばmain.pyなど、コピー元のディレクトリと同じパスに置いておきます。プログラムの44行目の第一引数に、コピー元のディレクトリ名を、第二引数にコピー先のディレクトリ名を書きます。コピー先のディレクトリは、事前に作成してもいいですし、作成しなくてもいいです。作成していない場合はプログラム実行時に自動で作成されます。
例えば以下のようにディレクトリとファイルを配置します。
├─src_dir(コピー元のディレクトリ名)
├─target_dir(コピー先のディレクトリ名 ※なくてもOK、ない場合は自動で作成される)
└─main.py
if __name__ == '__main__':
copy_tree('src_dir', 'target_dir') # 例
あとは、プログラムを実行するとプログラムと同じディレクトリに、コピー先のディレクトリがコピーされ、コピー先ではショートカットが実態に置き換わっているはずです。(ショートカット先の実態ファイルが存在する場合)
オプションとして、ディレクトリのショートカットをコピーするかどうかのオプションと、ショートカット先の実態ファイルがない場合に、そのショートカットをコピーするかどうかのオプションがあります。
ディレクトリショートカットのコピーはデフォルトでTrue(する)になっているので、ディレクトリのショートカットのコピーをしたくない場合は、44行目でcopy_shortcut_dir=Falseを指定して実行すればいいです。
if __name__ == '__main__':
copy_tree('path/to/src', 'path/to/target', copy_shortcut_dir=False)
ショートカットの実態ファイルがない場合にそのショートカットコピーはデフォルトでTrue(する)になっているので、ショートカットをコピーしたくない場合は、44行目で、copy_not_exist_shortcut=Falseを指定して実行すればいいです。
if __name__ == '__main__':
copy_tree('path/to/src', 'path/to/target', copy_not_exist_shortcut=False)
事前準備
win32comというパッケージが必要なので、インストールします。
condaを使っている場合は以下コマンドでインストールします。
conda install -c anaconda pywin32
pipを使っている場合は以下コマンドでインストールします。
pip install pywin32
プログラムの解説
プログラムの詳細を解説していきます。
パッケージのインポート
import os
from pathlib import Path
import shutil
import win32com.client
1~4行目では、プログラムで必要になるパッケージを使えるように、インポートしています。
osとPathは、パスの操作やファイルの情報を取得するためにインポートしています。shutilはファイルやディレクトリのコピー、win32com.clientはショートカットの実態パスを取得するためにインポートしています。
ショートカット先の実態のパス取得
def get_real_path(path):
"""
ショートカットの実態のパスを返す
"""
wshell = win32com.client.Dispatch('WScript.Shell')
return wshell.CreateShortcut(path).Targetpath if path.endswith('.lnk') else path
7~12行目のget_real_pathという関数で、ショートカット先の実態ファイルのパスを取得しています。引数としてファイルのパスを受け取ります。リンクの場合はpathの末尾が「.lnk」になるので、その場合wshell.CreateShortcut(path).Targetpathでショートカット先の実態のパスを返し、そうでない場合はpathをそのまま返します。
ディレクトリ事コピー
def copy_tree(src_dir, target_dir, copy_shortcut_dir=True, copy_not_exist_shortcut=True):
"""
ショートカットを実態としてディレクトリ事コピーする。
"""
shutil.copytree(src_dir, target_dir, dirs_exist_ok=True)
14行目でディレクトリ事コピーし、ショートカットを実態ファイルとしてコピーする関数を定義しています。引数の意味は以下のようになっています。
- src_dir:コピー元のディレクトリ名
- target_dir:コピー先のディレクトリ名
- copy_shortcut_dir:ディレクトリのショートカットの場合に実態をコピーするかどうか(デフォルトはコピーする)
- copy_not_exist_shortcut:ショートカットの実態が存在しない場合、ショートカットをコピーするかどうか。(デフォルトはコピーする)
18行目で、src_dirで指定したディレクトリをtarget_dirでしたディレクトリに丸ごとコピーしています。
ショートカットのファイルやディレクトリはそのままショートカットとしてコピーされるので、この後のプログラムでショートカットを実態に置き換えます。
ショートカットを実態に置き換える
for cur_dir, dirs, files in os.walk(target_dir):
for f in files:
if not f.endswith('.lnk'):
continue
22~25行目で、ショートカットファイルを実態に置き換えるために、コピーしたディレクトリの中のファイルをすべて走査しています。ショートカットの場合はファイル名が「.lnk」で終わるので、「.lnk」で終わっていない場合には置き換える必要がないので、continueでfor文の先頭に戻っています。
# ショートカット、実態の絶対パス取得
shortcut_abs_path = os.path.abspath(os.sep.join([cur_dir, f]))
entity_abs_path = get_real_path(shortcut_abs_path)
27~28行目で、ショートカットと実態ファイルの絶対パスを取得しています。get_real_pathは先ほど解説した関数ですね。
if os.path.isdir(entity_abs_path) and copy_shortcut_dir:
# ショートカットのフォルダを削除して、フォルダの実態をコピー
os.remove(shortcut_abs_path)
copy_tree(entity_abs_path, Path(shortcut_abs_path).parent / Path(entity_abs_path).name,
copy_shortcut_dir, delete_not_exist_shortcut)
elif os.path.isfile(entity_abs_path):
# ショートカットのファイルを削除して、ファイルの実態をコピー
os.remove(shortcut_abs_path)
shutil.copy(entity_abs_path, os.path.dirname(shortcut_abs_path))
elif not os.path.exists(entity_abs_path) and not copy_not_exist_shortcut:
# ショートカット先が存在しない場合はショートカットを削除
os.remove(shortcut_abs_path)
30~41行目で、コピー先のディレクトリのショートカットを実態ファイルに置き換えています。
30~34行目ディレクトリのショートカットを実態ファイルに置き換える処理をしています。実態ファイルが存在してかつ、copy_shortcut_dirがTrue(フォルダのショートカットの実態をコピーする場合)にまずは、ショートカットを削除し、自分自分を呼び出して、再帰的にコピーしています。
35~37行目では、ファイルのショートカットを実態ファイルに置き換える処理をしています。ショートカット先の実態ファイルが存在して、それがファイルの場合には、ショートカットを削除し後に、実態ファイルをコピーしています。
39~41行目で、実態ファイルが存在しない場合に、copy_not_exist_shortcutがFalseつまり、「ショートカット先が存在しない場合にショートカットを削除しない」となっている場合は単にショートカットを削除します。
次回は、macやlinuxでも動作するように改良します。