4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonAdvent Calendar 2022

Day 17

PythonでWTHOR(リバーシの棋譜データベース)のバイナリ読み込みをやってみる

Posted at

はじめに

どうも、y-tetsuです。

Pythonでバイナリファイルを読み込む練習として、WTHORというリバーシの棋譜データベースを題材にしたのでメモ。データベースを読み込んで、CSVファイルに出力するまでを試しました。

データベースの仕様や読み込みの方法は、@tanaka-a 様の以下の記事を大いに参考にさせていただきました。

できたもの

以下が完成したコードです。

import datetime
import csv


FORMAT = 'iso-8859-2'
RECORD_BYTES = {'header': 16, 'jou': 20, 'trn': 26}
MAX_MOVES = 60


class Wthor:
    def __init__(self, jou='WTHOR.JOU', trn='WTHOR.TRN', wtb='WTH_2022.wtb'):
        self.players = self._get_records(jou, RECORD_BYTES['jou'])
        self.tournaments = self._get_records(trn, RECORD_BYTES['trn'])
        self.games = self._get_games(wtb)

    def _get_records(self, filename, rbytes):
        ret = []
        with open(filename, 'rb') as f:
            f.read(RECORD_BYTES['header'])  # discard header
            for _ in range(self._decode_header(filename)['records']):
                ret.append(f.read(rbytes).decode(FORMAT).replace('\x00', ''))
        return ret

    def _get_games(self, wtb):
        ret = []
        header = self._decode_header(wtb)
        with open(wtb, 'rb') as f:
            f.read(RECORD_BYTES['header'])  # discard header
            for _ in range(header['game_count']):
                ret.append({
                    'match_year': header['match_year'],
                    'tournament': self.tournaments[self._byte2int(f.read(2))],
                    'black': self.players[self._byte2int(f.read(2))],
                    'white': self.players[self._byte2int(f.read(2))],
                    'board_size': header['board_size'],
                    'black_score': self._byte2int(f.read(1)),
                    'theoretical': self._byte2int(f.read(1)),
                    'depth': header['depth'],
                    'record': self._get_record(f),
                })
        return ret

    def _get_record(self, f):
        ret = ""
        for _ in range(MAX_MOVES):
            move = str(self._byte2int(f.read(1)))
            if len(move) == 2:
                ret += chr(ord('a') + int(move[1]) - 1) + move[0]
        return ret

    def _decode_header(self, dbname):
        with open(dbname, 'rb') as f:
            return {
                'created_date': self._get_created_date(f),
                'game_count': self._byte2int(f.read(4)),
                'records': self._byte2int(f.read(2)),
                'match_year': self._byte2int(f.read(2)),
                'board_size': self._byte2int(f.read(1)),
                'match_type': self._byte2int(f.read(1)),
                'depth': self._byte2int(f.read(1)),
            }

    def _get_created_date(self, f):
        y = str(self._byte2int(f.read(1))) + str(self._byte2int(f.read(1)))
        m = str(self._byte2int(f.read(1)))
        d = str(self._byte2int(f.read(1)))
        return datetime.datetime.strptime('-'.join([y, m, d]), '%Y-%m-%d')

    def _byte2int(self, byte, byteorder='little'):
        return int.from_bytes(byte, byteorder=byteorder)

    def to_csv(self, csv_file='output.csv'):
        header = [
            'Match Year', 'Tournament Name', 'Black Player Name',
            'White Player Name', 'Board Size', 'Black Score',
            'Black Theoretical Score', 'Depth', 'Record',
        ]
        with open(csv_file, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(header)
            for game in self.games:
                writer.writerow(game.values())


if __name__ == '__main__':
    Wthor().to_csv()

バイナリファイルの読み込みについて

バイナリモードでファイル読み込み

with open(ファイル名, 'rb') as f:

指定バイト数のbyte型データを取得

byte = f.read(バイト数)

もう一度呼ぶと、続きからデータを取得する。

byte型データをint型に変換

byte = f.read(バイト数)
intdata = int.from_bytes(byte, byteorder=バイトオーダー)

バイトオーダーには'little'または'big'を指定する。

byte型データをiso-8859-2に変換

byte = f.read(バイト数)
string = byte.decode('iso-8859-2')

おわりに

バイト列から数値への変換や文字列へのデコードなど、最初は頭の中がごっちゃになって、ちゃんと読み込めるまでに結構時間がかかりました…。参考にさせていただいた記事にて、すでにバイナリの仕様が整備されていたので、そのあたりの余計な調べものをせずに済み、個人的によい練習になったと思います。

今回の記事で紹介させていただいたコードは、以下でも公開しています。もし良かったら見てみて下さい。

それでは皆様、よきリバーシ・ライフを!

4
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?