アラートメールを判別させたい。 - XXXXXはワイルドカードなんです！ - 

アラートメールを判別させたい。 - XXXXXはワイルドカードなんです！ -

どうもこんにちは。NETS1の田中(た)です。

# はじまり

あるところに「アラートメールが届いたら、一覧表を参照して対応する」というよくある運用がありました。

一覧表

|エラーコード|エラーメッセージ|対応|
|:----------|:--------------|:------------:|
|ERROR002|app00x ERROR ERROR002 xxxxx すごいことがおきました。 XXXXX[10223] でエラー|電話連絡|
|ERROR002|すごいことがおきました。 XXXXX[YYYYY] でエラー|メール連絡|
|ERROR002|すごいことがおきました。 |無視|

実際のメール

```
Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！
```

この場合、おそらく皆さんは電話連絡をするでしょう。そしてその行動は正解です。
運用を続けていくうちに一覧表の内容が充実していき、多くの対応が列挙されるうちに
一つ一つを目で確認していくのが非常に負荷がかかる作業になっていくでしょう。
（たとえエラーコードである程度絞れたとしても！）

そしてこう思うはずです。
「ある程度機械的に絞れるようにしたい！視覚的にわかりやすくしたい！」

そんなところから、チャレンジは始まりました・・・。

# 環境
## 使用言語

Python 3.6 ([difflib](https://docs.python.org/ja/3.6/library/difflib.html) を利用)

## なぜ difflib か？

* 比較結果をリストで得られるので可視化の自由が大きそう
* 評価値を出すことができる
* 単純に最近 Python をよく触っていた

~~結局のところ一番下が大きな理由だったり・・・~~

# アラートメールを判別させたい
## 方向性を決める
優先順位は以下の通り設定しました。

1. diff した結果を視覚的にだす
2. 評価値をつける
3. 評価値をもとに対応方法を決定する

評価値を付けて判断してくれるのが一番いいですが、もし間違ってしまったときのことを考えると
評価値だけ出すというのはないかなと思いました。

## とりあえず diff してみる
### 実行結果

コード

```python

import difflib, sys

mail = 'Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！'
manual1 = 'app00x ERROR ERROR002 xxxxx すごいことがおきました。 XXXXX[10223] でエラー'
manual2 = 'すごいことがおきました。 XXXXX[YYYYY] でエラー'
manual3 = 'すごいことがおきました。'

manuals = [manual1, manual2, manual3]

print()
for manual in manuals:
    s = difflib.SequenceMatcher(None, mail, manual)
    for tag, i1, i2, j1, j2 in s.get_opcodes():
        if tag == 'delete':
            sys.stdout.write('\033[34m' + mail[i1:i2] + '\033[0m')
        elif tag == 'equal':
            sys.stdout.write('\033[32m' + mail[i1:i2] + '\033[0m')
        elif tag == 'replace':
            sys.stdout.write('\033[31m' + mail[i1:i2] + '\033[0m')
        elif tag == 'insert':
            sys.stdout.write('\033[33m' + manual[j1:j2] + '\033[0m')
        else:
            sys.stdout.write(mail[i1:i2])
    print()
    for tag, i1, i2, j1, j2 in s.get_opcodes():
        print(tag, ':', mail[i1:i2], '|', manual[j1:j2])
    print()
print()

```

比較した結果の詳細(opcodes)をうまく活用して表示させます。

結果
![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379557/fd2b978a-c814-28a1-dd9a-60109b3a6b18.png)


すこし細かく見てみましょう。

### 1行目 の比較結果

```
Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！
```

と

```
app00x ERROR ERROR002 xxxxx すごいことがおきました。 XXXXX[10223] でエラー
```

の比較です。

* 最初の app001 から equal 評価になっている(本当は2番目の app001 から評価してほしい)。
* ecnseci-1349, sugoi の部分がすべて replace 評価となっている。

### 2行目 の比較結果

```
Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！
```

と

```
すごいことがおきました。 XXXXX[YYYYY] でエラー
```

の比較です。

* sugoi と数字の部分が replace で評価されている。

### 3行目 の比較結果

```
Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！
```

と

```
すごいことがおきました。
```

の比較です。

* 一致分はすべて equal になって、その他は delete 評価となっている。

### 結果のまとめ
ざっくりみると、XXXXX となっている部分の replace をワイルドカードと判断してしまえば良さそうです。
とはいえ、1行目の app001 が初めから評価されているところが気がかりです・・・。

## 結果を補正する
### 2つの app001 対策

app001 対策は発想は簡単です。必要なところだけを比較すればいいんです。

 * 後ろの delete はいらない。 … 評価した後ろの delete を削除して再評価すればOK
 * 前の delete もいらない。 … 前から順番に評価して先頭が equal が出るところから評価すればOK

とはいえ、これを考えたところで、最初の app001 から評価が始まってしまうのに変わりはありません。
そこで、difflib の評価値を使いました。評価値は評価している２つの文字列の長さ・equal の数に依存するので、
文字列が短く・equal 率が多いほう、つまり2番目の app001 からの評価を正としてくれそうです。

### 対策結果

コード

```python
import difflib, sys

mail = 'Dec 14 12:59:12 app001 2020/12/14 12:59:12.449 app001 ERROR ERROR002 ecnseci-1349 すごいことがおきました。 sugoi[10223] でエラー あぁ、なんという！'
manual1 = 'app00x ERROR ERROR002 xxxxx すごいことがおきました。 XXXXX[10223] でエラー'

manuals = [manual1]

def search_space(message):
    '''スペースの次の文字位置を返す（都合上先頭はスペースとする）'''
    space_pos = [0]
    index = 0
    for c in message:
        if c == ' ':
            space_pos.append(index + 1)
        index+=1
    return space_pos

def check_message_by_difflib(manual, message):

    space_pos = search_space(message)

    ratio = 0
    # スペース毎に先頭と考えて評価していく
    for i in space_pos:
        recheck_flag = False
        s = difflib.SequenceMatcher(None, message[i:], manual)
        tag, i1, i2, j1, j2 = s.get_opcodes()[-1]

        # 最後が delete で終わっていたら再評価
        if tag == 'delete':
            s = difflib.SequenceMatcher(None, message[i:i + i1], manual)
            recheck_flag = True
        if ratio < s.ratio():
            ratio = s.ratio()
            s_max = s
            seek = i
            max_flag = recheck_flag

    # opcodes を作り直す

    # 先頭削除分を追加する
    opcodes = [['', 0, seek, 0, 0]]

    # 先頭削除した分を補正して opcodes を作り直す
    for tag, i1, i2, j1, j2 in s_max.get_opcodes():
        i1 = i1 + seek
        i2 = i2 + seek
        opcodes.append([tag, i1, i2, j1, j2])

    # 後方削除分を追加する
    if max_flag:
        opcodes.append(['', seek + s_max.get_opcodes()[-1][2], len(message), 0, 0])

    return opcodes, ratio

# main
for manual in manuals:
    opcodes, ratio = check_message_by_difflib(manual, mail)
    for tag, i1, i2, j1, j2 in opcodes:
        print(tag, ':', mail[i1:i2], '|', manual[j1:j2])
    print()
```

結果

```
 : Dec 14 12:59:12 app001 2020/12/14 12:59:12.449  |
equal : app00 | app00
replace : 1 | x
equal :  ERROR ERROR002  |  ERROR ERROR002
replace : ecnseci-1349 | xxxxx
equal :  すごいことがおきました。  |  すごいことがおきました。
replace : sugoi | XXXXX
equal : [10223] でエラー | [10223] でエラー
 :  あぁ、なんという！ |
```

いい感じ！

# これでおわり？

この結果を元に色付き表示させればよさそうです！
せっかく評価値もとっているので、評価値も出すようにすれば参考にはなるかな？

と、うきうき気分で別のタスクに取り掛かっていました。大きな落とし穴があることに気づかずに・・・。

次回、アラートメールを判別させたい。 - その x はワイルドカード？ -

続くかどうかはしりません。