Pythonで16進数ダンプするプログラムを作る

今日も見に来てくださって、ありがとうございます。

 Python勉強会、ひととおりの要素について、説明させていただきました。しかし、実際にプログラミングしないと、なかなか身につきません。ということで、「バイナリファイルの内容を16進数でダンプするプログラムを作る」という課題を用意しました。そんなに難しくはないのですけど、いろいろと考えなければならないことがあり、取り組みがいのある課題ではないでしょうか。

【課題】ファイルの内容を16進数でダンプする
ファイル名:hexdump.py
使い方:python hexdump.py ダンプしたいファイル名

 お時間のない方に、出来上がり結果は以下の通りです。

from sys import argv
from os.path import exists

process = '処理:ファイルの内容を16進数でダンプする'
howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'

if len(argv) != 2:
    print(howtouse)
    exit()

if not exists(argv[1]):
    print(f"指定されたファイルは存在しません。 [{argv[1]}] ")
    exit()

with open(argv[1], 'rb') as f:
    data = f.read()

for i, x in enumerate(data, 1):
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

 さて、まずは、引数の「ダンプしたいファイル名」が指定される、というところから。これは、sysモジュールのargvを使います。ちょっとテスト的に、以下のように入力してみましょう。

from sys import argv
print(len(argv), argv)

実行結果は以下のようになります。

(base) C:\work\friStudy>python hexdump.py test
 2 ['hexdump.py', 'test']

 sys.argvは、実行時引数が格納されていて、一つ目が自分自身、以降は指定された値が順番に格納されてきます。引数は「ダンプしたいファイル名」ということなので、len(argv) == 2はチェックする必要がありますね。次に指定されたファイルが存在することをチェックしましょう。ファイルが存在するかどうかは、os.path.existsを使って確認することができます。ここまでで、以下の通りです。

from sys import argv
from os.path import exists

process = '処理:ファイルの内容を16進数でダンプする'
howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名'

if len(argv) != 2:
    print(howtouse)
    exit()

if not exists(argv[1]):
    print(f"指定されたファイル[{argv[1]}]は存在しません。")
    exit()

 ここで何気なく登場しましたが、f'使い方:python {argv[0]} ダンプしたいファイル名'のように、文字列の引用符の前にfがついていると、文字列中の{...}の中に記載の変数を展開して文字列にしてくれます。この例の場合、argv[0]hexdump.pyになります。

 ここから本番です。指定されたファイルを開いて内容を16進数でダンプしていきます。1バイトずつ処理していきますので、文字列ではなく、バイトオブジェクトとして処理する必要がありますので、openのモードは'rb'で指定します。rreading、読み込み専用で、bbinary、バイナリですね。通常のプログラミングならopenしてcloseということになりますが、ここはパイソニックにwith句を使います。これで何かあっても必ず自動でcloseが呼び出されることが保証されます。

with open(argv[1], 'rb') as f:
    data = f.read()

これで、ファイルの内容をすべて取得できます。あとは、出力ですね。1バイトずつ、16進数で出力しましょう。スペースをひとつずつあけて、20バイトで改行、10バイトでスペースふたつ、出力するようにしてみましょう。

for i, x in enumerate(data, 1):
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

ここで何気なくenumerateが登場しました。基本的に、Pythonのforループはシーケンスと呼ばれる繰り返し要素を持ったもの(ここではdata)を繰り返して処理するために使います。対象となる要素をひとつずつ処理していくときにはdatafor x in data:と指定するだけで済むのです。では、enumerateは何のために必要なのでしょうか。今回のように、要素を数えていって、20個目で改行したいとか、別のリストの同じ位置の要素を一緒に処理したいときに使用できます。上記のように指定すると、インデックス(順番)と要素を同時に取得することができるようになります。いわゆるイディオムだと思って覚えておきましょう。ちなみに、今回指定した第二引数の1は、インデックスの初期値で指定しない場合は0から始まることになります。もちろんenumerateを使わなくても以下のように記載できますが、使った方がよりパイソニックだそうです。二行少なく書けるからかな?(笑)

i = 0
for x in data:
    i += 1
    print(f"{x:02x}",end=' ')
    if i % 20 == 0:
        print()
    elif i % 10 == 0:
        print(end=' ')

ちなみに、printend=' 'は、出力の最後に追加される文字で、初期値は改行コードになります。end=' 'を指定することで、改行が抑止されて空白が追加されます。このため、20行ごとの改行のためにprint()が記述されています。また、f'{x:02x}'の記載ですが、上述したように変数xの値を文字列に展開してくれますが:02xのようにコロン(:)を付けることで書式設定することができます。この指定で、前ゼロ付きの2桁、16進数で出力します。

これらを実行すると、以下のように結果が出力されます。

(base) C:\work\friStudy>python hexdump.py hexdump.py
66 72 6f 6d 20 73 79 73 20 69  6d 70 6f 72 74 20 61 72 67 76
0d 0a 66 72 6f 6d 20 6f 73 2e  70 61 74 68 20 69 6d 70 6f 72
74 20 65 78 69 73 74 73 0d 0a  0d 0a 70 72 6f 63 65 73 73 20
(… 中略 …)
20 20 20 70 72 69 6e 74 28 29  0d 0a 20 20 20 20 65 6c 69 66
20 69 20 25 20 31 30 20 3d 3d  20 30 3a 0d 0a 20 20 20 20 20
20 20 20 70 72 69 6e 74 28 65  6e 64 3d 27 20 27 29 0d 0a
(base) C:\work\friStudy>

これでいろいろなファイルを16進数でダンプ出力できるようになりました。

今回はここまでです。