QRコード(2次元バーコード)で出退勤打刻システムを実現
私が勤務する会社の勤怠管理は、出勤簿という超アナログな手法です。
労働基準法の改定をはじめとする法定外時間管理の厳格化の流れが中小企業にも訪れたことに伴い、ようやく勤怠管理のWEBサービスを使用することに。
勤怠システムは、今まで使用していたグループウェアと連携してシングルサインオンで打刻できる、AKASHIを使用することに決定。
使い勝手を優先した選定にもかかわらず、試験段階で次のような意見が。
・出社してパソコンを起動しないと打刻できないのが煩わしい
とある地方中小企業の社員より
・退社時、退勤打刻を忘れてパソコンをうっかりオフしてしまう
・共有打刻PC機能を使っても、自分の名前を選択したりが面倒
社員証がICカードにでもなっていれば良いものの、ICカードどころか社員証も無いという悲しい現実。
というわけで、下記を実行するシステムを自作することにしました。
- Webカメラから映像を取得する
- 取得した映像から2次元バーコードを読み取り、デコードする
- デコード結果に応じて、HTTP POSTによってAKASHIの公開APIを叩く
- 余っているPCを電源自動運用し、上記の処理を実行させる
当記事では、これらの実現方法を寄せ集めた過程を記載した上で、最後に赤っ恥覚悟のPythonコードを公開します。
Pythonでの実現性を確認
上の一連の処理のうちどこか一つで躓いてもたちまち野望は頓挫するわけです。
週末へっぽこプログラマーを自認し、家族に白い目を向けられても家族サービスそっちのけでPCに向って時間を費やす筆者にとって、それが与えるダメージは痛恨の一撃です。
しかしPythonの素晴らしいところは、豊富なライブラリ群を無料で使え、その利用のためのTipsがWEBにたくさんあること。
つまり偉大な先行者が残す記録たちを事前に探った上で、できそうならばそれをパクれば、いや、引用すれば良いわけです。(いろいろなご批判もあるでしょうけど、リスク等は重々承知の上です)
WEBカメラを使ったQRコードリーダー
ライブラリ
OpenCV、PIL、pyzbar
参考コード
一番肝となるQRコード読み込み。下記のサイトのコードを拝借しました。ほぼそのままで動きます。深謝です。
WEBカメラ
ときは新型コロナウィルス(COVID-19)の緊急事態宣言発令時ということで、テレワーク需要からWEBカメラがネット上で軒並み値上げ。とあるルートで偶然入手したLogicool C270を使いましたが、偶然にもこれがベストフィット。
筆者はWEB会議システム関係の仕事をしたこともあるので、LogicoolのWEBカメラの性能の良さはよくわかっていましたが、C270は下位機種過ぎてWEB会議利用では手を出したことはありません。
Logicool製品上位機種は、フルHD解像度でオートフォーカスのものも1万円前後で手に入るため、あえて固定フォーカスの720Pを使う理由はなかったのです。
しかし、今回は、これでいい。いや、これがいい。
- 余計な機能がないから、安い!(3000円台で2年メーカー保証つき)
- QRコードをかざす距離にカメラをフォーカス固定すれば、オートよりむしろ認識が早い
しかも、固定フォーカス距離は、本体のカバーをカパット開けて自分で物理的に調整が可能です。
下のサイトが詳しいです。
勤怠システムAPIをHTTPで叩く
使用する期待システム:AKASHIは、APIが公開されています。
ライブラリ
HTTP POSTでパラメータを渡し、レスポンスはJSONのテキストデータ。
ライブラリはRequestsをimportします。
POSTデータの作成、HTTPリクエスト送出
下記のサイトを参考にして、出退勤に必要な各パラメータをAKASHIのアプリサーバーにPOSTします。
JSONレスポンスをパースする
JSON形式のデータは深く階層化されていることが多いため、自力でテキスト処理などしようものならたいへんです。Requestsライブラリのjson()関数で容易に必要なデータを取り出すことができます。参考サイト。
期限付きトークンを管理する
公開APIを使うには、使用者の識別及びデータ保護のために認証が必要になることが常です。
中にはリクエストの都度一時トークンが発行されるものや、目的のAPIとは別にサイトの会員認証用APIでログインする必要があるなど、なかなか厳重なものも見受けられます。
AKASHIの場合は、打刻等を実行する会員ごとに管理される「アクセストークン」をHTTPリクエスト時にセットします。
それだけなので非常に処理はわかりやすいのですが、アクセストークンの有効期限は1ヶ月プラス1日。つまり今回の手作り簡易打刻システムでPOSTするデータを定期更新する必要があります。
しかしそこは抜かりなく、「アクセストークン再発行」も公開API化されているという親切設計。
とはいえ毎回更新するのも無駄なので、打刻システム用PCのローカルにトークンをテキストファイルとして保存しておき、それを更新・上書きする別のPythonプログラムを用意して、そっちは月に2回だけ動かすことにしました。
PythonでのCSVファイル読み書きについてはたくさん情報がありますが、今回は社員番号とトークンを対応させてファイル化するため、辞書として読み書きできれば便利。標準クラスで使えるDictReaderを使いました。
参考サイト↓
Windowsの電源自動運用
これは、出退勤システム自体とは関係なく、単なるWindows PCの電源運用の話なので詳細は省略します。
簡単に言えば下の流れ。
BIOSで起動
→ 休日判定。今回は、PINGで業務システムのサーバー生死確認
→ 休日ならシャットダウン、そうでなければPython実行
→ 夜になったら、タスクスケジューラーでPythonのプロセスををKill
→ shutdownコマンドを実行
実装。そしてトラブルシュート
ここまででようやく、実現できそうな感触を使み、どんどんコピペでコーディング。真のプログラマーではないため恥もプライドもありません。
そしてそういうズルする人間には、必ず試練が訪れます。
tkinter canvasが更新されない
ライブラリtkinterにてGUIウィンドウを作成します。
そこにWEBカメラから取得した画像を定期的に更新する流れです。
QRコードを読み取って出勤のHTTPリクエストに成功したら、tkinterでcreate_image()して次の画像を数秒間出します。

しかし、それをやろうとしても、画像表示タイミングが遅れる上に、create_image()後にsleep()関数などでディレイを入れたつもりでも一瞬だけしか画像が表示されない。
どうやら、WEBカメラの取得画像を描画するタイミングと、それとは別関数内で特定画像をcanvasに反映するタイミングが意図通りにならない模様。
しかしPythonの内部処理なので直接にはコントロールできません。
低級言語の側面を持つC言語などで組込みシステムを動かす場合には、フレームバッファという特殊なメモリ領域にアプリで描画した上で、LCDドライバに転送する、といった画面更新処理を意識的に実装します。
また、タスク優先度、ディスパッチタイミング、セマフォでのリソース排他など、RTOS特有のマルチスレッド設計を少しかじったことがある筆者としては、JavaやPythonなどの高級言語でそれらを意識しないで済むのが楽に感じる一方で、ガベージコレクションのトリガなど、ややもすれば非常にバグの温床となるような重要な処理を言語エンジン側の内部処理としてブラックボックス化されることに怖さも感じます。
Python with tkinter での描画タイミングについても同じことが言え、描画を止める、もしくは強制的に任意タイミングで描画させるといった制御手段が無い(わからない?)のはもどかしいです。
仕方ないので、出退勤画像の描画をcreate_image()で指示した後、waitするのではなくしばらくWebカメラ画像のcreate_rectangle()処理をスキップして処理ループだけ回してみたところ、その間所望の画像が表示されました。
画像表示を要求したら処理スキップ用の変数に値をセットし、スキップの都度その変数をデクリメントします。
変数がゼロのときだけ、WEBカメラ画像を描画するようにしました。
モニタの電源ON処理に苦戦
できるだけモニタ電源はオフしておき、QRコードがWebカメラにかざされたときだけONしたい。
無操作15分でモニタ電源OFFになるようWindowsの電源設定をした上で、Pythonで必要なときにモニタ電源を起こそうと考えました。
PythonはWin32 APIも叩けるのでPostMessageで余裕かと思いきや、一瞬だけついてすぐモニタが消えてしまう。
マウスカーソルを擬似的に動かせば大丈夫という記事を発見し、ctypes.windll.user32.SetCursorPos()を使うも改善せず。
pyautoguiでもだめ。
半ばあきらめかけたところ、下のサイトで、Win32APIの.SendInputを使うやり方が紹介されていたので、やってみたところ、これが大成功!深謝!
なぜかどんどんWEBカメラ画像の反映が遅くなる
Webカメラの取得画像をtkinterで一定時間ごとに描画し続けてしばらくすると、カメラに写した映像が描画されるまで、数秒もかかる状態に。
メモリリーク?不要なスレッドの発生?などと考えてタスクマネージャーでプロセスの状況を見てみるも、起動直後と大きな違いはない。
上で書いたとおり、描画関数は500msごとにafter()で再呼び出ししています。その都度きっちりと同期処理で描画が完了してから関数を抜けているとしたら、こんなに遅れるはずがない。
上の「tkinter canvasが更新されない」の問題に苦心してからというもの、描画タイミングを制御できないことにすっかりガクブル状態の筆者は、「描画するデータが溜まっているのでは?」とざっくり推論。
よくわかりませんが、delete("all")で、tkinterのキャンバス上のアイテムを全削除する処理を10分ごとに実行してみることにしました。
とりあえず改善した様子なので、実運用まで様子見。最悪は、タスクスケジューラーでpyプログラムのkillと再実行でしょうか。
その他の細かな工夫
ゆる画像をランダム表示し和ませる
QRコードをかざして表示される画像は、フリー画像から十数枚選択してランダム表示させます。
「会社」という堅苦しい空間にわずかでも安らぎをもたらしたいため、鳥獣戯画のフリー素材を使わせていただきました。
出勤/退勤のミス防止
出勤打刻なのか退勤打刻なのかは、読ませるQRコードを変えるだけです。
カード裏表で出勤/退勤を分けるつもりなので、裏表でカードの色を変えるなどわかりやすくする予定ですが、それでも「出勤打刻のつもりは間違えて退勤してしまった」などのミスはつきもの。
そこで単純に、時間を区切って早すぎる退勤打刻や遅すぎる出勤打刻はエラーにするようにしました。
ログを外部ファイルに記録
へっぽこ週末プログラマーの産物とはいえ、企業の出退勤時刻の管理は会社としても労働者としても重要です。
想定外のシステムエラーが発生したり、悪意を持った誰かが勤怠システムの記録をいじったり、といったことがないとも限らないので、テキストファイルにログ出力するようにしました。
Pythonではloggingライブラリに体系的によってログ出力を管理できます。
設定ファイルは外部ファイルとしてまとめました。↓参考サイト。