"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

PCのイベントログの起動時刻・ログイン時刻情報を半自動整形する/Python

PCの起動・終了/ログイン・ログオフのログをひろって、CSVファイルに保存したものを、半自動整形するPythonプログラムを作ります。 

f:id:arakan_no_boku:20190330140824j:plain

この記事の前提

 

この記事の続きです。

arakan-pgm-ai.hatenablog.com 

セットでみてもらえるといいですね。

 

対象となるイベントIDをおさらい

 

対象としたイベントIDは以下の通りです。

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

 

取得したCSVファイルのサンプルをおさらい

 

取得した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ばかり拾われてますが、ところどころ、6005、6006の通常の起動終了が混じってるようなデータです。  

1日の最初(出勤)と最後(退勤)の識別が少し面倒な理由

 

PCなんて1日の中で何回か起動・終了することがあります。 

徹夜で仕事してれば、起動した日と終了した日が異なる場合も、ままあります。 

だから。

どれがその日の初回起動時刻なのか、最終終了時刻なのかを確認するのに、結構面倒だし、うっかり間違いをしやすい。

これが問題です。

CSVファイルを出力して、EXCELに読み込ませるだけだと、ここは手作業でやらないといけません。 

だから。

対象のデータが少なければいいですが、がっつりまとまると、せめて対象日の「初回起動時刻」と「最終終了時刻」を見やすくしたいな。

それもできたら半自動で・・と考えてしまいます。

 

1日の最初(出勤)と最後(退勤)の識別をpythonのプログラムで実装する

 

さすがにPowerShellスクリプトだけでやるのは面倒くさいので、こういう処理が簡単にかける「python」を使ってやります。

 

処理する順序を整理する

 

整理する順番としてはこんな感じじゃないかと思ってます。

  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])

 

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

 

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

例えば、「{'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に更新することで、初回起動時刻が上書きされないようにしているだけの、ごくシンプルなロジックになってます。 

 

生成した辞書を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.6)でやってます。

 

# coding: UTF-8
import csv
import re

# CSVファイルを開いてリストに読み込む
with open('eventlog.csv','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) == 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.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ファイルの名前・パスは環境にあわせて書き換えてください。 

ではでは。