"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

Python3でイベントログのログオン・ログオフ情報から出勤・退勤時刻を特定する

f:id:arakan_no_boku:20190330140824j:plain

目次

今回の目的

ブラック企業労働争議などの記事に、「PCのログを押収し、起動時刻やログイン時刻を調査した・・」みたいな記述がよくあります。 

この情報を取得してCSVファイルに保存するやり方を以下の記事に書きました。

arakan-pgm-ai.hatenablog.com 

ただ、PCのイベントログから抜きだしただけのCSVファイルから、その日の出勤時刻・退勤時刻を特定するのはめんどくさい作業になります。

1日の中で何回もPCを起動・終了することもありますし、徹夜で仕事してれば、起動した日と終了した日が異なる場合もありますから。 

そこで、そういうめんどくさいことは、Pythonでやってみよう・・ということです。

仕様検討

PCのイベントログから抜き出したCSVファイルは既にある想定とします。

Pythonの中でイベントログから抜き出すところまでやると、実際に使うとき、調査対象のPCにいちいちPythonをインストールしないといけなっくなり、非現実的だからです。

処理の対象となるイベントIDは以下の通りです。

  • 6005 :起動
  • 6006 :シャットダウン
  • 6008 :正常ではない終了
  • 7001 :サービススタート(ログオン:Windows10高速起動時)
  • 7002 :サービスストップ(ログオフ:Windows10高速起動時)

取得したCSVファイルのイメージは以下の通りです。

#TYPE Selected.System.Diagnostics.Eventing.Reader.EventLogRecord
"TimeCreated","Id","Message"
"2018/01/12 12:42:52","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2018/01/12 9:19:16","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"
"2018/01/12 9:14:33","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2018/01/12 9:13:26","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"
"2018/01/12 8:16:04","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2018/01/12 1:02:41","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"
"2018/01/11 19:09:42","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2018/01/11 0:33:50","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"
"2018/01/10 19:22:06","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2018/01/10 0:33:46","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"
"2018/01/09 19:29:29","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"

 Windows10で高速起動・終了を使うと、このように、ほとんど7001、7002ばかり拾われるようなデータになるみたいです。。  

プログラムの仕様:処理する順序を整理する

整理する順番です。

  1. 日付・時間の昇順に並べ直す。
  2. 同じ日付で最初の起動・ログインを「初回起動時刻」最も遅い終了・ログオフを「最終終了時刻」とする。

こうやって、整形した結果を、「日付,初回起動時刻,最終終了時刻」みたいにして、1日1行で出力します。

プログラムの仕様:日付時刻の昇順に並べ直す 

単純にSortしようと思うと、ちょっと問題がありました。 

日付の部分は、きれいに0サプレスされて、01/05 のように統一されているのですが、時刻の方は時間の部分がゼロサプレスされていない行が混在しています。 

こんな感じで。

"2017/12/03 14:43:01","7001","カスタマー エクスペリエンス向上プログラムのユーザー ログオン通知"
"2017/12/03 8:56:50","7002","カスタマー エクスペリエンス向上プログラムのユーザー ログオフ通知"

なので、上記の「8:56:50」の部分を「08:56:50」に補正してやる必要があります。 

幸い、正規表現で引っ掛けられるので、置き換えでやってしまいます。 

# ソートの前に0:13:54のような時刻の頭が一桁の時間を2桁に補正する
pre_mk = re.compile(r'\s(\d{1}):')
for j in range(len(tmp_list)):
    tmp_list[j][0] = pre_mk.sub(r' 0\1:',tmp_list[j][0])

 

その結果をソートしてやれば良いですね。 

# 日付・時刻の昇順にソートしなおす
sl = sorted(tmp_list,key = lambda X:X[0])
プログラムの仕様:1日の最初と最後を識別する 

同じじ日付で最初の起動・ログインを「初回起動時刻」最も遅い終了・ログオフを「最終終了時刻」とします。 

これはネストした辞書を使ってみようと思います。 

例えば、「{'2017/12/01:{'start':'2017/12/01 09:01:03,'stop':'2017/12/02 00:36:34'}}」みたいに、日付をキーにして初回開始(start)と最終終了(stop)の2つのキーをもつ辞書をネストして保持するようにしてみようということです。 

こうしておけば、1日の最初の起動時刻のみをstartに更新し、stopは日付が切り替わるまで終了日時で更新していけば、結果的に「日付,初回起動時刻,最終終了時刻」の形でデータが残ります。  

Pythonソースコードのポイント:出力用の辞書を作る 

上記でまとめたロジックをソースで書くとこんな感じになります。 

is_started_dic = {} # 日別に最初の起動時刻を記録したらTRUE
in_day = '' # カレントの日付キーを保持する
day_dic = {} # 日別に初回起動時刻(start)と最終終了時刻(stop)を保持する

# 日別に初回起動時刻と最終終了時刻を更新する
dtpat = re.compile(r'(\d{4}/\d{2}/\d{2})\s+(\d+):(\d+):(\d+)')
for i in range(len(sl)):
    mo = dtpat.search(sl[i][0])
    if(mo):
        if(mo.group(1) not in is_started_dic):
            is_started_dic.setdefault(mo.group(1),False)
            
        if(sl[i][1] == '6005' or sl[i][1] == '7001'):
            in_day = mo.group(1)
            if(is_started_dic.get(mo.group(1),False) == False):
                is_started_dic[mo.group(1)] = True
                day_dic.setdefault(mo.group(1),{}).setdefault('start',sl[i][0])
                day_dic[mo.group(1)].setdefault('stop','')
        else:
            day_dic[in_day]['stop'] = sl[i][0]

 

sl[i][0] には、例えば「 2017/12/02 21:29:30」のような日付・時刻がはいります。 

sl[i][1]には、イベントIDですね。 

mo.group(1) には、正規表現で「r'(\d{4}/\d{2}/\d{2})\s+(\d+):(\d+):(\d+)'」のように、年月日にマッチする条件「\d{4}/\d{2}/\d{2}」をカッコでくくっているので、マッチした場合は、例えば上記の例だと「2017/12/02」がはいります。 

それがわかれば、「 is_started_dic」という辞書を、対象日付の最初の日付を更新したタイミングでTrueに更新することで、初回起動時刻が上書きされないようにしているだけの、ごくシンプルなロジックになってます。  

Pythonソースコードのポイント:生成した辞書をCSVに書き出す

辞書(day_dic)ができたら、CSVに書き出すだけです。 

# 結果をCSVに書き出す
with open('output.csv','w',newline='') as outcsv:
    csvwriter = csv.writer(outcsv)
    csvwriter.writerow(['年月日','初回起動','最終終了'])
    for k,v in day_dic.items():
        csvwriter.writerow([k,v['start'],v['stop']])
    outcsv.close()

 

そうすると、できあがったCSVはこんな感じになります。

f:id:arakan_no_boku:20180113161244j:plain

CSVEXCELで開くと、ちゃんと日付・時刻データとして認識します。 

pythonソース全文と実行時の注意

動作確認は、windows10のanaconda(python 3.8)でやってます。 

動かすと、コマンドラインで入力ファイル名(パス付)と出力ファイル名(パス付)の入力を求めるので、環境にあわせて指定してください。

# coding: UTF-8
import csv
import re
import os


def arrange_data(input_file_with_path, output_file_with_path):
    # CSVファイルを開いてリストに読み込む
    with open(input_file_with_path, 'r', encoding='utf8') as csvf:
        in_reader = csv.reader(csvf)
        tmp_list = list(in_reader)

    # ソートの前に0:13:54のような時刻の頭が一桁の時間を2桁に補正する
    pre_mk = re.compile(r'\s(\d{1}):')
    for j in range(len(tmp_list)):
        tmp_list[j][0] = pre_mk.sub(r' 0\1:', tmp_list[j][0])

    # 日付・時刻の昇順にソートしなおす
    sl = sorted(tmp_list, key=lambda X: X[0])

    is_started_dic = {}  # 日別に最初の起動時刻を記録したらTRUE
    in_day = ''  # カレントの日付キーを保持する
    day_dic = {}  # 日別に初回起動時刻(start)と最終終了時刻(stop)を保持する

    # 日別に初回起動時刻と最終終了時刻を更新する
    dtpat = re.compile(r'(\d{4}/\d{2}/\d{2})\s+(\d+):(\d+):(\d+)')
    for i in range(len(sl)):
        mo = dtpat.search(sl[i][0])
        if(mo):
            if(mo.group(1) not in is_started_dic):
                is_started_dic.setdefault(mo.group(1), False)

            if(sl[i][1] == '6005' or sl[i][1] == '7001'):
                in_day = mo.group(1)
                if(is_started_dic.get(mo.group(1), False) is False):
                    is_started_dic[mo.group(1)] = True
                    day_dic.setdefault(
                        mo.group(1),
                        {}).setdefault(
                        'start',
                        sl[i][0])
                    day_dic[mo.group(1)].setdefault('stop', '')
            else:
                day_dic[in_day]['stop'] = sl[i][0]

    # 結果をCSVに書き出す
    with open(output_file_with_path, 'w', newline='') as outcsv:
        csvwriter = csv.writer(outcsv)
        csvwriter.writerow(['年月日', '初回起動', '最終終了'])
        for k, v in day_dic.items():
            csvwriter.writerow([k, v['start'], v['stop']])
    outcsv.close()


input_file = input("input file>")
output_file = input("output file>")
input_file_with_path = str(input_file).replace(os.sep, "/")
output_file_with_path = str(output_file).replace(os.sep, "/")
arrange_data(input_file_with_path, output_file_with_path)

ではでは。

#python