Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのpack・unpackメソッド完全ガイド:バイナリデータの読み書きをマスターしよう

この記事では、Rubyのpackメソッドとunpackメソッドについて詳しく解説します。

そもそも、なぜこれらのメソッドが必要なのでしょうか?

テキストデータの処理は、バイナリデータの処理よりもはるかに簡単だからです。

テキストであれば、次のような便利な機能が使えます。

  • 正規表現
  • scanmatchなどの文字列操作メソッド
  • gsub

しかし、バイナリデータを扱う場合は追加の作業が必要になります。そこで活躍するのが、Array#packとString#unpackという2つのメソッドです。

まずはシンプルな文字列の例から始めて、徐々に実用的な内容へと進んでいきましょう。

文字列をASCII値(数値)に変換する

次のコードは、文字列内の各文字を10進数の数値に変換します。

str = "AABBCC"
str.unpack("c*")

# [65, 65, 66, 66, 67, 67]

unpackに渡している引数"c*"に注目してください。

これは「フォーマット文字列」と呼ばれるもので、unpackに対して「データをどう処理するか」を指示します。この場合、cは「1文字を取り出して整数値に変換する」という意味です(String#ordメソッドでも同じことができます)。

アスタリスク*は「入力データ全体に対してこのフォーマットを繰り返し適用する」ことを意味しています。

16進数を文字列に変換する

packメソッドでHを使うと、16進数から文字列への変換が行えます。

例:

["414243"].pack("H*")
# "ABC"

"ABC".unpack("H*")
# ["414243"]

16進数を整数に変換する方法

このフォーマット文字列は、4バイトのデータを受け取って整数を返します。ここで注意すべき点は、バイト列が「リトルエンディアン」形式であることです。

例:

"\xff\x00\x00\x00".unpack("l").first
# 255
"\x90\xC0\xDD\x08".unpack("l").first
# 148750480

firstを使用しているのは、unpackの戻り値が配列だからです。

unpackメソッドによるバイナリファイルの解析

EXE、PNG、GZIPのようなバイナリファイルはどうやって読み込めばよいのでしょうか?

これらのファイルを普通のテキストとして読み込むと、まるでランダムなデータのように見えてしまいます…

Rubyのpack・unpackメソッド完全ガイド:バイナリデータの読み書きをマスターしよう

しかし、それは決して無秩序なデータではありません。

多くのファイル形式には仕様が文書化されており、その構造に沿ってデータを読み取り、意味のある形に変換できるのがunpackメソッドです。

以下に例を示します:

binary_data     = "\x05\x00\x68\x65\x6c\x6c\x6f"
length, message = binary_data.unpack("Sa*")

# [5, "hello"]

この例では、バイナリデータ(1と0よりもはるかにコンパクトな16進数で表記)の先頭に、2バイト(16ビット)の長さフィールドがあり、後続の文字列の長さが格納されています。その後に文字列本体が続きます。

バイナリファイルやネットワークプロトコルには、「長さ」フィールドを持つ構造が非常に一般的です。

このフィールドがあることで、パーサーは読み込むべきバイト数を正確に把握できます

ちなみに、この例では長さとデータを一度に読み込んでいますが、これは説明をシンプルにするためです。実際の処理では段階的に読み分けることもできます。

BinData gemの使い方

さらに、バイナリ構造の解析専用に作られた「bindata」というgemもあります。

使用例:

class BinaryString < BinData::Record
  endian :little
  uint16 :len
  string :name, :read_length => :len
end

read_lengthパラメータに注目してください。これを指定すると、bindataが自動的にフィールドから長さを計算してくれるため、手作業の手間を大幅に省けます。

任意のバイナリ形式のパーサーを作成したい場合は、以下の手順に従いましょう。

  1. 対象フォーマットの仕様を見つける(公開されていない場合はリバースエンジニアリングが必要ですが、それ自体が一つの大きなテーマになります)
  2. ファイルの各セクションごとにbindataクラスを定義する(通常、最初にメタデータを含むヘッダーセクションがあり、その後に複数のデータセクションが続く構成になっています)
  3. データを読み込み、目的に応じて処理する(たとえばPNGなら画像の色を変更することも可能)
  4. 完成!

bindataの実践的な使用例を見たい方は、GitHubで公開しているPNGパーサーの実装を参考にしてみてください。

Base64エンコーディング

「Base64」というエンコーディング方式をご存知でしょうか?URLなどで見かけたことがあるかもしれません。

見た目はこんな感じです:

U2VuZCByZWluZm9yY2VtZW50cw==

末尾のイコール2つ(==)は、Base64エンコードされた文字列の典型的な特徴です。ただし、入力によってはイコール記号が付かない場合もあります(イコールはパディングとして使われます)。

では、なぜこの話をしているのか…

それ自体が有用な知識だということはもちろんですが、実は文字列をpackメソッドだけでBase64に変換できるのです。

以下のコードをご覧ください:

def encode64(bin)
  [bin].pack("m")
end

encode64 "abcd"

# "YWJjZA==\n"

実はこの方法こそ、標準ライブラリのBase64モジュール内部で実際に使われている手法なのです。

まとめ

この記事では、バイナリデータを扱うためのpackメソッドとunpackメソッドについて学びました。これらを活用すれば、バイナリファイルの解析、文字列からASCII値への変換、Base64エンコーディングなど、さまざまな処理が実現できます。

今後もこのような記事をお楽しみいただけるよう、ぜひシェアと購読をお願いします!🙂


  1. Rubyの入出力(IO)完全ガイド:ファイルディスクリプタから標準ストリームまで徹底解説

    「I/O」とは「Input/Output(入出力)」の略称です。 入力とは、コンピュータ、Rubyメソッド、さらには人間の脳といった対象に取り込まれる、あらゆるデータや情報のことを指します。 入力の例 キーボードで押下するキー マウスのクリック 読んでいる本 一方、出力とは入力の結果として外に出てくるものすべてです。 出力の例 1 + 1 の計算結果 読んだ記事の要約文 淹れたコーヒー Rubyにおいて「I/O」と言う場合、通常はファイルの読み書き、ネットワークソケットの操作、画面への情報表示を指します。 IOクラスを理解する Rubyでは IO もまたクラスの一つであり、File や

  2. Rubyで実践する関数型プログラミング完全ガイド ― 純粋関数・イミュータブルデータ・カリー化の基本

    Rubyを書いていると、「関数型プログラミング」という言葉を目にする機会が増えてきます。しかし、実際にどんなものなのか、自分のコードに取り入れるべきなのか、疑問に感じている方も多いのではないでしょうか。 関数型プログラミングとは、具体的に何を指すのか? オブジェクト指向プログラミング(OOP)とは何が違うのか? Rubyでも関数型的な書き方を採用すべきなのか? この記事では、これらの疑問にわかりやすく答えながら、Rubyで今日から使える関数型プログラミングの考え方とテクニックを解説します。 関数型プログラミングとは? 関数型プログラミングは一時的な流行や難しい専門用語ではなく、長い歴史を持