Goの標準ライブラリでファイル形式を判別する
リハビリを兼ねて小ネタをば。
何らかのプログラムを書いているときに「ファイル形式によってデコード方法を変えたい」など、ファイル形式の判別が必要な場合があります。ファイル形式は拡張子やファイルの先頭にある固有のバイト列を調べるなどすれば判別可能です。
でも世の中にいくつもあるファイル形式それぞれのヘッダに対応すべく、都度仕様を調べて実装するのはちょっとめんどくさい作業ですよね。かといってそれだけのためにサードパーティライブラリに依存するのも……となりがちです。
http.DetectContentType
そんなときこそnet/http
のDetectContentType
関数を使うときです。
func DetectContentType(data []byte) string
こいつは与えられたバイト列を基に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 ErrUnsupportedAudioFormat = 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 = ErrUnsupportedAudioFormat
}
return
}
ソースコードはこれです。結構短いですね。インターフェースを使ったダックタイピングをしていて、Goの良さがよくわかります。
ちなみに
標準ライブラリのimage
パッケージはinit
関数がパッケージの初期化時に呼ばれる仕組みを使って、個別のファイル形式用のパッケージを_
インポートするとimage.Decode
で統一的にデコードできるAPIになっています。
import (
"image"
_ "image/png"
_ "image/jpeg"
"io"
)
// なんやかんやあって……
img, err := image.Decode(r) // PNGでもJPEGでもこれを使う
image
パッケージではファイル形式の判別を自前で行っていて、net/http
への依存はありませんでした。
各フォーマットごとにinit
時にRegisterFormat
を呼び出してもらい、フォーマットごとに保存したマジックナンバーをmatch
という関数で検査する仕組みになっています。
おわり
おわりです。
何種類かの決まった形式のみを扱うのであれば自分で書いてもいいかもしれませんが、せっかく標準ライブラリにあるので、特に理由がなければこれを使えばいいんじゃないかなぁと思いました。
Discussion