LoginSignup
0
1

More than 5 years have passed since last update.

[ポエム] パーサーを作ろうぜ☆(^~^)やっぱ めんどくせ☆(^q^)他の誰かが書いてくれたのを使うのがいいよな☆(^q^)

Last updated at Posted at 2019-04-06
装備品
目: ひのきの目

機械学習するとき データを読み込みたくなる。
例えば 将棋なら棋譜を読み込みたくなる。
将棋の棋譜のファイルとか .kif, .csa, バージョン違い、他、独自形式がいくつかある。

daiwa.kif
# ----  Kifu for Windows V6.00 棋譜ファイル  ----
開始日時:2007/03/21
終了日時:2007/03/21
持ち時間:2時間
棋戦:大和証券杯特別対局
戦型:四間飛車
場所:品川プリンスホテル,クラブ eX
手合割:平手  
先手:Bonanza
後手:渡辺明
手数----指手---------消費時間--
   1 7六歩(77)   ( 0:01/00:00:01)
   2 8四歩(83)   ( 2:32/00:02:32)
   3 6六歩(67)   ( 0:01/00:00:02)
   4 3四歩(33)   ( 0:11/00:02:43)
   5 6八飛(28)   ( 0:01/00:00:03)
   6 6二銀(71)   ( 0:14/00:02:57)
   7 4八玉(59)   ( 0:01/00:00:04)
   8 5四歩(53)   ( 0:17/00:03:14)
   9 3八玉(48)   ( 0:01/00:00:05)
  10 4二玉(51)   ( 0:20/00:03:34)
以下略

これの変換プログラムも Blunder(ブランダー)のGitHubや、柿木(かきのき)将棋のホームぺージに置いてあるが、コンパイルしようとしたら あれやこれや 揃えるのが
めんどくさいんで、それでは作ったものを プログラマー以外の人に使ってもらうにも大変。 設定不要のパーサーを自分でイチから作る。

じゃあ、好きな欲望のレベルを選べだぜ。

  • バトル的な理由から、将棋の指し手だけ欲しい
  • エンターテイメント的な理由から、対局場などの情報も欲しい
  • ヒストリカル的な理由から、ファイルを読み込んで、書きだしたら、ファイルが完全一致するような 可逆性 があるものが欲しい

など、理由が いろいろ あれば、方法も それぞれある。ここでは バトル を選択する。

装備品
目: バトルの目

目を装備すると、今まで景色だったものは、素材に変わる。

   1 7六歩(77)   ( 0:01/00:00:01)
   2 8四歩(83)   ( 2:32/00:02:32)
   3 6六歩(67)   ( 0:01/00:00:02)
   4 3四歩(33)   ( 0:11/00:02:43)

上記のような 手数で始まってるとこだけ 欲しくて、

# ----  Kifu for Windows V6.00 棋譜ファイル  ----
開始日時:2007/03/21
終了日時:2007/03/21
持ち時間:2時間

上記のようなヘッダーは要らない☆
欲しいもの、捨てていいものがはっきりすると、そのあとの行動もスマートになる。

仕様書がどこに落ちてあるのか見つからなかったので、
実装を経験的に調べてみると

   1
   2
   3
   4
   5
   6
   7
   8
   9
  1
  2
  3
  4
  5
  6
  7
  8
  9
 1
 2
 3
 4
 5
 6
 7
 8
 9
1
2
3
4
5
6
7
8
9

上記の36パターンで始まる行は 将棋の指し手のようだ。

疑似コードで書くと

疑似コード
foreach line in file
{
    if line.starts_with("   1") | line.starts_with("   2") | line.starts_with("   3")
 | line.starts_with("   4") | | line.starts_with("   5") | line.starts_with("   6")
 | line.starts_with("   7")    以下略) 
    {
        # ここに処理を書く
    }
}

というコピペ技や、

疑似コード
foreach line in file
{
    line <= line.trim_begin()
    if try_parse_int(line[0])
    {
        # ここに処理を書く
    }
}

といった 数字の出現判定だけでいけそうだ。

Rust言語で文字列パーサーを書いているんだが、「始」が 3byte で めんどくさい。1文字切り取るのがめんどくさい。

thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '開' (bytes 0..3) of `開始日時:2007/03/21`', libcore\str\mod.rs:2111:5

パーサー以前に 文字列が めんどくさい。

    Finished dev [unoptimized + debuginfo] target(s) in 1.76s
     Running `target\debug\examples\eat_a_kif.exe --path C:/muzudho/kifuwarabe-wcsc29-learn/output-wcsc-record/copied-daiwa/daiwa.kif`
args.path = 'C:/muzudho/kifuwarabe-wcsc29-learn/output-wcsc-record/copied-daiwa/daiwa.kif'.
Ignored: # ----  Kifu for Windows V6.00 棋譜ファイル  ----
Ignored: 開始日時:2007/03/21
Ignored: 終了日時:2007/03/21
Ignored: 持ち時間:2時間
Ignored: 棋戦:大和証券杯特別対局
Ignored: 戦型:四間飛車
Ignored: 場所:品川プリンスホテル,クラブ eX
Ignored: 手合割:平手  
Ignored: 先手:Bonanza
Ignored: 後手:渡辺明
Ignored: 手数----指手---------消費時間--
Move   :    1 7六歩(77)   ( 0:01/00:00:01)
Move   :    2 8四歩(83)   ( 2:32/00:02:32)
Move   :    3 6六歩(67)   ( 0:01/00:00:02)
Move   :    4 3四歩(33)   ( 0:11/00:02:43)
Move   :    5 6八飛(28)   ( 0:01/00:00:03)
Move   :    6 6二銀(71)   ( 0:14/00:02:57)
Move   :    7 4八玉(59)   ( 0:01/00:00:04)
Move   :    8 5四歩(53)   ( 0:17/00:03:14)
Move   :    9 3八玉(48)   ( 0:01/00:00:05)
Move   :   10 4二玉(51)   ( 0:20/00:03:34)

指し手と判定すれば Move :、 それ以外は Ignored: を頭に付けて デバッグライト(debug write) する。

手数も消費時間も 今回は欲しくないので、

    1 7六歩(77)   ( 0:01/00:00:01)

ではなく

7六歩(77)

だけ欲しい。

"\s*\d+ ((.*))*\([ 0-9:/]+\)"
group[1]

ぐらい書けば 引っこ抜いてくれそうだが、実装的な説明は省略して そういうことは できたものとして話しを進める。
正規表現の説明は、めんどくせ、Debuggex 見て知れ。

正規表現チェックツールまとめ

正規表現を簡単に書くテクニックは、

.*(.*)?.*

この形から、

.*\s*(.*)?.*
.*\s*\d+(.*)?.*

と、正規表現をグラフィカルに見れるツールの画面を見ながら マッチするところを広げていけだぜ。

あっ!

  50 4二金(32)   ( 0:40/00:13:43)
  51 6六角打     ( 2:15/00:39:01)

打のとき 丸かっこ が外れるのか。

  47 6五歩(66)   ( 2:07/00:36:45)
  48 7七角成(33) ( 1:14/00:13:03)
  49 同 桂(89)   ( 0:01/00:36:46)
  50 4二金(32)   ( 0:40/00:13:43)
  51 6六角打     ( 2:15/00:39:01)

周りを見ると けっこう 書式がばらばらだ。

  80 3九成香(49) ( 0:55/01:01:22)

駒の名前も1文字だけではなく 成香 のように2文字のもある。

    Finished dev [unoptimized + debuginfo] target(s) in 1.90s
     Running `target\debug\examples\eat_a_kif.exe --path C:/muzudho/kifuwarabe-wcsc29-learn/output-wcsc-record/copied-daiwa/daiwa.kif`
args.path = 'C:/muzudho/kifuwarabe-wcsc29-learn/output-wcsc-record/copied-daiwa/daiwa.kif'.
Ignored: # ----  Kifu for Windows V6.00 棋譜ファイル  ----
Ignored: 開始日時:2007/03/21
Ignored: 終了日時:2007/03/21
Ignored: 持ち時間:2時間
Ignored: 棋戦:大和証券杯特別対局
Ignored: 戦型:四間飛車
Ignored: 場所:品川プリンスホテル,クラブ eX
Ignored: 手合割:平手  
Ignored: 先手:Bonanza
Ignored: 後手:渡辺明
Ignored: 手数----指手---------消費時間--
7六歩(77)
8四歩(83)
6六歩(67)
3四歩(33)
6八飛(28)
6二銀(71)
4八玉(59)
5四歩(53)
3八玉(48)
4二玉(51)

欲しいところが、欲しいところだけ 取れるようになる。ここまでが シンタックスパース(構文解析)、レキシカルパース(字句解析)フェーズだぜ。
だが、このままでは使いにくい。

6五歩(66)
7七角成(33)
同 桂(89)
4二金(32)
6六角打

この変化を 1パターンに おさめておきたい。

筋  段  同  駒  成  打  (  筋  段  )
--  --  --  --  --  --  -  -  -  -
6  五      歩          (   6  6  )
7  七      角  成      (   3  3  )
        同 桂          (   8  9  )
6  六      角      打

分解すると こうなるようだ。

Phase   0  1   2   3   4   5  6   7  8  9
       筋  段  同  駒  成  打  (  筋  段  )
       --  --  --  --  --  --  -  -  -  -
       6  五      歩          (   6  6  )
       7  七      角  成      (   3  3  )
               同 桂          (   8  9  )
       6  六      角      打

フェーズの番号を振ると 10。 逐次 マッチングしていけばいい。

疑似コード
struct Sasite {
    file : number,
    rank : number,
    same : bool,
    piece : string,
    promote : bool,
    drop : bool,
    file2 : number,
    rank2 : number,
}

説明するのがめんどくさいので こういう適当な入れ物を作っておくと思えだぜ。
この入れ物を埋めていけばいい。

疑似コード書くの めんどくさいので Rust言語そのまま張り付ける。

use parser::*;
use piece_etc::*;
use regex::Regex;

pub struct KifMove {
    pub dst_file : i8,
    pub dst_rank : i8,
    pub is_same : bool,
    pub piece : String,
    pub is_promote : bool,
    pub is_drop : bool,
    pub src_file : i8,
    pub src_rank : i8,
}
impl KifMove {
    pub fn to_sign(&self) -> String {
        let mut sign = "".to_string();

        sign = format!("{} {}", sign, self.dst_file);
        sign = format!("{} {}", sign, self.dst_rank);
        sign = format!("{} {}", sign, self.is_same);
        sign = format!("{} {}", sign, self.piece);
        sign = format!("{} {}", sign, self.is_promote);
        sign = format!("{} {}", sign, self.is_drop);
        sign = format!("{} {}", sign, self.src_file);
        sign = format!("{} {}", sign, self.src_rank);

        sign
    }

    pub fn parse(line:&str) -> Option<KifMove> {
        let re = Regex::new(r"\s*\d+ ((.*))*\([ 0-9:/]+\)").unwrap();
        let caps = re.captures(line).unwrap();
        let sign = caps.get(1).map_or("", |m| m.as_str());
        print!("{} -> ", sign);

        let mut mv = KifMove {
            dst_file : 0,
            dst_rank : 0,
            is_same : false,
            piece : "".to_string(),
            is_promote : false,
            is_drop : false,
            src_file : 0,
            src_rank : 0,
        };

        /**
Phase   0  1   2   3   4   5  6   7  8  9
       筋  段  同  駒  成  打  (  筋  段  )
       --  --  --  --  --  --  -  -  -  -
       6  五      歩          (   6  6  )
       7  七      角  成      (   3  3  )
               同 桂          (   8  9  )
       6  六      角      打
         */
        let mut nth = 0;

        if 0 < sign.len() - nth {
            // Phase 0.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.dst_file = match ch.as_str() {
                "1" => 1,
                "2" => 2,
                "3" => 3,
                "4" => 4,
                "5" => 5,
                "6" => 6,
                "7" => 7,
                "8" => 8,
                "9" => 9,
                _ => {nth -= 1; 0},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 1.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.dst_rank = match ch.as_str() {
                "一" => 1,
                "二" => 2,
                "三" => 3,
                "四" => 4,
                "五" => 5,
                "六" => 6,
                "七" => 7,
                "八" => 8,
                "九" => 9,
                _ => {nth -= 1; 0},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 2.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.is_same = match ch.as_str() {
                "同" => {
                    let ch = sign.chars().nth(nth).unwrap().to_string();
                    nth += 1;
                    match ch.as_str() {
                        " " => true,
                        _ => panic!("Unexpected same ch: '{}'.", ch),
                    }
                },
                _ => {nth -= 1; false},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 3.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.piece = match ch.as_str() {
                "歩" => "歩",
                "香" => "香",
                "桂" => "桂",
                "銀" => "銀",
                "金" => "金",
                "玉" => "玉",
                "角" => "角",
                "飛" => "飛",
                "と" => "と",
                "馬" => "馬",
                "龍" => "龍",
                "成" => {
                    let ch = sign.chars().nth(nth).unwrap().to_string();
                    nth += 1;
                    match ch.as_str() {
                        "香" => "成香",
                        "桂" => "成桂",
                        "銀" => "成銀",
                        "金" => "成金",
                        _ => panic!("Unexpected promoted ch: '{}'.", ch),
                    }
                },
                _ => {nth -= 1; ""},
            }.to_string();
        }

        if 0 < sign.len() - nth {
            // Phase 4.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.is_promote = match ch.as_str() {
                "成" => true,
                _ => {nth -= 1; false},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 5.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.is_drop = match ch.as_str() {
                "打" => true,
                _ => {nth -= 1; false},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 6.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            match ch.as_str() {
                "(" => {},
                _ => {nth -= 1},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 7.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.src_file = match ch.as_str() {
                "1" => 1,
                "2" => 2,
                "3" => 3,
                "4" => 4,
                "5" => 5,
                "6" => 6,
                "7" => 7,
                "8" => 8,
                "9" => 9,
                _ => {nth -= 1; 0},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 8.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            mv.src_rank = match ch.as_str() {
                "1" => 1,
                "2" => 2,
                "3" => 3,
                "4" => 4,
                "5" => 5,
                "6" => 6,
                "7" => 7,
                "8" => 8,
                "9" => 9,
                _ => {nth -= 1; 0},
            };
        }

        if 0 < sign.len() - nth {
            // Phase 9.
            let ch = sign.chars().nth(nth).unwrap().to_string();
            nth += 1;
            match ch.as_str() {
                ")" => {},
                _ => {nth -= 1},
            };
        }

        Some(mv)
    }
}

だいたい動くだろ。

同 飛(27)    ->  :  0 0 true 飛 false false 2 7
1五金打      ->  :  1 5 false 金 false true 0 0
2四歩打      ->  :  2 4 false 歩 false true 0 0
2六金(15)    ->  :  2 6 false 金 false false 1 5
2三歩成(24)  ->  :  2 3 false 歩 true false 2 4
同 金(32)    ->  :  0 0 true 金 false false 3 2
2四歩打      ->  :  2 4 false 歩 false true 0 0
2七歩打      ->  :  2 7 false 歩 false true 0 0
2三歩成(24)  ->  :  2 3 false 歩 true false 2 4
3九龍(99)    ->  :  3 9 false 龍 false false 9 9
2二と(23)    ->  :  2 2 false と false false 2 3
同 角(33)    ->  :  0 0 true 角 false false 3 3
3九銀(28)    ->  :  3 9 false 銀 false false 2 8
2八金打      ->  :  2 8 false 金 false true 0 0
同 銀(39)    ->  :  0 0 true 銀 false false 3 9
同 歩成(27)  ->  :  0 0 true 歩 true false 2 7
同 馬(91)    ->  :  0 0 true 馬 false false 9 1
2七歩打      ->  :  2 7 false 歩 false true 0 0
同 馬(28)    ->  :  0 0 true 馬 false false 2 8
同 金(26)    ->  :  0 0 true 金 false false 2 6
3九銀打      ->  :  3 9 false 銀 false true 0 0
3八銀打      ->  :  3 8 false 銀 false true 0 0
2八金打      ->  :  2 8 false 金 false true 0 0
同 金(27)    ->  :  0 0 true 金 false false 2 7
同 銀(39)    ->  :  0 0 true 銀 false false 3 9
2七歩打      ->  :  2 7 false 歩 false true 0 0

ぱっと見た感じ それっぽい感じで取れている。
ここらへんは まだ レキシカルパース(字句解析)☆

2八金打      ->  :  2 8 false 金 false true 0 0
同 銀(39)    ->  :  0 0 true 銀 false false 3 9
同 歩成(27)  ->  :  0 0 true 歩 true false 2 7
同 馬(91)    ->  :  0 0 true 馬 false false 9 1

が連続するところを見ていただきたい。
同は 何を示している 代わりの記号なのか。 というと 同が始まる最初の1個前の 2八 をずっと続けることだが、

レキシカルパースの中で 一緒に  も解決してしまうのではなく、
レキシカルパースの結果を使って  を解決することにする。
その理由としては、バグったときに ここまではバグっていない、ここから先でバグった、と問題を切り分けることができて バグを探しやすい ことだぜ。

        // '同'を解決する。
        let mut pre_file = 0;
        let mut pre_rank = 0;
        for mov in &mut record.items {
            if mov.is_same {
                mov.dst_file = pre_file;
                mov.dst_rank = pre_rank;
            }

            pre_file = mov.dst_file;
            pre_rank = mov.dst_rank;

            println!("Move : {}", mov.to_sign());
        }

実行速度を優先するのは 実行速度が遅いと思ってるやつが 完成しているときでいい。

Move :  6 7 false 飛 false false 6 8
Move :  8 6 false 歩 false false 8 5
Move :  8 6 true 歩 false false 8 7
Move :  8 6 true 飛 false false 8 3
Move :  9 1 false 角 true false 4 6
Move :  8 9 false 飛 true false 8 6
Move :  4 4 false 歩 false false 4 5
Move :  4 4 true 銀 false false 5 3
Move :  4 7 false 飛 false false 6 7
Move :  9 9 false 龍 false false 8 9
Move :  6 4 false 歩 false false 6 5
Move :  3 5 false 銀 false false 4 4
Move :  3 6 false 歩 false false 3 7
Move :  4 6 false 香 false true 0 0
Move :  3 7 false 飛 false false 4 7
Move :  2 6 false 銀 false false 3 5
Move :  2 6 true 銀 false false 2 7
Move :  4 9 false 香 true false 4 6
Move :  6 3 false 歩 true false 6 4
Move :  3 9 false 成香 false false 4 9
Move :  3 9 true 金 false false 3 8
Move :  2 5 false 歩 false false 2 4
Move :  2 5 true 銀 false false 2 6
Move :  2 7 false 歩 false true 0 0
Move :  2 7 true 飛 false false 3 7
Move :  2 6 false 歩 false true 0 0
Move :  2 6 true 飛 false false 2 7
Move :  1 5 false 金 false true 0 0
Move :  2 4 false 歩 false true 0 0
Move :  2 6 false 金 false false 1 5

ぱっと見た感じ、
3列目が true になっている行は、1列目、2列目の数が 上の行と同じになっているだろ。
このように テストをしながら
できている ところ
を増やして 進めていくのが開発だぜ。

これだけ変換できれば 将棋の駒を動かすことができる。 棋譜の解析はおわり。
途中っぽいが 終わり。

保存形式のコンバーターはどうやって作るかだって?

将棋盤で駒を動かせるんだったら 棋譜 は作れるだろ。
好きな形式のファイルを読み込んで、好きな形式のファイルで保存しろだぜ。
べつに どの形式から どの形式へ直接コンバートするっぽいやつは作らなくていい。ピボット!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1