見出し画像

【エンジニアの道は果てしない】CUIとGUIとバッチ処理(ファイル操作のプログラム視点と使い処)

こんにちは。すうちです。

師走(12月)に入り今年も残り1か月を切りました。ほんとに1年早いですね。今日予報では暖かいと聞いてましたが、私の家の周りはなぜか凍えるような寒さでした…笑

本題です。エンジニアの仕事をしていて、開発のプログラム書く以外にも目的に辿り着く過程で発生する単純作業や手作業で時間かかる時など、それ用のプログラムを書いたりツールと組み合わせて効率化を考えることがあります。

そのやり方はCUIだったりGUIだったりバッチ処理だったりする訳ですが、同じことを実現するのも色々やり方があって、私も(今更ですが)使い分けているなと気づきました。今回はファイル操作を例に上記に関する話です。


はじめに

まず本日のお題に関する前提条件を少し補足します。

CUICharacter User Interfaceの略でテキスト入力で処理実行。 GUIGraphical User Interaceの略でユーザ操作で実行。バッチ処理はプログラムやコマンドなど複数の処理をまとめて実行することです。

以降、下記をお題として進めます。

お題:あるフォルダの特定ファイルを別フォルダにコピー又は移動

フォルダ構成(初期状態)

D:
├─test1
│      note1.png
│      note2.png
│      test1.txt
│      test2.txt
│      test3.txt
│
├─test2
├─test3
├─test4
└─test5

Windowsのエクスプローラーの場合、マウスとショートカット操作(もしくはファイル選択して右クリック、コピー、ペースト)になると思います。

ctrl押して、目的のファイル選択
ctrl+c、ctrl+vで、目的のファイルをコピー

同じことはCUI(例:Windowsコマンドプロンプト)もできます。


CUI(コマンドプロンプト)

前述のフォルダ構成で’D:’ドライブにいる場合、
例:test1に移動(cd)、ファイルのコピー(copy)

>cd test1
>copy test1.txt test1_copy.txt

例:コピーしたファイルをtest2に移動(move)

>move test1_copy.txt d:/test2
 D:\test2 のディレクトリ

2022/12/04  18:43    <DIR>          .
2022/12/04  18:43    <DIR>          ..
2022/12/04  01:22                23 test1_copy.txt

同様にこれらはプログラムでも可能です。


プログラム(Python)

言語はPythonです。今回ファイル移動/コピーはshutilを使ってます。

ファイル操作のサンプルプログラム
注:移動先に同名ファイルある場合は上書き


import os
import shutil

# 特定ファイル抽出
def get_target_files(src_path, search_keys, is_and=False):
    
    file_list    = os.listdir(src_path)
    target_files = []
    for name in file_list:
        for k in search_keys:
            if k in name:
                target_files.append(name)
                break
    
    return target_files

# ファイル移動/コピー
def move_file(dst_dir, src_dir, target_files, is_move):
    
    cnt = 0
    for file_name in target_files:
        cur_path = os.path.join(src_dir, file_name)
        dst_path = os.path.join(dst_dir, file_name)
        
        if os.path.isfile(cur_path):
            
            if is_move:
                shutil.move(cur_path, dst_path)
                
            else:   # copy
                shutil.copy(cur_path, dst_path)
                
            cnt += 1
            
    if cnt == 0:
        str_msg = 'No files to move or copy w/ keywords ...'
    else:
        str_util = 'moved' if is_move == True else 'copied'
        str_msg = '{} files {} to {} ...'.format(cnt, str_util, dst_dir)
        
    return str_msg

# ディレクトリ判定
def is_valid(dst_path1, dst_path2):
    
    is_valid_cnt = 0
    
    if os.path.isdir(dst_path1):
        is_valid_cnt += 1
        
    if os.path.isdir(dst_path2):
        is_valid_cnt += 1
    
    if is_valid_cnt >= 2:
        return True
    else:
        return False


def util_file():
    # 設定
    dst_path    = 'd:/test2'
    src_path    = 'd:/test1'
    search_keys = ['test1.txt']
    is_move     = True
    
    # ファイル処理
    if is_valid(dst_path, src_path):
        file_list = get_target_files(src_path, search_keys)
        str_msg = move_file(dst_path, src_path, file_list, is_move)
    else:
        str_msg = 'Invalid src or dst folder...'
    
    print(str_msg)
    return str_msg
    

def main():
    util_file()

if __name__ == '__main__':
    main()

この場合、移動元や移動先のフォルダ指定が直接コード(’#設定’の箇所)に書いてあるため、条件を変える場合は毎回書換が必要です(差分は最初の設定だけですが)。

上記のような一部設定を変えたい場合は、後述のGUIの方が便利です。


プログラム(Python+GUI)

GUI部分は、以前noteに投稿したTkinterを使いました。

下記プログラム実行すると、このようなGUIが表示されます。

GUIの画面
import tkinter as tk
from   tkinter import filedialog

class ViewGUI():
    
    def __init__(self, main_window):
        
        # フレームサイズ指定
        main_window.geometry("380x350")
        # フレームタイトル
        main_window.title('GUI Util-File')
        
        # ストリング生成
        self.str_entry_dir1  = tk.StringVar()
        self.str_entry_dir2  = tk.StringVar()
        self.str_search_key  = tk.StringVar()
        self.str_util_msgs   = tk.StringVar()
        # InvVal生成
        self.int_val_radio   = tk.IntVar()
        # テキスト指定
        self.str_entry_dir1.set  ('Choose Folder(移動先) ...')
        self.str_entry_dir2.set  ('Choose Folder(移動元) ...')
        self.str_search_key.set  ('')
        # ラベル生成
        label_key1     = tk.Label(main_window,  text = 'search_key')
        label_msgbox   = tk.Label(main_window,  text = '[MessageBox]')
        label_msgs     = tk.Label(main_window,  text = '', textvariable=self.str_util_msgs)
        # テキストBOX(エントリ)生成
        entry_dstdir   = tk.Entry(main_window,  text = 'dst_dir',   textvariable=self.str_entry_dir1,  width=50)
        entry_srcdir   = tk.Entry(main_window,  text = 'src_dir',   textvariable=self.str_entry_dir2,  width=50)
        entry_search   = tk.Entry(main_window,  text = 'search_key',textvariable=self.str_search_key,  width=15)
        # ボタン生成
        button_dir_dst = tk.Button(main_window, text = 'set folder(dst)' ,width=10, command=self.event_button_dir1)
        button_dir_src = tk.Button(main_window, text = 'set folder(src)' ,width=10, command=self.event_button_dir2)
        button_run     = tk.Button(main_window, text = 'copy/move'       ,width=10, command=self.event_button_run)
        
        # ラジオボタン生成
        radio_copy     = tk.Radiobutton(main_window, text='copy', value=0, variable=self.int_val_radio,command=self.event_raido_change)
        radio_move     = tk.Radiobutton(main_window, text='move', value=1, variable=self.int_val_radio,command=self.event_raido_change)
        self.int_val_radio.set(0)
        
        # ラベル、テキストBOX、ボタン配置
        button_dir_dst.place (x=40, y=20)
        button_dir_src.place (x=40, y=80)
        label_key1.place     (x=40, y=140)
        label_msgbox.place   (x=40, y=270)
        label_msgs.place     (x=40, y=290)
        
        radio_copy.place     (x=40, y=200)
        radio_move.place     (x=110,y=200)
        
        entry_dstdir.place   (x=40, y=50)
        entry_srcdir.place   (x=40, y=110)
        entry_search.place   (x=40, y=160)
        button_run.place     (x=40, y=230)
        
    
    ## Event callback
    def event_button_dir1(self):
        self.str_util_msgs.set('')
        dir_path = filedialog.askdirectory(initialdir='D:', mustexist=True)
        self.str_entry_dir1.set(dir_path)
        
    def event_button_dir2(self):
        self.str_util_msgs.set('')
        dir_path = filedialog.askdirectory(initialdir='D:', mustexist=True)
        self.str_entry_dir2.set(dir_path)
    
    def event_raido_change(self):
        self.str_util_msgs.set('')
        
    def event_button_run(self):
        self.str_util_msgs.set('')
        args_dict   = {}
        args_dict['dst_dir']    = self.str_entry_dir1.get()
        args_dict['src_dir']    = self.str_entry_dir2.get()
        args_dict['search_nks'] = self.str_search_key.get().split(',')
        args_dict['radio_val']  = self.int_val_radio.get()

        str_msg = util_file_gui(args_dict)
        gui_msg = '\t' + str_msg
        self.str_util_msgs.set(gui_msg)


def main():
    # フレーム生成
    main_window = tk.Tk()
    # クラス生成
    ViewGUI(main_window)
    # Windowループ処理
    main_window.mainloop()

if __name__ == '__main__':
    main()

一部GUI設定用に書き換えたファイル操作の関数(内部処理は前述と同じ)

def util_file_gui(args_dict):
    # 設定
    dst_path    = args_dict['dst_dir']
    src_path    = args_dict['src_dir']
    search_keys = args_dict['search_nks']
    is_move     = args_dict['radio_val']
    is_move     = True if is_move == 1 else False
    # ファイル処理
    if is_valid(dst_path, src_path):
        file_list = get_target_files(src_path, search_keys)
        str_msg = move_file(dst_path, src_path, file_list, is_move)
    else:
        str_msg = 'Invalid src or dst folder...'
    
    return str_msg

GUIで設定した情報は、copy/moveボタン押下時に実行されるイベント(event_button_run)で以下の様に取得してます。

args_dict['dst_dir']    = self.str_entry_dir1.get()
args_dict['src_dir']    = self.str_entry_dir2.get()
args_dict['search_nks'] = self.str_search_key.get().split(',')
args_dict['radio_val']  = self.int_val_radio.get()

これにより移動先/移動元フォルダ、操作対象のファイル情報を関数(util_file_gui)の引数に渡せるので、プログラムの設定を動的に変えられます。

処理結果
例:移動元test1から'.png'に該当するファイルを移動先test2に移動

png拡張子のファイルをtest1からtest2へ移動

GUIは目で見て直感的に操作できる利点がありますが、欠点はユーザ操作が前提なので条件変える場合のプログラム実行に人の手がいることです。

例えば、移動対象のフォルダ数や条件が増えてくると人手では時間かかり、作業漏れのミスも発生したりします。

上記を解消する方法として、次はバッチ処理による自動化です。


バッチ処理

バッチ処理(複数の実行条件を記載したファイル)からプログラムの設定を受け取るため、Pythonのargparseをインポートします。

以下、argparseadd_argument()の使い方です。

主な引数のルール(左から)

-X     : 省略コマンド名
--X    : 正式コマンド名(プログラム参照時に必要)
type  : str:文字列, int:数値など
action  :
 store_true(真偽値の場合)
nargs   :'*' 複数指定(例:-ns text1, text2, text3…)
default : 初期設定

add_argumentの注意点として、bool型を使う場合 action=’store_true’指定が必要です(Pythonの仕様はtype=bool指定だと正しく認識されません)。
例では、--move_true指定があるとTrue、無い場合はFalseと解釈されます。

import os
import shutil
import argparse

_sdir = 'd:/test1'
_ddir = 'd:/test2'
_ser = ['.txt','.png']
_ism = False

def get_args():
    
    parser = argparse.ArgumentParser(description='file util')
    parser.add_argument('-s''--src',       type=str,  default= _sdir)
    parser.add_argument('-d''--dst',       type=str,  default= _ddir)
    parser.add_argument('-ns','--n_search',  type=str,  nargs='*',  default= _ser)
    parser.add_argument('--move_true', action='store_true')
    args = parser.parse_args()
    
    return args

def main(args):
    util_file_cui(args)

if __name__ == '__main__':
    args = get_args()
    main(args)

一部CUI設定用に書き換えたファイル操作の関数(内部処理は前述と同じ)

def util_file_cui(args):
    # 設定
    dst_path    = args.dst
    src_path    = args.src
    search_keys = args.n_search
    is_move     = args.move_true
    
    if is_valid(dst_path, src_path):
        file_list = get_target_files(src_path, search_keys)
        str_msg = move_file(dst_path, src_path, file_list, is_move)
    else:
        str_msg = 'Invalid src or dst folder...'
    
    print(str_msg)
    return str_msg

バッチファイル(.bat
例:test1からtest2/3/4/5フォルダに指定条件のファイルをコピー/移動

以下、バッチ処理の意味(引数の定義はadd_argumentで指定)です。
 → python [実行するプログラムのパス]
 → -s [移動元フォルダのパス]
 → -d [移動先フォルダのパス]
 → -ns [操作対象の文字例1] … [操作対象の文字列N]
 → --move_true 移動指定あり(True)

python D:/repo/FileUtil/file_util_cui_argparse.py -s d:/test1 -d d:/test2 -ns .png .txt
python D:/repo/FileUtil/file_util_cui_argparse.py -s d:/test1 -d d:/test3 -ns .png
python D:/repo/FileUtil/file_util_cui_argparse.py -s d:/test1 -d d:/test4 -ns .txt
python D:/repo/FileUtil/file_util_cui_argparse.py -s d:/test1 -d d:/test5 -ns .png .txt --move_true

処理結果

D:.
├─test1
├─test2
│      note1.png
│      note2.png
│      test1.txt
│      test2.txt
│      test3.txt
│
├─test3
│      note1.png
│      note2.png
│
├─test4
│      test1.txt
│      test2.txt
│      test3.txt
│
└─test5
        note1.png
        note2.png
        test1.txt
        test2.txt
        test3.txt

バッチ処理は一度中身を書いて実行してしまえば、人手を介さず別のことができるメリットが大きいです(特に時間かかる処理は手が離れて重宝します)。

ただし、バッチファイルは設定ミス(誤記)やプログラム自体にバグあるとせっかく自動実行しても当然欲しい結果は得られません。私も途中エラーに気づかず夜中に処理が止まり翌日やり直した経験も何度かあります。

バッチ処理も通常のプログラムと同様に十分テストして実績できたら使う形になるかと思います。


最後に

今回は簡単なファイル操作(移動、コピー)を例に書きましたが、この程度であればどれも大差ないです(逆にプログラムは書く手間やテストに時間かかる)。

実際の業務(データ解析や必要だが単純な作業)は、もう少し複雑な処理や組合せがあったりしますが、手動で何度もやってられない作業などプログラム化を検討します(適材適所でCUI/GUI/バッチやツールを使い分け)。

個人的にプログラム化の判断基準は「直近1週間又は1日に何回も同じ作業をする」時です。その場合、一旦手を止めてプログラムを書いている気がします。

近年は技術的な情報も豊富なのでその気になり調べればプログラムの情報はたくさん出てきます。またコードが書けなくてもノーコードやRPA等のツールの情報も見つかります。

エンジニアでない方も業務で単純作業や繰返しが多いなと感じたら、それを少しでも自動化できないか?の視点でプログラムやツールを触ってみると良いかもしれません。

私も必要に駆られて調べたり試すことが多いですが、その方が目的と合致するので、忘れにくく身に付きやすい気がします。

最後まで読んで頂き、ありがとうございました。


この記事が気に入ったらサポートをしてみませんか?