はじめに
どうも、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')
おわりに
バイト列から数値への変換や文字列へのデコードなど、最初は頭の中がごっちゃになって、ちゃんと読み込めるまでに結構時間がかかりました…。参考にさせていただいた記事にて、すでにバイナリの仕様が整備されていたので、そのあたりの余計な調べものをせずに済み、個人的によい練習になったと思います。
今回の記事で紹介させていただいたコードは、以下でも公開しています。もし良かったら見てみて下さい。
それでは皆様、よきリバーシ・ライフを!