前回は、 Python でのファイル・フォルダ一覧の取得や基本的な操作 を学習しながら、クラウドストレージ(想定)にアップロードされたファイルをより細かなフォルダに分割・保存する方法を学びました。
なぜ、わざわざアップロードされたファイルを更に整理するかというと、検索性の確保とクラウドストレージの共有方法によっては、取引先によりファイルが削除されてしまう恐れがあったからでした。2022 年現在、改正電子帳簿保存法によりクラウドストレージ(訂正・削除の記録がされるもの)による保存が認められていますが、このようなトラブルを避けるのであれば、改正前と同じく認定タイムスタンプを活用するのが安心です。みんなのタイムスタンプ等であれば、低コストから導入できるので、取引数が限られている場合などは、こちらの方が低コストでしょう。
さて、今回は、前回の課題である「書類の種類や内容をファイル名に反映させられない」という問題に、まずは人間の手で対応していきたいと思います。
結局、人間の目と手と頭が必要
AI, 機械学習や RPA, または IoT などの普及により人手を削減、生産性を向上させる取り組みが一般化しています。しかし、そうはいっても未だに最終的に人間の確認の必要性は残っています。書類の正当性やその金額などはその最たるものですね。
ということで、今回は整理のために移動するファイル名を人手でも加工できるようにします。
人手が入るなら大差ないのでは?
そうでもありません。例えば、ファイルを Ctrl + X で該当フォルダに移動するだけで、数秒~数十秒はかかりますし、アップロード日を入力となればなおのことです。
また、前回のスクリプトで移動した後のファイルを編集しようと思うと、自動入力された部分は残してファイル名を編集する必要があり、ミスや工程の増加につながります。生産性の向上のためには繰り返される処理の効率化と、精神面を含めた労働者の負荷の軽減が肝要です。できないもの・かけられないコストは仕方がないですが、できるところは丁寧にサポートできるようにしましょう。
PySimpleGUI を使う
前回の完成 Script であればCUI から入力してもらう方法もあります。ただ、想像以上に CUI, 黒い画面への忌避感は強烈です。今回は Python で GUI を作成できるパッケージの中でも簡単な PySimpleGUI を利用します。
簡単に使える パッケージ、ライブラリの欠点は機能が充分でなかったり、細かなカスタマイズなどができない点です。そういったところを省いているからこそ、簡単に扱えます。今回は、繰り返し処理されるファイルに、名前を付ける程度なので簡単なものを採用しています。
一方で、 Python には Eel などのように HTML+CSS+JavaScript を活用して高度な GUI を構築できるものもあります。 MVC モデルや MVVC でアプリケーションを構築したい場合は、便利でしょう。または、見やすい GUI を作りたいときに、 HTML+CSS で構築するといった用途に使えます。
PySimpleGUI のインストールは、 pip を使い、
pip install pysimplegui
または
py -m pip install pysimplegui
#windows で Py を使い複数バージョンを使っている場合など
でインストールできます。使うときは import PySimpleGUI ですね(長いので as sg とするのが公式流のようです)。
PySimpleGUI は(英語の)ドキュメントが充実しているので、一通りの使い方は公式のドキュメント、特に Cookbook から学習できます。
この記事では実践編ということで、前回のスクリプトに組み込んでいきます。
ロジック・要件を考える
検索要件を満たす上では、
- 取引相手
- 取引における日付・その他の日付
- 金額
が必要になります。1, 2 は前回のスクリプトで自動的に入力されました。その他、法的には定められていませんが、書類の種類(契約書、領収書、請求書、見積書……等)も区別できると便利そうです。また電帳法関連以外の社内で使用される設計書などにも適用したいときなどに、必要となりそうです。これは選択式がよさそうですね。
残るは金額ですが、これは人間が目視で確認する以外、今のところ方法がなさそうです。ただ、元のファイルをいちいち開いて確認するのは大変そうです。ファイルを移動する前に、自動で開いてくれたら、次々と処理が捗りそうです。
今回はこれらの要件を満たすように作って行きます。
PySimpleGUI で GUI をつくる!
ベースになるスクリプトは 前回の記事 を参照。
毎回、ファイルが移動されてしまうとデバッグが大変になるので move メソッドの行をコメントアウトしておきます。また、いつでも元に戻れるようにファイルをコピーしておいてもいいでしょう。
# sh.move(f, '\\'.join([moveto_root, c, year, month, new_name])) #移動先にファイルを移動する
ファイル名を変更してバージョン管理するのは、プログラム開発上よくない習慣です。というか、電帳法の例示を見る限り、国税局がファイルの変更履歴が残っていれば OK としたのも、このどれが最新のファイルか分からない現象を問題視しているから、というようにもとれる資料があります。
とはいえ、プログラムの場合、通常の書類と違って大きく機能を変更 / 追加することがあり単純に履歴が残っていればいいとは言えない面があります。
そういうときには、 Git のようなバージョン管理 / ソースコード管理ツールを使って管理すると個人開発でも便利です。
まずはウィンドウと金額自由入力欄を作る
ファイル先頭の import 群に、
import PySimpleGUI as sg
として、 PySimpleGUI への参照を追加します。
ウィンドウを使い回したり、会社ごと(サブフォルダごと)に一括処理したり、いろいろな応用が考えられますが、今回は 1 ファイルごとに個別にウィンドウを作り処理していくこととします。
今回は各ファイルごとにウィンドウを作成するので moveFiles 関数の先頭に以下のように追記します。
def moveFiles(c, f): # for 文が長くなりすぎないように map 関数で呼び出す関数
layout = [[sg.Text(c), sg.Text(os.path.basename(f))],
[sg.Text('金額: '), sg.InputText()],
[sg.Submit()]]
window = sg.Window(os.path.basename(f), layout)
event, values = window.read()
window.close()
layout 変数
3行に分かれていますが、カンマ区切りにより、事実上 1行です。
2次元の list 変数(layout) に、PySimpleGUI のメソッドの返り値を入れていくことで GUI をレイアウトするのが基本的な使い方となります。改行で直感的に分かる通り、2次元リストで定義される内側(2番目の添え字側)が横方向の行として並び、外側が縦方向の列として並びます。
- Text メソッドは引数をテキスト(キャプション)として表示
- InputText メソッドは1行テキストの入力を表示
- Submit メソッドは Submit ボタンを表示
となっています。
window 変数
Window メソッドの返り値を受け取っている window 変数には、作成されたウィンドウのインスタンスが格納されています。 Window メソッドで新規ウィンドウを作成して以降の操作をこの変数で行います。
また、 Window 関数の第一引数がウィンドウの「タイトル」、第二引数が GUI に表示するパーツを定義したレイアウト(ここでは layout 変数)となります。
- read メソッドで入力された値やイベント(ボタンが押された、など)を取得
- close メソッドでウィンドウを閉じる
実行結果
実行し、上図のようなウィンドウが表示されれば成功です。実際の運用時は、テキスト入力欄に金額を入力して運用することになりますね。
フォーカスをセットする
金額を入力していくと、次々とウィンドウは表示されますが、ウィンドウがアクティブにならなかったり、なってもテキスト入力欄にあわなかったりと、連続して入力していく際に少し使いづらい感じがします。
そこで、フォーカスの設定を行います。
i = sg.InputText()
layout = [[sg.Text(c), sg.Text(os.path.basename(f))],
[sg.Text('金額: '), i],
[sg.Submit()]]
window = sg.Window(os.path.basename(f), layout, finalize=True)
window.force_focus()
i.set_focus()
まず、 layout の前に i(Inputの意味)変数を定義して、InputText() メソッドの返り値を代入します。これは、window 変数のように後から操作するために変数として参照しておくためです。
layout 変数には、 sg.InputText() の代わりに、変数 i をリストの値として代入します。
Window メソッドでは、 force_focus メソッドを使うために、 finalize 引数に True を代入しておきます。
続いて、
window 変数に force_focus メソッドで強制的にフォーカスを当て、続けて、i のメソッド set_focusでウィンドウ内で InputText の入力欄にフォーカスを当てます。
これで、次々と Enter を叩いてもフォーカスが InputText にあいつづけてくれます。
値を受け取る
さて、 GUI を表示したわけですが、入力した値が使えなければ意味がありませんね。ということで、値の受け取り方を見て行きます。
window.read() の readメソッドの返り値で、値は受け取れます。event, values の values 変数がそれに該当します。
print(values[0])
と、window.close() の後に追加して実行してみましょう。
入力した数字が表示されているのが分かります。
値がきちんと受け取れていることが確認できたので、print(values[0])を消して、
price = values[0]
として、変数に代入するように書き換えておきます。要件では、書類の種類を選択できるようにする予定でした。スペースも考えて、ドロップダウンリストが便利でしょう。PySimpleGUI では OptionMenuが使えます。
それでは、 layout の定義を以下のように書き換えます。
i = sg.InputText()
option = sg.OptionMenu(['請求書', '領収書', '見積書', '契約書'])
layout = [[sg.Text(c), sg.Text(os.path.basename(f))],
[sg.Text('種別: '), option],
[sg.Text('金額: '), i],
[sg.Submit()]]
また、 set_focus を i から option につけかえると、最初に OptionMenu にフォーカスが当たっていて、スペースキーとカーソルキーで項目を選択できるので、 UI 的にも使い勝手がよくなります。テキストボックスにカーソルを移したいときは、 Tab キーで移動できますね。
ソースコードの通り、 OptionMenu の第一引数に選択肢の List や Tuple を渡すとそれが表示されます。
こうした Tab キーなどを使った操作は、 OS が違ってもほぼ共通の(CUI時代からの)操作です。しかし、マウスで操作するのが当たり前なので、こういった操作方法を知らないオペレーター・事務職の方は多くいます。
社内でツールを使う場合は、「当たり前」と思わずにマニュアルに記載しましょう。総合的に生産性が向上し、ツール作者の評価もあわせて向上するかもしれません。
無事実行できましたか?
実は、少し問題が起きています。
price に values[0] を代入しているけど……
price に values[0] を代入するようにしましたが、これを print してみましょう。すると、 OptionMenu を追加するまでは正常に動いていたはずなのに、今は金額に入力した値ではなく、種別で選択した値が表示されてしまいます。
カンのいい方ならお分かりの通り、 values[1] に TextInput に入力した内容が格納されています。layout の中に入れた順番に、 values の添え字が増えて行っているだけですね。
なんてことはないのですが、 GUI の要素を足したり引いたりする度に調整するのは面倒ですし、何よりバグの元です。出来れば、 GUI のパーツを安定して一意に指定したいものです。
そういうときに、 PySimpleGUI では key 引数を用います。変数の定義を以下のように変更します。
i = sg.InputText(key='-PRICE-')
option = sg.OptionMenu(['請求書', '領収書', '見積書', '契約書'], key='-TYPE-')
また、window.closeの下に以下のように記述し、変数の代入も変更します。
print('金額:', values['-PRICE-'])
print('種別:', values['-TYPE-'])
price = values['-PRICE-']
type = values['-TYPE-']
実行すると、正常に金額、種別が呼び出せていることが分かります。 また、こうすると数字の添え字では呼び出せなくなるので変数への代入も忘れずに書き換えます。
ファイル名の作成部分を書き換える
前回のスクリプトでは
new_name = '-'.join([date, c, os.path.basename(f)]) #年月日-社名-元のファイル名 の形式のファイル名を作る
となっていたファイル名作成部分を、今回手動で対応したデータを含めるように変更します。
new_name = '-'.join([type, date, c, price + '円']) + os.path.splitext(f)[1] #種別-年月日-社名-金額円.元の拡張子 の形式のファイル名を作る
join メソッドの引数の list に type と price + ‘円’を追加します。更に、 join メソッドの後に、 splitext メソッドで取得した元のファイルの拡張子をつけます。
全て join メソッドの中にいれてしまうと、
種別-20221010-楽介-1000000-円-.pdf
のようになってしまい、見栄えが悪いからです。通貨単位に関しては、もしかしたら – で区切られていた方が、検索性が確保できるかもしれません……。拡張子前については見栄え以外に意味はないので、面倒であれば全て join の list に加えてもいいでしょう。
さて、ここまでで結構実用できそうな形になってきました。最後は、移動予定のファイルを自動的に開く仕組みを整えたいと思います。
中小企業でも海外取引のある会社は多いでしょう。そうすると、通貨単位が様々に入り交じることが考えられます。GUI で選択したり、フォルダの名前で切り替えたり、 スクリプトを別種にしたりなど、対応方法は様々に考えられます。
今後、対応方法の紹介も考えていますが、必要な方は練習も兼ねて対応方法を実装してみるのも楽しいですね。 Python のようなスクリプト言語の場合、簡単な設定であればその場でスクリプト本体を書き換えて対応もできるので、無理に設定ファイルや GUI を作らなくても対応できる場合もありますよ。
ファイルを開いて閉じる
Python は Windows や Mac, Linux 関係なく動作するのが魅力ですが、 os モジュール関連、特にプロセス周りは OS ごとの違いが大きいです。
今回はラッパーパッケージを使い、できるだけどの OS でも動作するようなコードとしていますが、それでも動作しない場合があると思います。ご了承ください。
psutil をインストールする
プロセス周りを楽にしてくれる、 psutil パッケージをインストールします。
pip install psutil
または
py -m pip install psutil
を shell や terminal で実行してインストールします。
また、編集している python ファイルの先頭、 import 文を書いているところに、
import psutil
import time
を書いて、psutil パッケージと、 time パッケージ(こちらはインストール不要です)への参照を作っておきます。
サブプロセスを起動する
def moveFiles 関数の先頭に、
process = psutil.Popen([r'/path/to/execution.ext', f])
として、 psutil で移動予定のファイルを開くように指定します。引数はリスト形式にすると、シェルのように半角スペースで区切る必要がなくなって楽です。上記の場合、シェルで
/path/to/execution.ext 移動予定のファイル.pdf
と言うコマンドを実行したことになります。 /path/to/execution.ext はファイルを開くためのパスとなります。例えば、 Acrobat DC であれば
C:\Program Files\Adobe\Acrobat DC\Acrobat\Acrobat.exe
といった形式になります。実行形式の場所は、ショートカットやスタートメニューを右クリックして「ファイルの場所を開く」をクリックすると簡単に調べられます。ただ、Windows のスタートメニューから「ファイルの場所を開く」をした場合、開いた先もショートカットなので、もう1, 2回繰り返す必要があります。
process 編集に psutil.Popen 関数の結果を代入しているのは、 PySimpleGUI と同じで、後から操作を行うためです。
この状態で実行すると、ファイルごとに次々と新しいウィンドウやタブが開いていきます。これでも最低限、ファイルの確認はできますが、ちょっと鬱陶しいのと、アプリケーションによってはファイルを開いたときに「ロック」してしまい、続くファイルの移動が上手くできない場合があります。そこで、ファイルを移動する処理の前に、起動したサブプロセス(ウィンドウ)を閉じます。
サブプロセスを終了する
window.close() の行の次に
process.kill()
time.sleep(2)
を追加して、新規に起動したウィンドウを閉じます。また、時刻の取得で time というローカル変数を使ってしまっていたので、以下のように書き換えます。
t = dt.datetime.fromtimestamp(os.path.getctime(f)) #ファイルの作成時間を取得して扱いやすい形に
date = t.strftime('%Y%m%d')
year = t.strftime('%Y')
month = t.strftime('%m')
ブラウザのタブで PDF を開いた場合などに上手くいかない場合があるので、そういうときには他のウィンドウを閉じて実行するようにしてください。
また、ただウィンドウを閉じるだけだとファイルの移動命令までにアプリケーションが完全に終了せず、ファイルのロックが解除されない場合があります。そこで、 time.sleep 関数を用いてアプリケーションが完全に終了するのを待ちます。ここでは、 2秒待機していますが、環境などにより時間を調整してください。
Popen 関数に、shell=True 引数を渡すなどで、ファイルに関連付けされたアプリケーションで自動で開くことができます。
ただ、楽介の環境(Windows 11)では、どうやってもこの方法で開いたアプリケーションを安定して閉じることができませんでした。 PyAutoGUI などと組み合わせることで閉じる方法もありますが、それなら Popen 関数に実行ファイルを渡すのと余り変わらないので今回のような方法にしています。
Mac, Linux 環境ではもう少し簡単にサブプロセスを終了できるはずですし、調べた限り、 Windows 環境でも以前はできたようです。
ウィンドウをアクティブにする
ここまでで概ね、期待通りの動作をしています。しかし、入力用のアプリケーションが最前面に来る動作が上手く動いていません。こちらも PyAutoGUI などを使う方法もありますが、ここでeはPySimpleGUI で対処してみましょう。
しかし、 PySimpleGUI にはウィンドウをアクティブにする関数・メソッドは調べた限りではなさそうです。そこで、無理矢理前面に持ってきます。
window.hide()
window.un_hide()
hide メソッドはウィンドウを隠し、un_hide メソッドはその反対で隠したウィンドウを元に戻します。
この2つのメソッドを連続して記述すると、一度ウィンドウを「隠して、復元する」という流れになり、結果としてウィンドウが最前面に来ます。
また、サブプロセスで起動するアプリケーションを待つために、適当な秒数 sleep してから実行するといいでしょう。実際の window 周りのプログラムは、以下のような感じになります。
window = sg.Window(os.path.basename(f), layout, finalize=True)
time.sleep(1)
window.hide()
window.un_hide()
window.force_focus()
option.set_focus()
今回までの全体像
import os
import glob
import shutil as sh
import datetime as dt
import PySimpleGUI as sg
import psutil
import time
uploads_root = r'/path/to/uploads/root' #各社のアップロードフォルダが格納されているフォルダ(一つ上)
moveto_root = r'/path/to/moveto/root' #各社のデータを整理して保存しておくフォルダの一番上
ext = '*.txt' #コピーしたいファイルの拡張子
def moveFiles(c, f): # for 文が長くなりすぎないように map 関数で呼び出す関数
process = psutil.Popen([r'/path/to/exe.ext', f])
i = sg.InputText(key='-PRICE-')
option = sg.OptionMenu(['請求書', '領収書', '見積書', '契約書'], key='-TYPE-')
layout = [[sg.Text(c), sg.Text(os.path.basename(f))],
[sg.Text('種別: '), option],
[sg.Text('金額: '), i],
[sg.Submit()]]
window = sg.Window(os.path.basename(f), layout, finalize=True)
time.sleep(1) #要調整
window.hide()
window.un_hide()
window.force_focus()
option.set_focus()
event, values = window.read()
window.close()
process.kill()
time.sleep(1) #要調整
price = values['-PRICE-']
type = values['-TYPE-']
t = dt.datetime.fromtimestamp(os.path.getctime(f)) #ファイルの作成時間を取得して扱いやすい形に
date = t.strftime('%Y%m%d')
year = t.strftime('%Y')
month = t.strftime('%m')
new_name = '-'.join([type, date, c, price + '円']) + os.path.splitext(f)[1] #種別-年月日-社名-金額円.元の拡張子 の形式のファイル名を作る
print(new_name)
os.makedirs('\\'.join([moveto_root, c, year, month]), exist_ok=True) #移動先のルート\社名\年\月 フォルダを作る
sh.move(f, '\\'.join([moveto_root, c, year, month, new_name])) #移動先にファイルを移動する
for company in glob.glob(uploads_root + '\\*\\'): #アップロードフォルダの中にある取引先フォルダを列挙
company_name = os.path.basename(company.rstrip('\\')) #社名を取得
fileList = glob.glob(company + ext) #移動したいファイルの一覧を取得
print(fileList)
cl = [company_name] * len(fileList) #コピーしたいファイルと同じ数だけコピーされた社名リストを作成
list(map(moveFiles, cl, fileList)) #上で定義したmoveFiles 関数に、社名と各ファイルのフルパスを渡す
次は?
現在のところまでで、「自分で使う」ツールとしてはなかなかに便利なものが出来上がっています。使って見ると、手動で操作する数倍~数十倍の効率で次々にファイルを処理していけるでしょう。何より、「フォルダを開いて、準備して……」という段取りが、Python スクリプトを起動するだけになるので、圧倒的に早くなります。
これは、まとめて処理するときの疲労を低減し、少しずつ処理するときはオーバーヘッドを削減する効果がありますので、総合的な生産性を向上させられます。
一方で、「自分以外」が使うとなると、一回一回黒い画面が出て来たり、ボタンやEnterキー以外でウィンドウを閉じるとエラーで強制終了したり、金額に数字以外が入力できたり……と、オペレーターのミスへの対応が貧弱(というかない)です。
次回は、機能の追加の前に、これらのフールプルーフを実装しましょう。
終わりに
今回は、 PySimpleGUI を使って GUI を実装しました。また、psutil を使ってサブプロセスの管理も行いました。
このように、他のアプリケーションとの関連を持つようになると、いわゆる RPA 用のパッケージを使っていないにもかかわらず、 RPA 的な動き・考え方が必要になってきます。
この点、 Python は PyAutoGUI をはじめ RPA パッケージも使えますし、今回のように GUI も非常に簡単な記述で実現できます(また、インタラクティブな高機能な GUI を実装することもできます)。そのため、 GUI を備えた RPA ツールよりとっつきは悪いですが、「現場が欲しい」ツールを迅速に構築できます。
もちろん、更に使いこなせば機械学習やディープラーニングも見えてきますし、いじっていて楽しい言語ですね。
楽介でした。