🔍

Goの標準ライブラリでファイル形式を判別する

2024/05/19に公開

リハビリを兼ねて小ネタをば。

何らかのプログラムを書いているときに「ファイル形式によってデコード方法を変えたい」など、ファイル形式の判別が必要な場合があります。ファイル形式は拡張子やファイルの先頭にある固有のバイト列を調べるなどすれば判別可能です。

でも世の中にいくつもあるファイル形式それぞれのヘッダに対応すべく、都度仕様を調べて実装するのはちょっとめんどくさい作業ですよね。かといってそれだけのためにサードパーティライブラリに依存するのも……となりがちです。

http.DetectContentType

そんなときこそnet/httpDetectContentType関数を使うときです。

net/http
func DetectContentType(data []byte) string

https://pkg.go.dev/net/http#DetectContentType

こいつは与えられたバイト列を基にMIMEタイプを返してくれます。不明なときはapplication/octet-streamを返し、判別には最大でも512バイトあればいいそうです。

元々HTTPサーバで使用する想定の関数でWHATWGの仕様に準拠したアルゴリズムを使用しているため、対応しているファイル形式の数が桁違いに多く(準拠しているアルゴリズムの仕様書)、インターフェースもシンプルでいい感じです。

例えば、(MP3、Ogg Vorbis、WAVEのいずれかである)任意のio.ReaderをEbitengineのaudioパッケージでデコードする関数はこう書けます。

import (
    "errors"
    "io"

    "github.com/hajimehoshi/ebiten/v2/audio/mp3"
    "github.com/hajimehoshi/ebiten/v2/audio/vorbis"
    "github.com/hajimehoshi/ebiten/v2/audio/wav"
)

// エラーを定義しておくのもいいかも
var UnsupportedAudioFormatError = errors.New("unsupported audio format")

// 音声ファイルのファイル形式を判別してデコードする関数
func Decode(r io.Reader) (s io.Reader, ty string, err error) {
    // 最初の512バイトを読み込む
    buf := make([]byte, 512)
    if _, err := io.ReadAtLeast(r, buf, 512); err != nil {
        return
    }
    
    // 判別して対応するデコーダでデコード
    ty = http.DetectContentType(buf)
    switch ty {
    case "audio/mp3":
        s, err = mp3.DecodeWithoutResampling(r)
    case "audio/ogg":
        fallthrough
    case "audio/vorbis":
        s, err = vorbis.DecodeWithoutResampling(r)
    case "audio/wav":
        s, err = wav.DecodeWithoutResampling(r)
    default:
        err = UnsupportedAudioFormatError
    }
    return
}

ソースコードはこれです。結構短いですね。インターフェースを使ったダックタイピングをしていて、Goの良さがよくわかります。

https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/net/http/sniff.go;l=21

ちなみに

標準ライブラリのimageパッケージはinit関数がパッケージの初期化時に呼ばれる仕組みを使って、個別のファイル形式用のパッケージを_インポートするとimage.Decodeで統一的にデコードできるAPIになっています。

import (
    "image"
    _ "image/png"
    _ "image/jpeg"
    "io"
)

// なんやかんやあって……
img, err := image.Decode(r)  // PNGでもJPEGでもこれを使う

imageパッケージではファイル形式の判別を自前で行っていて、net/httpへの依存はありませんでした。

https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/image/format.go

各フォーマットごとにinit時にRegisterFormatを呼び出してもらい、フォーマットごとに保存したマジックナンバーをmatchという関数で検査する仕組みになっています。

おわり

おわりです。

何種類かの決まった形式のみを扱うのであれば自分で書いてもいいかもしれませんが、せっかく標準ライブラリにあるので、特に理由がなければこれを使えばいいんじゃないかなぁと思いました。

Discussion