Teks bukan satu-satunya penghuni disk: gambar, audio, video, arsip zip, dan database menyimpan byte-byte yang tidak dimaksudkan sebagai teks. Membuka berkas tersebut dengan mode "r" biasa bukan hanya salah — prosesnya akan gagal di tengah jalan karena byte sembarangan tidak selalu membentuk teks yang sah. Chapter ini membuka berkas sebagaimana adanya: bytes mentah, dibaca dengan mode "rb" dan ditulis dengan mode "wb". Posisi ini juga melengkapi gambaran str vs bytes dari Chapter Unicode dan Encoding.

Mode rb dan wb #

Dalam mode biner, method write() menerima object bytes dan read() mengembalikan bytes — tanpa penerjemahan encoding, tanpa penerjemahan newline:

python
data = b"Halo, file biner!\nBaris kedua.\n"

with open("catatan-biner.dat", "wb") as berkas:
    jumlah = berkas.write(data)
    print("Byte yang ditulis:", jumlah)

with open("catatan-biner.dat", "rb") as berkas:
    isi = berkas.read()

print(type(isi).__name__)
print(isi)
print(repr(isi))

Output yang Dihasilkan:

text
Byte yang ditulis: 31
bytes
b'Halo, file biner!\nBaris kedua.\n'
b'Halo, file biner!\nBaris kedua.\n'

Perhatikan dua hal:

  1. Baris 1 (data = b"Halo, file biner!\nBaris kedua.\n"): prefix b membuat literal menjadi bytes, bukan str. Mode "wb" menolak string biasa — konversi str ↔ bytes harus eksplisit lewat encode()/decode().
  2. write() mengembalikan 31: di mode biner, nilai balik dihitung dalam byte, bukan karakter. Keduanya sama untuk teks ASCII, tetapi untuk karakter multi-byte jumlahnya berbeda — ingat kasus é yang memakan dua byte pada UTF-8.

Setiap berkas pada dasarnya adalah berkas biner; mode teks hanyalah lapisan penerjemah di atasnya. Ketika kita memilih "rb"/"wb", kita mematikan penerjemah dan bekerja dengan isi mentahnya.

Mengapa Berkas Biner Tidak Bisa Dibaca sebagai Teks #

Mencoba menterjemahkan byte sembarangan sebagai teks berakhir buruk:

python
>>> b"\x89PNG\r\n\x1a\n".decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte

Byte pertama 0x89 bukan awal karakter UTF-8 yang sah, dan proses dekode langsung berhenti. Kejadiannya identik dengan galat berkas Latin-1 pada Chapter Encoding dan Newline — bedanya, kali ini tidak ada encoding “yang benar” untuk dicoba, karena byte tersebut memang bukan teks melainkan bagian struktur format biner. Satu-satunya cara membaca data biner adalah memahami format-nya: kesepakatan byte ke-berapa berisi apa.

struct — Membaca dan Menulis Angka Biner #

Format biner lazim menyimpan angka tetap lebar: 4 byte untuk integer besar, 2 byte untuk integer kecil. Module struct mengubah angka Python menjadi byte dan sebaliknya:

python
import struct

header = struct.pack("<4sHH", b"DATA", 7, 2025)
print(header, "panjang", len(header))

kode, versi, tahun = struct.unpack("<4sHH", header)
print(kode, versi, tahun)

Output yang Dihasilkan:

text
b'DATA\x07\x00\xe9\x07' panjang 8
b'DATA' 7 2025

String format "<4sHH" adalah kontrak yang dibaca dari kiri:

  1. <: byte order little-endian — byte rendah dulu; simbol > berarti big-endian (byte tinggi dulu).
  2. 4s: empat byte data mentah, cocok untuk kode seperti b"DATA".
  3. H dua kali: dua integer 16-bit tanpa tanda — angka 7 menjadi \x07\x00 dan angka 2025 menjadi \xe9\x07 dalam little-endian.

Hasil pack() persis 8 byte, dan unpack() mengembalikan tuple dengan komponen-komponennya. Inilah cara program membaca header berkas, packet jaringan, dan rekaman data biner lain.

Bedah Header PNG #

Sebagai contoh nyata pertama, mari kita buat berkas PNG berukuran 69 byte — satu piksel merah — lalu membedah dua bagian pertamanya: signature dan chunk IHDR:

python
import struct
import zlib

def buat_png_merah():
    header = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)
    ihdr = struct.pack(">I", len(header)) + b"IHDR" + header
    ihdr += struct.pack(">I", zlib.crc32(b"IHDR" + header))

    piksel = zlib.compress(b"\x00\xff\x00\x00")
    idat = struct.pack(">I", len(piksel)) + b"IDAT" + piksel
    idat += struct.pack(">I", zlib.crc32(b"IDAT" + piksel))

    iend = b"\x00\x00\x00\x00IEND" + struct.pack(">I", zlib.crc32(b"IEND"))

    return b"\x89PNG\r\n\x1a\n" + ihdr + idat + iend

with open("piksel.png", "wb") as berkas:
    berkas.write(buat_png_merah())

with open("piksel.png", "rb") as berkas:
    signature = berkas.read(8)
    panjang = berkas.read(4)
    jenis = berkas.read(4)
    print("Signature :", signature)
    print("Chunk     :", jenis, "ukuran", struct.unpack(">I", panjang)[0])
    info = struct.unpack(">IIBBBBB", berkas.read(13))
    print("Lebar     :", info[0])
    print("Tinggi    :", info[1])
    print("Detail    : bit depth", info[2], ", color type", info[3])

print("Ukuran berkas:", len(open("piksel.png", "rb").read()), "byte")

Output yang Dihasilkan:

text
Signature : b'\x89PNG\r\n\x1a\n'
Chunk     : b'IHDR' ukuran 13
Lebar     : 1
Tinggi    : 1
Detail    : bit depth 8 , color type 2
Ukuran berkas: 69 byte

Struktur PNG terbaca langsung dari kode tersebut:

  1. 8 byte signature b"\x89PNG\r\n\x1a\n": penanda khas yang sama pada semua berkas PNG — alat deteksi tipe berkas memakai byte ini, bukan sekadar ekstensi .png.
  2. Setiap chunk berpola [panjang][jenis][data][CRC32]: panjang 4 byte big-endian (>), jenis 4 huruf, data, lalu checksum zlib.crc32() untuk mendeteksi kerusakan.
  3. Chunk IHDR selebar 13 byte: >IIBBBBB berisi lebar, tinggi, bit depth, color type, dan tiga byte pengaturan. Nilai (1, 1, 8, 2, ...) membaca: gambar 1x1 piksel, 8 bit per kanal, color type 2 (RGB).

Fungsi buat_png_merah() mengemas scanline piksel merah \xff\x00\x00 lewat zlib.compress() — inti kompresi PNG — lalu menyusun ketiga chunk sesuai spesifikasi. Hasilnya berkas PNG sungguhan yang bisa dibuka aplikasi gambar apa pun: biner yang utuh, dibangun murni dari bytes dan struct.