AutoHotKeyにより日報の入力を半自動化

今回は題の通りで、日報の入力作業をAutoHotKeyによって半自動化した話となります。

弊社の日報はWebで入力する形式です。
しかし、自分は前日の日報をコピーして編集・記入することが多いためWebで直接入力せず、
①テキストファイルで作成→②翌日は前日のファイルをコピー・編集し作成→③Webに転記
のように入力していたのですが、
この③のWebへの転記作業にて1項目ずつコピペする必要があり大変煩わしく思っていました。
※今年度からは日報に詳細は記載する必要はなくなったため簡素な入力でも問題ないのですが、
 翌日の作業の整理と翌週の朝礼で報告する内容の確認のために
 自分は以前と同じように入力しています。

煩わしく思いつつ特に何もしていなかったのですが、
転記作業自体は自動化すれば良いことに今更気が付きました。
そこでチャットAIにWebへの入力作業を自動化するのに適した手法を聞いてみたところ、
単純な作業なら作業自動化用スクリプト言語のAutoHotKeyが良さそうだと教えてもらい、
AutoHotKeyで転記処理を実装することにしました。

一旦チャットAIにコードを書いてもらったのですが上手く動作しなかった(※)ので、
それをひな型に自分で組み直しました。
※AutoHotKeyで何ができるのかわかっていなかったのもあり、そもそも指示が良くなかったです。
実装したコードは下記になります。

内容としては、
1. 起動時にクリップボードから入力データを読取
2. ホットキーで始動する処理内で、入力データをデータごと・項目ごとにキー送信
を実現しているコードとなります。
コードの前に参考までに、入力データ例(※)と入力画面を貼っておきます。
※入力データのフォーマットは今回の実装に合わせて少し調整しました。

組んでいる途中で思ったのですが、
単純な処理なのでC#でSendKeyを使用して実現するのでも良かった気がしますね。
ですがスクリプト言語は手軽ではあるので、一手段として触っておくのは意義がある気がします。
AutoHotKeyの文法自体は癖がなく書きやすいと感じました。
ただ代入が=ではなく:=なため、何度も癖でコロンを忘れてしまいエラーになりました。。。

以上、日報入力の半自動化(テキスト手動編集後のWebへの入力作業自動化)を行った話でした。
今後も自動化可能な作業に見つかった際は折を見て自動化するようにしたいと思います。

入力データ例

9999
0.5
[雑務]
1. 掃除・朝礼
---
12345678
7.5
[計測ソフト開発]
1. 仕様書作成
2. 実装中
---
_
_
[明日の予定]
1. 計測ソフト開発
2. サーバープログラムビルド送付
---
_
_
[出退]
08:55
18:15

入力画面

実装したコード:DailyReportAutomation.ahk

#Requires AutoHotkey v2.0
;開発使用バージョン:v2.0.19
;-------------------------------------------------------------------------------
;[グローバル変数]
; 標準出力へのグローバル参照:
;  ファイル名に*を指定してFileOpenすることで標準出力へ書き込み
;  実行時にmoreコマンドを指定して出力のリダイレクトを行う必要あり
;  (moreなしだとエラーになる)
;  次のように実行→ AutoHotkey64.exe DailyReportAutomation.ahk | more
global Stdout := FileOpen("*", "w")

;-------------------------------------------------------------------------------
;[起動時初期化処理:クリップボードを読み取り入力用レコードデータ作成]
if !ClipWait(1) {
    ; クリップボードが空ならエラー終了
    MsgBox("No text found in clipboard.", "Error", 16)
    ExitApp
}
buff := ClipboardAll() ; クリップボードを退避
clipboard := A_Clipboard ; クリップボードを取得
records := Parse(clipboard) ; クリップボードからレコードリストをパース
for item in records
    Log(item.ToString() . "`n")
A_Clipboard := buff ; 退避していたクリップボードを復元
if (records.Length = 0) {
    ; クリップボードが空ならエラー終了
    MsgBox("No records in clipboard.", "Error", 16)
    ExitApp
}
if (records.Length > 10) {
    ; クリップボードが10以上ならエラー終了
    MsgBox("Too much records in clipboard.", "Error", 16)
    ExitApp
}
; エラーなければ常駐開始

;-------------------------------------------------------------------------------
;[常駐時ホットキー]
;1. Windows + s:入力処理開始(開始前に先頭の入力欄にカーソルを合わせておくこと
#s:: {
    ; IMEが全角か半角かチェックし、全角(true)なら半角に切り替えます
    if (CheckImeIsFullOrHalf("Infortec 日報"))
        SwithImeToFullorHalf()
    ; 与えられたレコードデータの項目を順に入力
    for record in records {
        ; 工番入力→案件名入力→内容入力→コメント欄飛ばす→進捗欄飛ばす→時間入力
        ClearSend(record.ProjectNo)
        Next()
        ClearSend(record.ProjectName)
        Next()
        ClearSend(record.Content)
        Next()
        Next()
        Next()
        ClearSend(record.Time)
        Next()
    }
    ExitApp ; アプリ終了
}

;2. Windows + q:アプリ終了
#q:: {
    ExitApp
}

;-------------------------------------------------------------------------------
;[クラス]
;レコードクラス
class RecordData{
    ;コンストラクタ:工番、時間、案件名、内容
    __New(projectNo, time, projectName, content){
        this.ProjectNo := projectNo
        this.Time := time
        this.ProjectName := projectName
        this.Content := content
    }

    ;Func 文字列化
    ToString(){
        txts := [this.ProjectNo, this.Time, this.ProjectName, this.Content]
        return Join(txts, "`n")
    }
}

;-------------------------------------------------------------------------------
;[関数群]
;Func 指定したタイトルを持つウィンドウのIMEが全角か半角かチェックします
CheckImeIsFullOrHalf(windowTitle) {
    ; 日報ウィンドウのIMEの状態を取得
    SetTitleMatchMode 1 ; タイトル部分一致
    WM_IME_CONTROL := 0x0283
    IMC_GETOPENSTATUS := 0x0005
    imeWnd := DllCall("imm32.dll\ImmGetDefaultIMEWnd", "Uint", WinExist(windowTitle))
    imeStatus := DllCall("user32.dll\SendMessageA", "UInt", imeWnd, "UInt", WM_IME_CONTROL, "Int", IMC_GETOPENSTATUS, "Int", 0)
    Log(imeStatus ? "全角" : "半角")
    return imeStatus
}

;Func IMEの全角/半角を切り替えます。
SwithImeToFullorHalf() {
    Send("{vkF3sc029}") ; 半角/全角キー送信
}

;Func 入力欄をクリアして与えられた文字列を入力します
ClearSend(text) {
    Sleep 80
    Send("^a")
    Sleep 20
    Send("{Del}")
    Sleep 20
    Send(text)
    Sleep 80
}

;Func 次の入力欄へ移動します
Next(){
    Send("{Tab}")
    Sleep 20
}

;Func テキストからレコードリストをパースします
Parse(text){
    ; 改行を正規化
    text := StrReplace(text, "`r`n", "`n")
    text := StrReplace(text, "`r", "`n")
    ; 項目ごとに文字列を分割
    items := StrSplit(text, "---")
    ; 項目が複数ないなら終了
    if (items.Length < 2)
        return []
    records := []
    for item in items {
        ; 項目からレコードを読み取る
        item := Trim(item, "`n")
        ; 行ごとに分割
        rows := StrSplit(item, "`n")
        ; 4行未満なら無効データとして飛ばす
        if (rows.Length < 4)
            Continue
        ; 行ごとの処理:主に複数行に跨る内容行の結合を行う
        contents := []
        for index, row in rows {
            ; プレースホルダ _ は空行に変換
            rows[index] := row := row = "_" ? "" : row
            ; 4項目目までは飛ばす
            if (index < 4){
                Continue
            }
            ; 4項目目以降を内容行配列に追加
            contents.Push(row)
        }
        content := Join(contents, "`n")
        ; レコード作成:工番1行目、時間2行目、案件名3行目、内容4行目~
        record := RecordData(rows[1], rows[2], rows[3], content)
        records.Push(record)
    }
    return records
}

;Func 文字列配列を区切り文字で連結
Join(array, delimiter){
    ret := ""
    ; 戻り値に配列内の各文字列を区切り文字と合わせて結合
    for str in array
	    ret .= str . delimiter
    ret := RTrim(ret, delimiter) ; 末尾の区切り文字を削除
    return ret
}

;Func ログ出力
Log(msg){
    Stdout.WriteLine(msg)
}

コメントを残す