【Python】ショートカット先の実態を再帰的にコピーするプログラムを作ってみた その1

Python
この記事は約11分で読めます。

先日業務で、ショートカット先の実態ファイルをコピーする作業がありました。サブディレクトリも複数あり、超絶面倒くさかったので、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行目では、プログラムで必要になるパッケージを使えるように、インポートしています。

osPathは、パスの操作やファイルの情報を取得するためにインポートしています。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でも動作するように改良します。

タイトルとURLをコピーしました