#この記事のモチベーション#
昨年、生産技術職からデータ分析業に転職し、Pythonを書く機会が多くなりました。今は機械学習用の前処理ツールを開発する案件に携わっています。そんな中、生産技術者として働いていた時のことを顧みると自動化できた作業が色々あったなーと思いました。転職後に得たPythonの知識と生産技術者時代の知見を踏まえて、再び製造現場で働く際に使えそうなネタを記載します。同じようなニーズに直面している方の参考にもなれば幸いです。
本記事は既存技術を組み合わせてこうしたら良さそう!ということを記載したものであり、技術的に目新しいことがない点を初めに断っておきます。
#自動化する内容#
生産技術者として働いていた時に工程から出てくるデータを1日の終わりに集約するという作業がありました。製造装置のログや検査結果を製品ごとにcsvファイルで保存し、1日の終わりに集約してその日の進捗率や不良率を確認します。担当工程では、以下の図のように1つ製品を製造するごとに4つのcsvが出力される仕様になっていました。
◎ 製造装置の設定値ログ
◎ 製造装置の状態ログ
◎ 製品の形状データ
◎ 形状データから算出した判定結果
形状を判定する箇所はPythonで作られていたのですが、これは判定方法を私が指示して情シス部のプログラマーに作成を依頼したものです。
上記4つのcsvを毎日1つのExcelファイルに集約していました。つまり4×製造台数 個のファイルをまとめていた訳です。その日の生産終わりからまとめ始めると、完了が夜分になってしまうため、生産の合間に製造員の方に集約いただくようお願いしていました。
当然ですが、この作業は全く人がやる必要ありません。隙間時間にやってもらっていましたが、製品に付加価値を生む作業ではないので、時間を割いてもらうべきではなかったと反省しています。その時間を不良低減のアイデア出しなど(=人にしかできない仕事)に割いてもらうべきでした。今更ではありますが、現在の知識でこの作業を自動化する方法を記載します。
#自動化ツールを作成する際に留意すること
Pythonで自動化ツールを作り、製造現場で活用するためにいくつか念頭に置かないといけないことがあります。前提として、生産技術者(=私)がツールを作成し、製造員(=実際に製品を作る方)に使っていただくことを想定します。
####◎プログラミング知識ゼロの人が実行できる形にする
製造現場には、生産技術者を含めてプログラミングの知識がある人はほとんどいないと思ったほうが良いです。ツールは基本的にクリックだけで実行できる形で渡しましょう。これは、製造員に手間をとらせない観点からもベターです。また、ツールの動作がPythonのパッケージに依存する場合、「pandasは0.25.1を入れて、openpyxlは3.0.0を入れてください。、、」とお願いするのは無理があります。環境構築なしで実行できる形で渡す必要があります。
####◎日常的に変更がありそうな項目はPythonスクリプトに書かない
例えば、現場の都合でデータの保存先を変更することがあります。その場合に、ファイルのパスをPythonスクリプト内に書いてしまうと変更がある度にPythonスクリプトを書き換える必要が発生します。Pythonが読み書きできる人が現場にいないと対応できない事態になります(=その度に現場に呼ばれる事態になります)。
####◎現場のPCにpipを使ってパッケージをインストールできないと思ったほうが良い
企業や工場のルールによりますが、製造現場のネットワークはそのライン内、もしくは工場内で閉じおり、外部に繋がっていないことが多いです。製造条件や取得されるデータはメーカーの第一級機密事項なので、万が一にも外部に漏れないように物理的に外部とネットワークが遮断されてることがほとんどだと思います。今やっている案件でも、データを見に顧客の工場に出向いています。
####◎動作中にMicrosoft Officeが問題なく使える仕様にする
製造現場のPCにOfficeがインストールされている場合、ツール動作中もOfficeが問題なく使える(=フリーズしない)ことが求められます。と言いますのは、同じPCを使って製造員の方がExcelで日報を書かれたり、過去のデータが集約されたExcelを確認することがあるからです。処理中にOfficeが使えないと評判が激落ちするでしょう(笑)。
####◎OSSだけで構成する
生産技術者(ノンプログラマー)が自動化ツールを自作するのでOSSの使用が前提になります。また、顧客にプロダクトとして納入する際も、ツールを引き渡した後は顧客の製造・情シス部門でなるべくメンテしていきたいという理由から、OSSだけで作成することを要求されることがあります。
####◎あとは現場(=製造員)の声をよく聞く
全てを包含するようなことを書いてしまいましたが(笑)、それぞれの現場によって要望が異なるのでよくヒアリングしてから作成に取り掛かりましょう。
例えば、上記のcsvファイルを集約する例だと、
● 製品毎に処理(つまり、その日の生産が終わると同時に集約も終わっている)
● 生産が終わった後に一括処理
の2パターンが考えられます。
こういった細かい仕様は、事前によくヒアリングしてユーザビリティを最大化したいものです。今回は「製品毎に処理する」ことを例に説明します。
(注)判定結果が連続してNGの場合、工程異常としてラインを緊急停止する必要があるので、判定自体は製品毎に実施することが基本です。
#ツール作製に使えそうな技術
上記のそれぞれの項目に使えそうな技術を記載します。
| 留意点 | 対応策 | 使用するライブラリ |
|-----|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
|プログラミング知識ゼロの人が実行できる形にする| ・Pythonスクリプトをexe化する
・ツールはバッチファイルで提供する | PyInstaller |
|日常的に変更がありそうな項目はPythonスクリプトに書かない | 変更がありそうな項目は、コマンドライン引数(sys.argvの利用)や設定ファイルから読み込む形式にする | sys |
|現場のPCにpipを使ってパッケージをインストールできないと思ったほうが良い | Pythonスクリプトをexe化する | PyInstaller |
|動作中にMicrosoft Officeが問題なく使える仕様にする | (例)処理中にExcelが安定して使えるための処置
Excelを長時間操作する場合(大量のテキストを書き込むなど)は、PythonではなくVBScriptを使ったほうがベター | openpyxl
(処理時間が短い個所) |
|生産が終了した段階で処理が終わっているようにする(要望による) | 製品毎に処理する | watchdog |
もう少し詳しく説明します。
◎「プログラミング知識ゼロの人が実行できる形にする」への対応
これは、作成したPythonスクリプトをPyInstallerというライブラリを使ってexe化することで可能になります。exe化することで、製造現場での環境構築が不要になります(exeの中に環境が一緒に入っているイメージ)。更にexeをバッチファイルにしてに提供することでユーザーはバッチファイルをダブルクリックするだけでツールを使えるようになります。
参考にさせていただいたサイト
● PyInstallerでexeファイル化
● バッチファイルの作成
◎「日常的に変更がありそうな項目はPythonスクリプト内に書かない」への対応
変更がありそうな項目はコマンドライン引数(バッチファイルに記載)を使ったり、別途設定ファイルを作成して、そこから読み込む形にしておきます。そうしておけば、現場で変更したい個所が発生した場合にも、バッチファイルをテキストエディタで書き換えれば良いだけなので、Pythonの知識がない人でも対応できます。
参考にさせていただいたサイト
Pythonでコマンドライン引数を扱う方法
◎「現場のPCにpipを使ってパッケージをインストールできないと思ったほうが良い」ことへの対策
上記1つめの対策と同じ
◎「動作中にMicrosoft Officeが問題なく使える仕様にする」への対応
PythonでExcelファイルを操作する場合、openpyxlというライブラリを使うのがスタンダードだと思います。しかし、過去にopenpyxlを使って複数のシートを集約していく作業中に全く関係のない別のExcelファイルを操作できない状態になったことがあります(原因を掴めていません。。)。なので、今回はcsvを集約する箇所はPythonではなく、VBScriptを使うことにしました。Excelに限らず、ツール動作中にMicrosoft Officeが題なく使えることを確認しておいたほうが良いと思います。
◎「生産が終了した段階で処理が終わっているようにする」への対策
形状データが格納されるフォルダをwatchdogというライブラリで監視して、データが格納されたタイミングで処理を開始することにします。そうすることで、ユーザーは生産開始前にツールを起動(=バッチファイルをダブルクリック)し、その日の生産が終わった時点でバッチファイルを止める(=Ctrl+C) だけで良くなります。
参考にさせていただいたサイト
ファイルの更新をきっかけにコマンド実行 (python編)
上記のサイト以外に、
「退屈なことはPythonにやらせようノンプログラマーにもできる自動化処理プログラミング」を参考にしました。
#実際にコードを書いてみる
以下では、上の留意点を踏まえて複数のcsvファイルを1つのExcelファイルに自動的に集約するコードを記載します。以降はメモだと思ってください。。
###動作内容
以下の図のような処理を考えました。
製品は左から右に流れ、処理は上から下に向かって進むと考えてください。
登場するファイルは5つ、スクリプトは3つです。
ファイル:
◎製造装置から出力される設定値ログ(csv)
◎製造装置から出力される状態ログ(csv)
◎形状測定機から出力される形状データ(csv)
◎形状データを基に算出した判定結果(csv)
◎4つのcsvを集約したExcel
これらのファイルはそれぞれ別のフォルダに保存されます。
スクリプト:
◎integration.py
【役割】
処理の流れを制御する
【動作】
・形状データが保存されるフォルダを監視し、ファイルが発生したら形状を判定する
(=ShapeJudgment.pyを実行する)
・判定結果が保存されたら、csvを集約するexcel_merge.vbsを実行する
・集約が終わったら、csvを「集約済みフォルダ」に移す
◎ShapeJudgment.py
【役割】
形状データを基にOK/NG判定する
【動作】
形状データを読み込んで判定結果を保存する
(注)判定結果は品質に直結するので、情シス部門の熟練者が作成したコードを流用
◎excel_merge.vbs
【役割】
csvファイルを1つのExcelファイルに集約する
【動作】
csvファイルを読み込んで、Excelファイルのシートとして追加していく
最終的にユーザーに提供するバッチファイルのコマンドライン引数は以下のようにしました。ややこしいですが、、
特にややこしいのは、integration.py内で、コマンドライン引数にsys.argv[1]、sys.argv[4]、sys.argv[5]を渡してShapeJudgment.pyを実行している箇所です。ShapeJudgment.py内ではそれらはsys.argv[0]、sys.argv[1]、sys.argv[2]に対応します。
コマンドライン引数を使うことで、Pythonスクリプトに具体的なパスを書く必要がなくなり、パスを変更したい場合はバッチファイルだけを編集すれば良いことになります。
内容 | バッチファイル内での位置 | integration.py内での対応 | ShapeJudgment.py内での対応 |
---|---|---|---|
integration.exeのパス (integration.pyをexe化したもの) |
第0引数 | sys.argv[0] | - |
ShapeJudgment.exeのパス(ShapeJudgment.pyをexe化したもの) | 第1引数 | sys.argv[1] | sys.argv[0] |
「設定値ログ」が保存されるフォルダの パス |
第2引数 | sys.argv[2] | - |
「装置状態ログ」が保存されるフォルダの パス |
第3引数 | sys.argv[3] | - |
「形状データ」が保存されるフォルダの パス |
第4引数 | sys.argv[4] | sys.argv[1] |
「判定結果」が保存されるフォルダのパス | 第5引数 | sys.argv[5] | sys.argv[2] |
「集約されたExcelファイル」が保存されるフォルダのパス | 第6引数 | sys.argv[6] | - |
excel_merge.vbsのパス | 第7引数 | sys.argv[7] | - |
処理の流れは以下のようになります。1製品につき、10ステップの処理を行います。
製品のタクトタイム(流れてくる間隔)に対して、10ステップに要する時間は十分短いという前提で進めます。タクトタイムが短いと、処理中に次の製品が流れてくる事態になるので処理方法を工夫しないといけないと思います。
順 | 出来事 | 保存先 (バッチファイルのコマンドライン引数) |
---|---|---|
0 | integration.pyで 形状データが保存されるフォルダの監視を開始する |
|
1 | 当該工程に製品が流れてくる | |
2 | 製造装置で製品が加工される | |
3 | 設定値ログが保存される | sys.argv[2] |
装置状態ログが保存される | sys.argv[3] | |
4 | 形状測定機で製品を測定する | |
5 | 形状データが保存される | sys.argv[4] |
6 | integration.pyからShapeJudgment.pyが実行される | |
7 | 判定結果が保存される | sys.argv[5] |
8 | integration.pyから excel_merge.vbsが実行される |
|
9 | 集約されたExcelファイルが 保存される |
sys.argv[6] |
10 | integration.pyで 集約済みのcsvを別のフォルダへ移動する |
各コマンドライン引数の親フォルダ内 |
11 | 1に戻る (次の製品が流れてくる) |
###コード例
実際に動きを確認したコードを記載します。
実行環境
OS:Windows 10 Home バージョン1909
Python 3.7.6
watchdog 0.10.2
openpyxl 3.0.0
■処理の流れを制御するスクリプト
# 必要なライブラリのインポート
import datetime
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import subprocess
import sys
import os
import openpyxl
import shutil
import pathlib
# ShapeJudgment.pyとexcel_merge.vbsを実行するクラス
class Execute(FileSystemEventHandler):
def __init__(self, path):
self.path = path
def _run_command(self):
'''
コマンドライン引数にShapeJudgment.pyのパス(sys.argv[1]),
「形状データ」が保存されるフォルダのパス(sys.argv[4]),
「判定結果」が保存されるフォルダのパス(sys.argv[5])
を指定してShapeJudgment.exeを実行する
'''
command = [sys.argv[1],sys.argv[4],sys.argv[5]]
subprocess.run(command, shell=True)
'''
集約したExcel(sys.argv[6]内に「日付け+_集約」の名前)がなければ作る
通常、その日の1台目の製品が流れる際に作られる
'''
if not os.path.isfile(
os.path.join(sys.argv[6], datetime.date.today().strftime('%Y-%m-%d') + "_集約.xlsx")):
wb = openpyxl.Workbook()
wb.save(os.path.join(sys.argv[6], datetime.date.today().strftime('%Y-%m-%d')) + "_集約.xlsx")
'''
各csv(sys.argv[2] ~ sys.argv[5])を渡して、VBScriptを実行する
集約したExcelはsys.argv[6]内に「日付け+_集約」の名前で保存される
'''
command = [r"wscript",
sys.argv[7],sys.argv[2],
sys.argv[3], sys.argv[4],
sys.argv[5],os.path.join(sys.argv[6],datetime.date.today().strftime('%Y-%m-%d'))+"_集約.xlsx"]
subprocess.run(command, shell = True)
'''
各csvの保存場所 sys.argv[2] ~ sys.argv[5]の親に「集約済み」フォルダが無ければ作る
そして集約し終わったcsvを「集約済み」フォルダに移す
「集約済み」フォルダに既に同じ名前のファイルがある場合はパスする
'''
for i in range(2, 6):
if not os.path.isdir(os.path.join(pathlib.Path(sys.argv[i]).parents[0], "集約済み")):
os.mkdir(os.path.join(pathlib.Path(sys.argv[i]).parents[0], "集約済み"))
try:
shutil.move(sys.argv[i] + "\\" + os.listdir(sys.argv[i])[0],
os.path.join(pathlib.Path(sys.argv[i]).parents[0], "集約済み"))
except shutil.Error:
pass
def on_created(self,event):
self._run_command()
'''
検査装置の形状データ(csv)が保存されるフォルダを監視する関数
ファイルが新たに発生したらExecuteクラス内のメソッドが実行される
監視対象は「形状データ」が保存されるフォルダ(sys.argv[4])
Ctrl+Cが押されるまで監視し続ける
'''
def watch_shape(path = sys.argv[4]):
while True:
event_handler = Execute(path)
observer = Observer()
observer.schedule(event_handler,path,recursive = True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == '__main__':
watch_shape()
■形状データに基づきOK/NG判定を保存するスクリプト
import pandas as pd
import numpy as np
import os
import sys
import glob
# 形状データを基に判定結果を保存する関数
def main():
'''
形状データを読み込む(同時刻にcsvファイルは常に1つしか存在しない前提)
ファイルが無ければpassする
sys.argv[1]は形状データが保存されているフォルダのパス
'''
try:
shape_df = pd.read_csv(os.path.join(sys.argv[1] , glob.glob("*.csv")[0]))
except IndexError:
pass
else:
'''
・・・・・判定アルゴリズムが動作・・・・・
ここは品質に直結する箇所なので、判定方法を指定した上で
情シス部の熟練者に書いてもらう
'''
'''
判定結果(judgment_df)を保存する
sys.argv[2]は判定結果を保存するフォルダのパス
ファイル名は「判定結果_形状データ.csv」
'''
judgment_df.to_csv(os.path.join(sys.argv[2], "判定結果_"+glob.glob("*.csv")[0]))
if __name__ == '__main__':
main()
■4つのcsvファイルを1つのExcelファイルに集約するスクリプト
'--------------------------------------------------------------------------------------
'
' 装置から出力されるcsvファイルをシートごとに1つのExcelファイルにまとめる
'
'
' 注意点 スクリプト実行中は集約するExcelファイルを開かないでください。
' スクリプト実行前に集約するExcelファイルのロックを解除してください(読み取り専用でない状態)。
' 製造装置、検査装置の出力先フォルダにはcsvファイル以外を入れないでください
' このスクリプトが異常終了した場合はExcelのプロセスを手動で終了してください。
'
'
'--------------------------------------------------------------------------------------
Option Explicit
Dim fileSystem
Dim targetFolder
Dim fileList
Dim Excel
Dim merge_Workbook
Dim objWorkbook
Dim objSheet
Dim wkFile
Dim i
'sys.argv[2] = WScript.Arguments(0) : 製造装置の設定値ログ(csv)があるフォルダのパス
'sys.argv[3] = WScript.Arguments(1) : 製造装置の状態ログ(csv)があるフォルダのパス
'sys.argv[4] = WScript.Arguments(2) : 検査装置の形状データ(csv)があるフォルダのパス
'sys.argv[5] = WScript.Arguments(3) : 検査装置の判定結果(csv)があるフォルダのパス
'sys.argv[6] = WScript.Arguments(4) : 上記csvを集約したExcelファイルのパス
'Excelオブジェクトを開く
Set Excel = CreateObject("Excel.Application")
'Excelの表示設定(この設定をしておけば処理中にExcelが開いたり閉じたりしない)
Excel.Visible = False
Excel.DisplayAlerts = False
Excel.ScreenUpdating = False
'集約するWorkbookを読み込む
Set merge_Workbook = Excel.Workbooks.Open(WScript.Arguments(4))
'ファイルごとにループする
For i = 0 To 3 '集約するcsvのファイル数だけループする
'各フォルダのファイル一覧を取得する(ファイルは1つしかない前提だが一応)
'ファイルシステムを扱うオブジェクトを作成
Set fileSystem = CreateObject("Scripting.FileSystemObject")
Set targetFolder = fileSystem.getFolder(WScript.Arguments(i))
Set fileList = targetFolder.Files
For Each wkFile In fileList
Set objWorkbook = Excel.Workbooks.Open(WScript.Arguments(i) & "\" & wkFile.Name)
'シートオブジェクトを取得(コピー元のファイルのシートは1つという前提)
Set objSheet = objWorkbook.Sheets(1)
'シートオブジェクトを取得(コピー元のファイルのシートは1つという前提)
objSheet.Copy ,merge_Workbook.Sheets(merge_Workbook.Sheets.Count)
'シート名はcsvファイルのファイル名とする(".csv"は削除する)
'merge_Workbook.Sheets(merge_Workbook.Sheets.Count).Name = replace(wkFile.Name,".csv","")
'csvファイルを閉じる
objWorkbook.Close
Set objWorkbook = Nothing
Next
Next
'初めの回だけSheet1を削除する
If merge_Workbook.Sheets.Count < 6 Then
'Sheet1を指定して削除
merge_Workbook.Sheets(1).Delete
End If
'はじめのシートをアクティブシートに設定
merge_Workbook.Sheets(1).Activate
'集約するWorkbookを上書き保存
merge_Workbook.Save
'集約するWorkbookを閉じる
merge_Workbook.Close
Set merge_Workbook = Nothing
'Excelの終了
Excel.ScreenUpdating = True
Excel.Quit
Set Excel = Nothing
以上のコードを動かすと、(4×製造台数)個のcsvファイルが1つのExcelファイルに集約されます。
このまとめ方はかなりイケていませんが、あくまで例ということで(笑)
###バッチファイルの作り方
次にバッチファイルを作る方法について説明します。
バッチファイルは2段階の順を踏んで作ります。
1. Pythonスクリプトをexe化する
2. 各種パスをコマンドライン引数としてバッチファイルを作る
まず1.のexe化ですが、前述のとうりPyinstallerを使って行います。
コマンドプロンプトでintegration.pyとShapeJudgment.pyが存在するディレクトリに移動した上で以下のコマンドを実行してください。
$ pyinstaller integration.py --onefile
$ pyinstaller ShapeJudgment.py --onefile
すると、
44536 INFO: Building COLLECT COLLECT-00.toc completed successfully.
というメッセージが表示された後、同じディレクトリにdistというフォルダが作成されます。そのフォルダの中にintegration.pyとShapeJudgment.pyがそれぞれexe化された
integration.exeとShapeJudgment.exeが入っています。
次に2.のバッチファイルの作成を行ます。
以下の内容をメモ帳などのテキストエディタに書いて、拡張子を「.bat」にして保存してください。1行目はコメントです。それぞれのパスの間には半角スペースが入ることに注意してください。また^(キャレット)は改行する際に必要な記号です。
@rem integration.exe実行するbatファイル
"integration.exeのパス" ^
"ShapeJudgment.exeのパス"^
"「設定値ログ」が保存されるフォルダのパス"^
"「装置状態ログ」が保存されるフォルダのパス"^
"「形状データ」が保存されるフォルダのパス"^
"「判定結果」が保存されるフォルダのパス"^
"「集約されたExcelファイル」が保存されるフォルダのパス"^
"excel_merge.vbsのパス"
↓記載例
@rem integration.exe実行するbatファイル
"C:\Users\PycharmProjects\untitled2\dist\integration.exe" ^
"C:\Users\PycharmProjects\untitled2\dist\ShapeJudgment.exe"^
"C:\Users\input\製造装置ログ\製造装置の設定値ログ"^
"C:\Users\input\製造装置ログ\製造装置の状態ログ"^
"C:\Users\input\検査装置ログ\検査装置の形状データ"^
"C:\Users\input\検査装置ログ\検査装置の判定結果"^
"C:\Users\output\集約したExcelファイル"^
"C:\Users\VBS\excel_merge.vbs"
3行目~10行目はそれぞれintegration.py内に書いたsys.argv[1] ~ sys.argv[7]に対応しています。
###結局、何を製造現場のPCに入れれば良いのか?
色々ファイルが出てきたのですが、最終的に製造現場のPCに入れるファイルは以下の4つです。当然ですが、バッチファイルに書いたパスは製造現場のPCに合わせて変更してください。
その日の生産開始前に製造員の方に「実行ファイル.bat」をダブルクリックしてもらいます。すると製品が流れる度に4つのcsvファイルが1つのExcelファイルに自動的に集約されます。生産終了後に「Ctrl+C」を押してもらいバッチファイルを止めます。その時点でExcelファイルへの集約が完了しているというわけです。
以上の処理は一例ですが、
上に書いたライブラリを使って製造現場でニーズがある色々な自動化に対応できそうです。
#注意点
上記方法にはいくつか注意点がありますので、記載しておきます。
■exeファイルが巨大になることがある
Pyinstallerを使ったexe化では、作成時のPython環境にあるライブラリが全て取り込まれてしまいます。そのためファイルサイズが巨大化し、exeの動作が遅くなることがあります。これを避けるために、使わないライブラリを除いてexe化するのが良いです。特定のライブラリを除いてexe化する方法は、以下の記事が参考になります。
Pythonで組んだプログラムをPyInstallerでexe化したらめちゃくちゃサイズがでかくなった件【2019年6月、解決策追記】
■集約されたExcelファイルを生産中に確認できない
書き込み動作をしている最中にExcelファイルを開いていると、書き込みエラーが発生します。なので、生産中に集約されたExcelファイルを確認したい場合は、別途コピーしてそちらを開く必要があります。
#所感
昨今、プログラミングのハードルが格段に下がってきているので、ノンプログラマーがプログラミングを使って業務の自動化を行う機会が多くなると思います。その際に重要になるのは、プログラミングスキルよりも現場のニーズをきちんと把握するコミュニケーション能力と、「現場で使えるツール」を作る姿勢かもしれません。