すーみそー
sumiso_c0db8cだ。
今回は、何故かバイナリファイルの解読が必要になったとき、Pythonを利用して調査するのに使ったコードをまとめました。
各コードブロックを1つのセルとして実行できるように記載します。
Jupyter Notebookとは
Pythonなどのコードを「セル」単位で実行できるインタラクティブな開発環境です。コードをセル単位で実行・修正・再実行ができ、ファイルの読み書きを試行錯誤するのに便利です。
VScodeでファイルの拡張子を.ipynbとするとnotebook形式で表示されます。Pythonの実行環境にライブラリjupyterの追加が必要です。
# pipでインストール
pip install jupyter
# uvの場合
uv add jupyter
詳しい使い方は解説記事を探してください。
バイナリファイルとは
0と1のビット列で構成されたデータファイルです。テキストファイルとは異なり、人間が直接読めない形式です。pngなどの画像や、Excelのファイルもバイナリファイルです。AIに渡すと、コンテキストによっては解読してみせたりします。
本記事ではバイナリファイルの読み書きをjupyter notebook上で行います。
各セルをコピペで動作確認できるようにしましたので、お試しください。
ファイル読み込み
はじめにバイナリファイルの読み込みから。
基本的にテキストファイルの読み込みと同じですが、モードにバイナリのbを添えます。
filepath = 'b\sumiso.dat'
# ファイルを読み込む
with open(filepath, 'rb') as f:
data = f.read()
data
b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
バイト列が表示されます。\xe3は1バイトのデータを16進数で表現したものです。コピペするときは、1バイトの塊ごとコピーします。
ファイル書き出し
データを処理して最終的に書き出したいときに。
読み込みの時と同様に、モードにバイナリのbを添えます。
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
# ファイルを書き出す
with open("b\sumiso.dat", "wb") as f:
f.write(binary)
デコード
バイナリを指定のエンコードで文字列に戻します。
# デコード
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
binary.decode('utf-8')
'すみそ'
エンコード間違えるとエラーになります。
# デコード
# utf-8
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
binary.decode('shift-jis')
UnicodeDecodeError: 'shift_jis' codec can't decode byte 0x9d in position 8
エンコード
文字列をバイト列に変換します。
# エンコード
name = 'すみそ'
name.encode('utf-8')
b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
エンコードが異なると出力も変わります。
# エンコード
name = 'すみそ'
name.encode('shift-jis')
b'00\x82\xb7\x82\xdd\x82\xbb'
アルファベットの場合、見た目ほぼ変わりませんがデータ型はバイナリ列になります。
name = 'sumiso_c0db8c'
name.encode('utf-8')
b'sumiso_c0db8c'
デコードできます。
b'sumiso_c0db8c'.decode('utf-8')
'sumiso_c0db8c'
数値のエンコード to_bytes
よく文字列のデータの前に、そのデータの長さを指定のバイト数で記載するパターンがあります。後続のデコード処理で、文字列の境界を正しく認識するためです。そのようなときの長さの部分のエンコード例です。
バイナリデータの長さ自体はlenで取得できます。
# 長さを取得
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
len(binary)
9
長さを10進数から、バイナリデータに変換します。
バイト数とバイトオーダーを指定します。
# 長さを取得
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
# 2バイト、リトルエンディアン
len(binary).to_bytes(2, 'little')
b'\t\x00'
バイトオーダーとは、マルチバイトのデータをバイト列として格納する際の並び順です。リトルエンディアンとビッグエンディアンがあります。リトルエンディアンとビッグエンディアンでは並び順が逆になります。
# 長さを取得
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
# 2バイト、ビッグエンディアン
len(binary).to_bytes(2, 'big')
b'\x00\t'
数値のエンコード struct.pack
標準ライブラリのstructを使用しても、バイト列にエンコードできます。
import struct
# 長さを取得
binary = b'\xe3\x81\x99\xe3\x81\xbf\xe3\x81\x9d'
# リトルエンディアン unsigned short(2バイト)
struct.pack('<H', len(binary))
b'\t\x00'
フォーマット文字によって、バイト数やバイトオーダーを指定できます。
# import struct
number = 12639116
display('リトルエンディアン unsigned long long(8バイト)')
display(struct.pack('<Q', number))
display('ビックエンディアン unsigned int(4バイト)')
display(struct.pack('>I', number))
displayは変数や戻り値をそのまま出力する関数です。
'リトルエンディアン unsigned long long(8バイト)'
b'\x8c\xdb\xc0\x00\x00\x00\x00\x00'
'ビックエンディアン unsigned int(4バイト)'
b'\x00\xc0\xdb\x8c'
数値のデコード
反対に、長さ部分をデコードする例です。フォーマット文字はエンコードのときと同様です。
# import struct
c0db8c = b'\x8c\xdb\xc0\x00\x00\x00\x00\x00'
# リトルエンディアン unsigned long long(8バイト)
struct.unpack('<Q', c0db8c)[0]
12639116
まとめ
本記事では、jupyter notebookでバイナリファイルの内容を調査・解析するための基本的な手法をまとめました。
-
バイナリファイルの読み書き
open()関数のモードに'rb'や'wb'を指定して、bytes型としてデータを扱います。 -
文字列とバイト列の変換
文字列オブジェクトの.encode()でバイト列に、バイト列オブジェクトの.decode()で文字列に変換します。この際、エンコーディングの指定が必須です。 -
数値のバイト列変換
整数型の.to_bytes()や標準ライブラリstructを使って、数値(長さなど)をバイト列に変換します。
この記事で紹介した基本操作は、より複雑なファイルフォーマットを解析するための土台となります。ぜひ、次は実際のファイル(例:PNG, GIF, カスタムデータ形式)の仕様書を入手し、struct.unpackなどを活用して、ファイルヘッダーやデータブロックの解析に挑戦してみてください。