【201911~201912】最近作った雑多なプログラム・ツールなど

# この記事について
最近個人的な用として作った雑多なプログラム・ツールなど。


- 記事として個別に立てるほどのものでもない小規模かつ簡単なもの。
- 使用言語はPython, Powershell, ExcelVBA。
- 使用環境は
    - Windows10。
    - Powershell5.1。
    - Python3.7.0。
    - Office2016。

######記事が長いので、目次から見たい項目だけスポット的にジャンプしたほうが良いかもしれません。
######※最新のツールとソースコードはGitHubに上げてます。
[https://github.com/dede-20191130/CreateToolAndTest](https://github.com/dede-20191130/CreateToolAndTest)

# 1.Python
## 1-1.oldとか旧とか名前のついたフォルダを直下のファイルごとゴミ箱にぶちこむプログラム
###きっかけ
oldとかの名前のついたフォルダって意識して整理しないと永遠に貯まることになるので
断捨離をしたかった。

###コード

```py:Crean_OldNamed_Folders.py
import glob
import os
import tkinter as tk
import tkinter.simpledialog as sim
from tkinter import messagebox
import send2trash


def Main():
    root = tk.Tk()
    root.withdraw()
    tmpPath = sim.askstring("oldフォルダ探索フォルダ指定", "oldフォルダを探索するフォルダパスを指定してください", initialvalue="")
    if tmpPath not in (None, ''):
        ThrowAwayOld(tmpPath)


def ThrowAwayOld(baseFolderPath):
    """oldなフォルダを再帰的に探索しその中身ごとゴミ箱に送る"""
    if not os.path.isdir(baseFolderPath):
        messagebox.showerror("エラー", "存在しないフォルダパスです。")
        exit(1)

    rmNum = 0
    for f in glob.glob(baseFolderPath + '\\**', recursive=True):
        if os.path.isdir(f) and os.path.basename(f).lower() in ('old', '古い', '旧'):
            send2trash.send2trash(os.path.abspath(f))
            rmNum += 1

    messagebox.showinfo("正常終了", "処理が完了しました。\n" + str(rmNum) + "個の古いフォルダをゴミ箱に送りました。")


if __name__ == '__main__':
    Main()

```

###解説

```
    for f in glob.glob(baseFolderPath + '\\**', recursive=True):
        if os.path.isdir(f) and os.path.basename(f).lower() in ('old', '古い', '旧'):
            send2trash.send2trash(os.path.abspath(f))
            rmNum += 1

```

glob.glob関数にrecursiveを設定することで再帰的なファイル検索ができる。

「old」や「古い」などのワードを含んだフォルダをゴミ箱に送付。

リムーブするともとには戻れないのでごみ箱に送るという形でワンクッション置きたかった。


##1-2.テキストの中の特定のファイルパス文字列だけ汎用的なものに変更するプログラム

###きっかけ
QiitaなどのSNSにソースコードを公開するに当たり、
個人的なファイルパスやファイル名を含んでしまうのはなんとなく嫌だった。
そのためファイルパスを一括で置き換えられるプログラムを作成した。

###ソースコード

```py:ChangePrivatePathsOfText.py
import os
import pathlib
import re
import chardet


def Main():
    with pathlib.Path(os.getenv("HOMEDRIVE") + os.getenv(
            "HOMEPATH") + r'\PycharmProjects\CreateToolAndTest\PrivateTool\ChangedString.txt') as pp:
        # 文字コードを検出
        with open(pp.absolute(), "rb") as f:
            temp = chardet.detect(f.read())['encoding']

        # 文字をインプット
        allText = pp.read_text(encoding=temp)


        # ///正規表現による置換
        # ファイルパスの置換
        allText = re.sub(r'"[a-zA-Z]:[\\/].*\.', r'"C:/Users/UserName/MyFolder/Foo.', allText)
        allText = re.sub(r"'[a-zA-Z]:[\\/].*\.", r"'C:/Users/UserName/MyFolder/Foo.", allText)
        # フォルダパスの置換
        allText = re.sub(r'"[a-zA-Z]:[\\/](?!.*\.).*"', r'"C:/Users/UserName/MyFolder/Foo."', allText)
        allText = re.sub(r"'[a-zA-Z]:[\\/](?!.*\.).*'", r"'C:/Users/UserName/MyFolder/Foo.'", allText)

        # 置換後をアウトプット
        pp.write_text(allText, encoding=temp)


if __name__ == '__main__':
    Main()

```

###解説

pathlibモジュールのread_text・write_text関数とwith構文を用いて
ファイルの入出力を簡略化している。

```
        # 文字コードを検出
        with open(pp.absolute(), "rb") as f:
            temp = chardet.detect(f.read())['encoding']

        # 文字をインプット
        allText = pp.read_text(encoding=temp)
```

予め検出した文字コードを用いて全文字列を読み込みしている。

```
        # ///正規表現による置換
        # ファイルパスの置換
        allText = re.sub(r'"[a-zA-Z]:[\\/].*\.', r'"C:/Users/UserName/MyFolder/Foo.', allText)
        allText = re.sub(r"'[a-zA-Z]:[\\/].*\.", r"'C:/Users/UserName/MyFolder/Foo.", allText)
        # フォルダパスの置換
        allText = re.sub(r'"[a-zA-Z]:[\\/](?!.*\.).*"', r'"C:/Users/UserName/MyFolder/Foo."', allText)
        allText = re.sub(r"'[a-zA-Z]:[\\/](?!.*\.).*'", r"'C:/Users/UserName/MyFolder/Foo.'", allText)

```

ファイルパスに関しては、

- 最初が「"」か「'」で始まる
- 次にアルファベッド文字＋：＋（\か/のどちらか）が来る
- 最後に「.」が来る

という条件で正規表現の置換を用いている。

フォルダパスに関しては、

- 最初が「"」か「'」で始まる
- 次にアルファベッド文字＋：＋（\か/のどちらか）が来る
- それ以降に「.」文字を含まない
- 最後が「"」か「'」で終わる

という条件で正規表現の置換を用いている。


##1-3.テキストの中の特定のURL文字列だけ汎用的なものに変更するプログラム

###きっかけ

1-2とほぼ同じ

###ソースコード

```py:ChangeURLPartsOfText.py
import os
import pathlib
import re
import chardet


def Main():
    with pathlib.Path(os.getenv("HOMEDRIVE") + os.getenv(
            "HOMEPATH") + r'\PycharmProjects\CreateToolAndTest\PrivateTool\ChangedString.txt') as pp:
        # 文字コードを検出
        with open(pp.absolute(), "rb") as f:
            temp = chardet.detect(f.read())['encoding']

        # 文字をインプット
        allText = pp.read_text(encoding=temp)
        # print(allText)

        # 正規表現による置換
        allText = re.sub(r'"https?://.*"', r'"http://foobar.com"', allText)
        allText = re.sub(r"'https?://.*'", r"'http://foobar.com'", allText)

        # 置換後をアウトプット
        pp.write_text(allText, encoding=temp)


if __name__ == '__main__':
    Main()
```

###解説

```
        # 正規表現による置換
        allText = re.sub(r'"https?://.*"', r'"http://foobar.com"', allText)
        allText = re.sub(r"'https?://.*'", r"'http://foobar.com'", allText)
```

シングルクオートおよびダブルクオートのどちらでくくられた文字列にも対応している。

##1-4.自作クラス：tkinterのメッセージボックスの表示の簡略化

###きっかけ
pythonのメッセージボックス表示は
rootフレームの設定など、色々前準備がめんどくさいので
staticメソッドの呼び出し一発でメッセージを表示できるようにしたかった。

###ソースコード

```py:TkHandler_MsgBox_1.py
import tkinter as tk
from tkinter import messagebox


class TkHandler_MsgBox_1:
    """
    メッセージボックスのハンドリングクラス_1
    """

    def __init__(self):
        self.root = tk.Tk()
        self.root.withdraw()

    def __del__(self):
        self.root.destroy()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def normalMsgBox(self, title='メッセージ', content='ここにメッセージが表示されます。'):
        messagebox.showinfo(title, content)

    def errorMsgBox(self, title='エラー', content='エラーが発生しました。'):
        messagebox.showerror(title, content)

    @staticmethod
    def showStandardMessage(title, content, isError=False, errorTitle=None, errorContent=None):
        """
        標準的な用法でメッセージを表示する場合に用いる。
        Tkオブジェクトは再利用しない想定ですぐ解放する。

        :arg
            (error)title:(エラー)タイトル
            (error)content:(エラー)メッセージ内容
            isError:エラーが発生した場合はエラーメッセージを表示する。

        """
        with TkHandler_MsgBox_1() as objMsg:
            myTitle = title if not isError else errorTitle
            myContent = content if not isError else errorContent
            calledFunc = objMsg.normalMsgBox if not isError else objMsg.errorMsgBox

            calledFunc(title=myTitle, content=myContent)
```

###解説

```
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass
```

上記はwith構文を用いる際のコンテキストマネージャ 。
[参考：https://python.keicode.com/lang/context-manager.php](https://python.keicode.com/lang/context-manager.php)

今回は特に何もしない。

```
    def normalMsgBox(self, title='メッセージ', content='ここにメッセージが表示されます。'):
        messagebox.showinfo(title, content)

    def errorMsgBox(self, title='エラー', content='エラーが発生しました。'):
        messagebox.showerror(title, content)
```

それぞれを呼び出すと、引数に設定した条件でメッセージを表示する。
今回は情報メッセージとエラーメッセージに対応した関数のみを実装。

```
            myTitle = title if not isError else errorTitle
            myContent = content if not isError else errorContent
            calledFunc = objMsg.normalMsgBox if not isError else objMsg.errorMsgBox

            calledFunc(title=myTitle, content=myContent)
```

isErrorの値に従って、三項演算子により変数を条件分岐。

最終的に実行される関数calledFuncはnormalMsgBox かerrorMsgBoxのどちらかとなる。

###使用法

こんな感じで用いる。
rootフレーム等の設定の必要なしで一行で呼べる。

```py
from Commons.TkHandler_MsgBox_1 import TkHandler_MsgBox_1
 TkHandler_MsgBox_1.showStandardMessage('処理完了', 'ファイル移動が完了しました。')
```

##1-5.指定フォルダの中のファイルを別のフォルダ群にそれぞれコピーするプログラム

###きっかけ
スマホなどで取り込んだ写真などを、
PCのピクチャとGドライブフォルダの両方に移しておきたかった。

###ソースコード

```py:CopyPicsToFolders.py
import glob
import os
import sys

# モジュール検索パスを再設定
# ダブルクリックによる起動にも対応できるようにする
sys.path.append(os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + r"\PycharmProjects\CreateToolAndTest")
import shutil
import send2trash
# Common parts in dede-20191130's GitHub
# https://github.com/dede-20191130/CreateToolAndTest
from Commons.TkHandler_MsgBox_1 import TkHandler_MsgBox_1


def Main():
    isSuccess = True

    try:
        mySrcPath = r'C:/Users/UserName/MyFolder/Foo'
        myDestpathList = [r'C:/Users/UserName/MyFolder/Bar', r'C:/Users/UserName/MyFolder/Dede']
        movedFileList = []

        # 指定フォルダ直下のすべての画像ファイルを取得
        if os.path.isdir(mySrcPath):
            for file in glob.glob(mySrcPath + '\\*', recursive=False):
                if os.path.isfile(file):
                    movedFileList.append(file)
        else:
            print('No Directory')
            exit(0)

        # 指定出力先フォルダにコピー
        for dst in myDestpathList:
            [shutil.copy(os.path.abspath(file2), dst) for file2 in movedFileList]
    except:
        isSuccess = False

    finally:
        if isSuccess:
            TkHandler_MsgBox_1.showStandardMessage('処理完了', 'ファイル移動が完了しました。')
            for file in glob.glob(mySrcPath + '\\*', recursive=False):
                send2trash.send2trash(os.path.abspath(file))


if __name__ == '__main__':
    Main()
```

###解説

```
    except:
        isSuccess = False

    finally:
        if isSuccess:
            TkHandler_MsgBox_1.showStandardMessage('処理完了', 'ファイル移動が完了しました。')
            for file in glob.glob(mySrcPath + '\\*', recursive=False):
                send2trash.send2trash(os.path.abspath(file))
```

例外をキャッチしなかった場合
正常判定とみなしてメッセージを表示し、
元フォルダにおけるファイルをゴミ箱に送る。

##1-6.randomモジュールを用いないでランダムな整数を取得するサイコロツール

###きっかけ
ナノセカンドレベルで時刻を取得すれば
人間の体感レベルではランダムな数字を取得できるのでは？と思って作った。

###ソースコード

```py:DiceAppWithoutRandomLibrary.py
import time
import tkinter as tk
from tkinter import messagebox


def diceResult():
    # 時刻により（人間の体感時間では）ランダムな変数取得
    temp = int((time.time_ns() % 100000000) / 100)
    # ６で割った余りを取得
    myTempRmd = temp % 6
    if myTempRmd == 0:
        myTempRmd = 6

    return myTempRmd


if __name__ == '__main__':
    # サイコロ結果を取得
    result = diceResult()
    # サイコロを振った結果としてメッセージで表示
    root = tk.Tk()
    root.withdraw()  # 小さなウィンドウを表示させない
    messagebox.showinfo("サイコロ", "出目は" + str(result) + "です")
```

###解説

※プログラムで実行し続けると実行タイミング次第で
規則性が出てきてしまうかもしれない。
あくまで人間の体感レベルならということで。。。

#2.Powershell

##2-1.指定秒後に休止状態にするスクリプト

###きっかけ
寝る前、
Gドライブにファイルを転送したあとに寝ようとすると
転送が完了するまで待たなければいけない。
ある程度時間がたったあとに（転送が完了したあとに）休止にさせるスクリプトを作成した。

###ソースコード

```powershell:hibernationTimer.ps1
Param(
    [long]$waitTime = 300   # 待機時間（s）
    )

sleep $waitTime 
# 休止状態
shutdown -h

# 当スクリプトを実行したコンソールウィンドウのみ閉じる（PIDは実行中のPowerShellのプロセスIDが設定される自動変数）
Stop-Process -Id $PID
```

###解説

引数に指定した秒数（デフォルトは300秒）を指定して
コンソール上で実行。

休止状態に入る際に自動でコンソール画面を閉じる。

##2-2.WinMergeを、いつも使う2フォルダを比較した状態で起動するスクリプト

###きっかけ

いつも比較するフォルダ（新旧プロジェクトフォルダなど）を比較した状態で起動できれば
いちいちフォルダ指定画面で指定する必要がなくなると思い作成。

###方法

Powershellのprofileファイルに次のように追記する。

```powershell:Microsoft.PowerShellISE_profile.ps1
Sal wmg C:\Tool_nonInstall\winmerge-2.16.4-x64-exe\WinMerge\WinMergeU.exe
function wtmp() {
    wmg $HOME\Documents\Dif_1 $HOME\Documents\Dif_2
}

```

###解説

コンソール上で「wtmp」と実行すると
Dif_1とDif_2を比較した状態でWinMergeを起動する。


#3.ExcelVBA

##3-1.エクセルブックの体裁を整えるプログラム

###きっかけ

エクセルブックを相手に渡すときに、
シートが一番左のシート、
カーソルがA1セルを選択してある状態をマナーとする風潮が一部会社であるという。

そのような場合に、何分もかけてA1に移動させる手間を省くプログラム

###ソースコード

```vb:プログラム
'******************************************************************************************
'*関数名    ：formatForSubmission
'*機能      ：エクセルブックを相手に提出する際に体裁を整える必要があるときに手早く行える。
'             すべてのシートのカーソルを左上セル（A1セル）に合わせ、一番左のシートをアクティブにした状態にする。
'*引数(1)   ：無し
'******************************************************************************************
Public Sub formatForSubmission()
    
    '定数
    Const FUNC_NAME As String = "formatForSubmission"
    
    '変数
    Dim cntObj As Object                         'ループカウンタ
    
    On Error GoTo ErrorHandler
    '---以下に処理を記述---
    
    'すべてのシートのカーソルを左上セル（A1セル）に合わせる
    For Each cntObj In Worksheets
        '非表示セルは飛ばす
        If cntObj.Visible = True Then
            cntObj.Select
            cntObj.Range("A1").Select
        End If
    Next
    
    '可視シートのうち最も番号の若いものを選択
    For Each cntObj In Worksheets
        If cntObj.Visible = True Then
            cntObj.Select
            Exit For
        End If
    Next
    

ExitHandler:

    Exit Sub
    
ErrorHandler:

    MsgBox "エラーが発生しましたので終了します" & _
           vbLf & _
           "関数名：" & FUNC_NAME & _
           vbLf & _
           "エラー番号" & Err.Number & Chr(13) & Err.Description, vbCritical
        
    GoTo ExitHandler
        
End Sub

```


##3-2.選択した範囲のすべてのセルの値の前か後ろに文字列を挿入するプログラム

###きっかけ

markdownの文章を作成する際、
引用符「>」の挿入や打ち消し線装飾「（文の前後に）~」の挿入を
簡単にできるようにするプログラムが欲しかった。

###ソースコード

```vb:プログラム
'特定のオブジェクトまたは値について
'関数処理をする位置を表す
Public Enum ProcessingPosition
    Front = 1
    Back = 2
    Both = 3
End Enum

'******************************************************************************************
'*関数名    ：insertStrToSelection
'*機能      ：選択した範囲のすべてのセルの値の指定位置に文字列を挿入する。
'*              指定位置：前方・後方あるいは前後両方
'*引数(1)   ：無し
'******************************************************************************************
Public Sub insertStrToSelection()
    
    '定数
    Const FUNC_NAME As String = "insertStrToSelection"
    
    '変数
    Dim insertedStr As String
    Dim insertPosition As Long
    Dim positionStr As String
    Dim cnt As Variant
    
    On Error GoTo ErrorHandler
    '---以下に処理を記述---
    
    '◆◆◆どこに文字を挿入するか：ここを利用目的ごとに変更する。
    insertPosition = ProcessingPosition.Back
    '◆◆◆◆◆
    
    'insertPositionを文字で表現する
    Select Case insertPosition
    Case ProcessingPosition.Front
        positionStr = "値の前方"
    Case ProcessingPosition.Back
        positionStr = "値の後方"
    Case Else
        positionStr = "値の前後両方"
    End Select
    
    '挿入文字列を入力させる
    insertedStr = InputBox("ここに挿入する文字列を入力してください。" & _
                           vbNewLine & _
                           "現在の挿入位置は【" & _
                           positionStr & _
                           "】です。", "挿入する文字列を指定", "")
        
    '各セルの値に文字列を挿入
    For Each cnt In Selection
        Select Case insertPosition
        Case ProcessingPosition.Front
            cnt.Value = insertedStr & cnt.Value
        Case ProcessingPosition.Back
            cnt.Value = cnt.Value & insertedStr
        Case Else
            cnt.Value = insertedStr & cnt.Value & insertedStr
        End Select
    Next
    
        
ExitHandler:

    Exit Sub
    
ErrorHandler:

    MsgBox "エラーが発生しましたので終了します" & _
           vbLf & _
           "関数名：" & FUNC_NAME & _
           vbLf & _
           "エラー番号" & Err.Number & Chr(13) & Err.Description, vbCritical
    
    GoTo ExitHandler
        
End Sub
```