👋

Entersys作成記

2023/11/02に公開

はじめに

こんにちは.まずはこの記事を開いてくれてありがとうございます.

これは私が所属するMMAというサークル(総合格闘技じゃないよ)の部室の入退室管理システムEntersysを作成した際の記録です.

初心者が初心者なりに作成して記事を書くので,拙い部分も多いとは思いますが,最後まで読んでいただければ幸いです.

Entersys とは

Entersysは,MMA の部室の入退室管理を行うシステムです.

この名前の由来は,EnterSystemを組み合わせたものです.MMA には...sysというシステムが数多く存在しています.その慣習に合わせて,Entersysという名前にしました.(入ることしか考えてないネーミングですね.いったい誰が考えたんでしょう.)

実際に動作すると次の画像のようになります.

bg right 30%

作成の経緯

数年前に新型コロナ(COVID-19)が流行し,部室に誰が出入りしたのかを把握する必要が出てきました.これまでは Slack 上に毎回て入力をしていました.しかし,これは面倒です.そこで,入退室管理をなるべく簡潔にしようと思い,自動化することにしました.そしてできたのが Entersysです.

Entersys の概要

先述のようにEntersysは,部室の入退室管理を行うシステムです.学生証についているバーコード(学籍番号)を読み取ることで,入退室の記録を簡潔にしました.読み取った学籍番号から MMA 内での id を取得します.その後は,入退室処理を行い,その結果と在室者を Slack に通知します.

作成詳細

ここからは作成についての詳細を書いていきます.
細かい関数の使い方やオプションなどについてはたくさんネットに転がっているので省略します.

動作環境

  • Raspberry Pi 4
  • Python3

ディレクトリ構成

この記事内では,Entersysのディレクトリ構成を以下のようにします.また,この記事内で書かれるパスはEntersysのディレクトリからの相対パスとします.

ディレクトリ構成
Entersys
├── src
│   ├── main.py
│   ├── update_member.py
│   ├── get_id.py
│   └── slackicon.py
└── data
    ├── user_data.json
    ├── member.csv
    └── inside.txt

id の取得

まずは id の取得です.学生証のバーコードから学籍番号を読み取ります.id は./src/get_id.pyで学籍番号から取得します.対応表は./data/member.csvにあります.この対応表から学籍番号を検索し,id を取得します.下のコードのCSVFILE./data/member.csvのパスです.また,update_member.update_member()./src/update_member.pyにある関数です.これは入部届と紐付けされているスプレッドシートから学籍番号と id を取得し,./data/member.csvに書き込んで更新する関数です.

get_id.py
./src/get_id.py
def get_id(number):
    id = ""
    with open(CSVFILE) as f:
        reader = csv.reader(f)
        row_list = [row for row in reader]
    lastrow = len(row_list)
    with open(CSVFILE) as f:
        for i in range(0, lastrow):
            if number == row_list[i][1]:
                id = row_list[i][0]
                return id

    update_member.update_member()
    with open(CSVFILE) as f:
        reader = csv.reader(f)
        row_list = [row for row in reader]
    lastrow = len(row_list)
    with open(CSVFILE) as f:
        for k in range(0, lastrow):
            if number == row_list[k][1]:
                id = row_list[k][0]
                return id

このファイル内で行っていることは次の通りです.

  1. ./data/member.csvを読み込み,それぞれを配列に格納
  2. 学籍番号を検索し,id を取得
  3. id が見つからなかった場合,update_member()関数を実行し,./data/member.csvを更新
  4. 再び 1.~2.を行う
  5. これでもない場合はError: Number Not Foundと表示して,再び学籍番号の入力を待つ(この例外処理は./src/main.pyで行っています.)

入退室処理

在室者リストを./../data/inside.txtで準備しておき,学生証を読み取ると./../data/inside.txtに在室者がいるかどうかを確認します.この時,入室者リストを配列に格納してから確認しています.入室者リストにない場合は入室,ある場合は退室として処理を行います.また,Slack 通知のためにここでmesssageを決めています.
ENTLANTSとは./../data/inside.txtのことです.

入退室処理プログラム
./src/main.py
message = "enter"
with open(ENTLANTS, "r", encoding="utf-8") as f:
    inside = f.readlines()
inside = [line.rstrip("\n") for line in inside]
for someone in inside:
    if someone == id:
        inside.remove(id)
        message = "exit"
        break
if message == "enter":
    inside.append(id)

このファイル内で行っていることは次の通りです.

  1. ./../data/inside.txtを読み込み,それぞれを配列に格納
  2. ./../data/inside.txtに在室者がいるかどうかを確認
  3. 入退室の処理

Slack 通知

Incoming Webhook

次に Slack への通知部分です.Slack への通知はrequestsPostを使って行っています.Slack にはIncoming Webhookという機能があり,これを使うことで簡単に通知を行うことができます.Incoming Webhookは Slack に送信するメッセージをプログラムで設定することができます.今回はblocksという形式で送信しています.blocksjson形式で設定できます.

Slack アイコンの取得

初めの方にあるslackicon.get_url(id)にて,先ほど取得したidからSlackのアイコンを取得しています.これは MMA の先輩が Slack から取得してきてくれているものなので省略させていただきます.次のfor文では取得した画像をblocksに追加しています.blocks8人ごとに改行しています.これは Slack 上での横並びの最大が 8 人までだっためこのようにしています.(自動で改行してくれなかったのが残念.)

Slack 通知の送信の装飾

要素 どうしたか
チャンネル 送信したいチャンネルを選択
ユーザー名 先ほど取得した id
アイコン 先ほど取得したユーザーアイコン
メッセージ 先ほど決めたメッセージと在室者リスト
Slack 通知プログラム
./src/main.py
user_icon = slackicon.get_url(id)
slack_user_icon = slackicon.get_url(id)
blocks = [
    {
        "type": "context",
        "elements": [
            {"type": "mrkdwn", "text": f":{message}:	在室"},
        ],
    }
]
for i in range(len(inside)):
    user_icon = slackicon.get_url(inside[i])
    if i % 8 == 0 and i != 0:
        blocks.append({"type": "context", "elements": []})
    if user_icon:
        blocks[i // 8]["elements"].append(
            {
                "type": "image",
                "image_url": user_icon,
                "alt_text": f"@{inside[i]}",
            }
        )
    else:
        blocks[i // 8]["elements"].append(
            {"type": "mrkdwn", "text": f"@{inside[i]}"}
        )
with open(ENTLANTS, "w") as f:
    for one in inside:
        f.write("%s\n" % one)
print("%s : %s" % (id, message))
requests.post(
    WEB_HOOK_URL,
    data=json.dumps(
        {
            "channel": CHANNEL,
            "username": id,
            "icon_url": slack_user_icon,
            "blocks": json.dumps(blocks),
        }
    ),
)

その他の処理

入室者のリセット

退室処理を忘れた時のために,入室者リストをリセットしています.これは./src/main.pyで行っています.scheduleモジュールを使って,毎日 4 時に./data/inside.txtをリセットしています.

入室者リストのリセット
./src/main.py
```python:./src/main.py
def inside_update():
    with open(ENTLANTS, "w") as f:
        f.write("")

メンバーの更新

id が見つからなかった時のみでなく,毎朝 4 時に./data/member.csvを更新しています.これは./src/update_member.pyで行っています../data/member.csvは入部届と紐付けされているスプレッドシートから取得しています.

メンバーの更新のプログラム
./src/update_member.py
def update_member():
    try:
        with open(csvfile) as f:
            reader = csv.reader(f)
            row_list = [row for row in reader]
        lastrow_in = len(list(filter(None, worksheet.col_values(2))))

        if worksheet.acell("C" + str(lastrow_in)).value != row_list[0][0]:
            for k in range(2, lastrow_in + 1):
                if row_list[0][0] == worksheet.acell("C" + str(k)).value:
                    worksheet_last_id = worksheet.acell("C" + str(lastrow_in)).value
                    while row_list[0][0] != worksheet_last_id:
                        row_list.insert(
                            0,
                            [
                                worksheet.acell("C" + str(k + 1)).value,
                                worksheet.acell("E" + str(k + 1)).value,
                            ],
                        )
                        k += 1
                    break
            with open(csvfile, "w") as f:
                writer = csv.writer(f)
                writer.writerows(row_list)
    except:
        pass
    else:
        return 0

ここでは,./data/member.csvを読み込み,それぞれを配列に格納しています.次に,最新の情報が異なっていれば入部届と紐付けされているスプレッドシートから学籍番号と id を取得し,./data/member.csvに書き込んでいます.

参考

GitHubで編集を提案

Discussion