はじめに
どうも!生産技術部で製品の検査工程を担当しているエンジニアです。最近はElastic-stack(Elasticsearch)にハマっていますw
Advent Calender 2020が始まって、5日目になりますね!
今年もテーマがオセロということで、書くことないよね?って思いながら、2019年のAdvent Calenderを眺めていたら、いいアイデアを見つけちゃいました。
@tanaka-a 様のオセロの棋譜データベースWTHORの読み込み方、オセロの棋譜にも使えるファイル形式SGFとは
@hajipong 様の連盟公式データをAPIで連携させる計画の話
オセロの棋譜に使われているファイル形式関連ですね。上記の記事を拝読して、Elasticsearch?棋譜?取り込めるんじゃない?ってなりましたので、棋譜を取り込んでみます。取り込んだデータをどうしたいの?に対する回答は持ち合わせてないです。どなたか使ってあげてくださいw
リポジトリはこちら -> elastic-othellodb
以前に投稿した関連記事 -> 脱Excel Elastic Stack(docker-compose)でcsvログを分析・可視化 - Elastic Stackとは
やったこと
GGF形式で保存されたオセロの棋譜データをパースして、Elasticsearchに保存し、Kibanaで可視化しました。
KibanaのDashbordに黒番を持ったプレイヤーのレーティングの最大値をランキング順に並べてみました。(なんとなくDashbordに表示したかっただけなので、データに意味はないです。w)
オセロの保存形式って何があるの?
Edaxのソースコードを軽く見てみました。ちょっと待って、いろいろありすぎて分からない。ターゲットはどの形式にしたら良いのか。
/**
* @brief convert an allinf.oko game to a Game.
*/
void oko_to_game(const OkoGame *oko, Game *game)
/**
* @brief convert a Wthor game to a Game.
*/
void wthor_to_game(const WthorGame *thor, Game *game)
/**
* @brief Parse a ggf game.
*/
static int game_parse_ggf(FILE *f, char *tag, char *value)
/**
* @brief Parse a Smart Game Format (sgf) game.
*/
static int game_parse_sgf(FILE *f, char *tag, char *value)
/**
* @brief Read a game from a pgn file.
*/
void game_import_pgn(Game *game, FILE *f)
/**
* @brief Write a game to an eps file.
*/
void game_export_eps(const Game *game, FILE *f)
ターゲットの保存形式は何にしよう
@tanaka-a 様のWTHOR形式についての記事によると、そもそもバイナリデータであるそうなので、即却下しました。大変そうだし、Elasticsearchの公式ブログにもこう書いてあるので。
Filebeatは、ファイル形式のソースからデータを読み取り、事前処理、送信を行います。多くのケースでFilebeatはログファイルの読み取りに使用されていますが、実はあらゆるノンバイナリファイル形式をサポートしています。
続いて、もうひとつの記事に記載されているGGFってなんだろうと思い、調べてみると、Generic Game Format
の略語であり、オセロ以外のゲームの棋譜も記述できるようです。GGF形式のドキュメントを見る限り、囲碁、チェス、チェッカー、アマゾン?でも使えそうです。データの例は、こんな感じで扱いやすそうです。(ただし、実際のデータは改行されず1行で書かれます。)サンプルデータも豊富にありましたので、ターゲットはGGFに決定です。
(;GM[Amazons]PC[GGS/ams]DT[945277754]
PB[amsbot5]PW[Raphael]
RB[1897.01]RW[1874.15]
TI[30:00/00:05/02:00]
TY[10]RE[-3.00]
BO[10
---*--*---
----------
----------
*--------*
----------
----------
O--------O
----------
----------
---O--O---
*]
B[D1-D7-G7/11.50/5.04]W[J7-G4-B4//21.42]
B[G1-E3-E10/8.30/5.01]W[G10-I8-I5//14.01]
B[A4-A6-G6/9.00/5.01]W[A7-D4-A7//8.03]
B[A6-C8-H8/10.60/5.01]W[G4-I4-J5//7.42]
B[D7-F9-D9/12.20/5.01]W[D10-F8-E8//10.12]
B[J4-H2-H4/10.80/5.02]W[I4-G2-G3//6.24]
B[C8-C6-D5/10.00/5.01]W[F8-F5-F8//15.82]
B[F9-H9-E6/9.60/5.02]W[D4-C5-B5//12.27]
B[C6-C7-B7/13.60/5.01]W[C5-C6-D6//8.54]
B[E3-E2-C4/8.10/5.02]W[F5-D3-D2//4.29]
B[H2-I2-H1/8.30/5.01]W[G2-H3-G2//6.33]
B[E2-E3-I7/6.80/5.02]W[H3-F5-H3//10.15]
B[I2-J3-I3/8.00/5.03]W[F5-H5-I4//4.56]
B[E3-E5-G5/6.30/5.02]W[D3-E4-D4//10.25]
B[E5-F4-G4/10.60/5.01]W[E4-E3-F3//2.99]
B[F4-E4-C2/2.30/5.57]W[E3-D3-E3//3.94]
B[J3-I2-I1/2.10/5.01]W[D3-A3-D3//19.68]
B[C7-D7-C7/2.00/5.01]W[H5-H7-F9//35.67]
B[H9-I9-H10/0.50/5.01]W[H7-G8-H9//13.11]
B[I9-J8-I9/2.00/5.01]W[G8-F7-F5//34.53]
B[D7-D8-D7/-1.50/5.09]W[I8-J7-I8//40.46]
B[D8-F10-D8/1.00/5.01]W[F7-G8-G9//7.21]
B[E4-E5-E4/-3.00/5.01]W[G8-F7-F6//73.36]
B[I2-H2-G1/-3.00/5.01]
;)
使用するデータ
データはサンプルデータの最新のファイルを使います。解凍すると拡張子がついていないため、ggfを付けました。データ量が多いため、2018年以前のデータは削除し、Othello.latest.294420.ggf
としました。
構成とデータの流れ
Elastic-stackの構成とデータの流れは以下になります。
- ggfデータをFilebeatで取り込み、Logstashに1行づつ転送
- Logstashで1行分のデータを加工
- Elasticsearchにデータを取り込む
- Kibanaで可視化
実際のログからパースの方法を考える
実際のログを見てみると、
- 頭に数字がついており、1行に記載されている試合数を示しているようです。(;から;)までが1試合分であり、2と書いてあれば2試合分が1行となっています。(泣いてる顔文字にしか見えない)
- [ ]で囲まれた部分が取り出したい情報となっています。取り出したい情報には、
GGS/os
のように単純な英数字だけでなく記号のスラッシュが含まれています。 - タイムスタンプに使う時間は、
DT[2018.03.03_03:45:03.MST]
のように年.月.日_時:分:秒.タイムゾーンの形式になっています。
1 (;GM[Othello]PC[GGS/os]DT[2018.03.03_03:45:03.MST]PB[rho]PW[Saio7000]RB[2351.94]RW[2345.59]TI[05:00//02:00]TY[8]RE[+0.000]BO[8 -------- -------- -------- ---O*--- ---*O--- -------- -------- -------- *]B[C4//0.01]W[e3/0.64/0.01]B[F6//0.02]W[e6/0.65/0.01]B[F5//0.03]W[c5/0.66/0.01]B[F4//0.01]W[g6/0.69/0.01]B[F7//0.02]W[g5/0.71/0.01]B[D6//0.03]W[d3/0.71/0.01]B[F3//0.03]W[b5/0.68/0.01]B[C3//0.02]W[g3/0.72/0.01]B[B3//0.03]W[b4/0.74/0.01]B[C6//0.02]W[e2/0.76/0.01]B[C2//0.60]W[d2/0.81/0.01]B[H6//0.01]W[f2/0.89/0.01]B[H5//0.01]W[b6/0.90/0.01]B[A6//0.02]W[c7/0.92/0.01]B[D8//0.03]W[c8/0.92/0.01]B[B8//0.02]W[a5/0.96/0.01]B[A4//0.02]W[e7//0.01]B[G4//0.01]W[h4//5.26]B[E8//0.01]W[h7//0.01]B[D7//0.01]W[b7//0.01]B[H2//0.01]W[c1//0.01]B[F1//0.01]W[d1//0.01]B[E1//0.01]W[g1//0.01]B[H3//0.01]W[h1//0.01]B[G2//0.01]W[a2//0.01]B[A8//0.01]W[a7//0.01]B[A3//0.01]W[b2//0.01]B[B1//0.01]W[a1//0.01]B[G7//0.01]W[f8//0.01]B[G8//0.01]W[h8//0.01];)
2 (;GM[Othello]PC[GGS/os]DT[2018.03.03_04:04:54.MST]PB[rho]PW[Saio7000]RB[2649.71]RW[2731.86]TI[05:00//02:00]TY[s8r18]RE[-18.000]BO[8 -------- --O--*-- -OOO*--- OOO****- -*O**--- ---*---- -------- -------- *]B[E2/-16.47/43.89]W[a5/16.00/48.24]B[C1/-20.00/18.63]W[d2/16.00/29.86]B[D1/-20.00/32.02]W[f3/16.00/25.66]B[B6/-20.00/55.01]W[c7/20.00/127.31]B[A6/-20.00/27.79]W[a7/20.00/17.32]B[A2/-20.00/22.22]W[h5/18.00/13.40]B[B2/-18.00/17.66]W[g5/18.00/6.32]B[F5/-18.00/1.16]W[c6/18.00/4.35]B[H3/-18.00/0.91]W[e7/18.00/4.26]B[D7/-18.00/0.76]W[a3/18.00/6.26]B[A8/-18.00/1.16]W[a1/18.00/0.01]B[B8/-18.00/1.01]W[b1/18.00/0.01]B[F7/-18.00/0.64]W[e6/18.00/0.01]B[B7/-18.00/0.02]W[h4/18.00/0.01]B[F6/-18.00/0.02]W[g3/18.00/0.01]B[H6/-18.00/0.02]W[g6/18.00/0.01]B[H2/-18.00/0.02]W[g1/18.00/0.01]B[G7/-18.00/0.02]W[f8/18.00/0.01]B[D8/-18.00/0.02]W[h8/18.00/0.01]B[G2/-18.00/0.02]W[h7/18.00/0.01]B[G8/-18.00/0.02]W[e1/18.00/0.01]B[F1/-18.00/0.02]W[h1/18.00/0.01]B[E8/-18.00/0.02]W[c8/18.00/0.01];)(;GM[Othello]PC[GGS/os]DT[2018.03.03_04:04:54.MST]PB[Saio7000]PW[rho]RB[2731.86]RW[2649.71]TI[05:00//02:00]TY[s8r18]RE[-22.000]BO[8 -------- --O--*-- -OOO*--- OOO****- -*O**--- ---*---- -------- -------- *]B[e2/-16.00/41.56]W[A5/20.00/71.41]B[c1/-16.00/30.59]W[D2/20.00/26.85]B[d1/-16.00/26.77]W[F3/20.00/53.86]B[a6/-16.00/23.96]W[A7/20.00/45.09]B[a2/-18.00/20.41]W[B6/20.00/37.31]B[c6/-18.00/17.39]W[H5/22.00/27.02]B[b2/-20.00/86.28]W[G5/22.00/13.80]B[g3/-20.00/9.24]W[F1/22.00/4.79]B[e1/-22.00/19.24]W[A3/22.00/1.27]B[a8/-22.00/4.40]W[A1/22.00/1.38]B[h4/-22.00/0.01]W[B1/22.00/2.04]B[f6/-22.00/0.01]W[H3/22.00/1.66]B[g2/-22.00/0.01]W[G1/22.00/1.13]B[c7/-22.00/0.01]W[E6/22.00/0.02]B[f7/-22.00/0.01]W[H1/22.00/0.02]B[h2/-22.00/0.01]W[F5/22.00/0.03]B[g6/-22.00/0.01]W[H6/22.00/0.02]B[h7/-22.00/0.01]W[H8/22.00/0.02]B[pass]W[F8/22.00/0.02]B[g7/-22.00/0.01]W[G8/22.00/0.02]B[pass]W[E7/22.00/0.02]B[d7/-22.00/0.01]W[D8/22.00/0.02]B[c8/-22.00/0.01]W[E8/22.00/0.02]B[b7/-22.00/0.01]W[B8/22.00/0.01];)
1行に複数試合が含まれるものを分割する
Filebeatで受け取った時に、分割することをまず最初に考えますが、Filebeatは複数行を1行にまとめることはできますが分割することはできません。そこで、Logstashに転送した後に、分割します。分割方法は、;)
で区切り、配列にした後、splitを使って分割します。
mutate {
split => { "message" => ";)" }
}
split {
field => "message"
}
[ ]で囲まれた部分の情報を取り出す
今回はgrokフィルタを使って取り出してみます。grok-patternの一覧には、[ ]の中身を上手く取り出すことができる正規表現がありません。そこでカスタムパターンを作ります。 ]では無い連続したデータを対象の文字列としました。
CUSTOM_WORD [^\]]+
作成したカスタムパターンは以下のように使用します。
grok {
patterns_dir => ["/opt/logstash/extra_patterns"]
match => { "message" => "GM[\[]%{CUSTOM_WORD:game}[\]]PC[\[]%{CUSTOM_WORD:place}[\]]" }
}
ログの時間をパースして、timestampに利用する
ログに含まれる時間も、標準のパターンには無いため、カスタムパターンを用意します。
CUSTOM_DATE %{YEAR}[\.]%{MONTHNUM}[\.]%{MONTHDAY}
CUSTOM_TIMESTAMP %{CUSTOM_DATE}[\_]%{TIME}
作成したカスタムパターンは以下のように使用します。
grok {
patterns_dir => ["/opt/logstash/extra_patterns"]
match => { "message" => "DT[\[]%{CUSTOM_TIMESTAMP:custom_timestamp}[\.]%{WORD:custom_timezone}[\]] }
}
取り出したcustom_timestampフィールドは文字列なのでdateフィルタでdate型に変換し、timestampに上書きします。timezoneは上手く設定する方法がわかりませんでした、、、
date {
match => ["custom_timestamp", "yyyy.MM.dd_HH:mm:ss"]
target => "@timestamp"
#TODO: fix timezone
timezone => "Asia/Tokyo"
}
最後に
blacks_ratingとwhites_ratingを文字列からfloat型に変換して完成です。
mutate {
convert => {
"blacks_rating" => "float"
"whites_rating" => "float"
}
}
一応、ElasticsearchのID重複エラーの対策で、以下を実施しています。
Elasticsearchでのイベントベースのデータ重複を効果的に防止
Advent Calender 2020はまだまだ続きますが、いろんな方の記事を楽しみに待っています。