何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Pythonでバイナリ列から画像の種類を判別する

拡張子を信じたい人生であった。

画像ファイルがネットワークを介して飛んでくる。 拡張子を信じて保存をしたいが、拡張子だけ書き換えたり中身が改ざんされているかもしれない。 画像フォーマットごとにあるヘッダを読んで画像を識別したい。

表題には「Pythonで」と書いたが、本質的にバイナリ列と正規表現が扱えるプログラミング言語ならば大差は無いと思われる。 意外とPythonで書かれた記事が見当たらないので一応まとめておこうという気になって書いたのがこのエントリ公開の動機である。

基本方針

大抵の画像フォーマットには仕様が定められている。 つまり、何らかの決まったヘッダーが存在する。 そのヘッダー情報を正規表現で読み取って判別しよう、というのが基本方針である。

JPEG

JPEGは離散コサイン変換を利用した非可逆圧縮の画像フォーマットである。 ISO/IEC 10918-1で仕様が定められ、JISでもJIS X 4301で定まっている。 仕様を読み解くと*1、画像の始まりを示すマーカー(SOI)として\xff\xd8が定義されている。 よって、JPEGがどうかを判定するには次のようにすればよい。

import re
import typing


def is_jpg(b: bytes) -> bool:
    """バイナリの先頭部分からJPEGファイルかどうかを判定する。"""
    return bool(re.match(b"^\xff\xd8", b[:2]))

PNG

PNG可逆圧縮の画像フォーマットである。 ISO/IEC 15948で仕様が定められ、PNGファイルの最初の8バイトは\x89\x50\x4e\x47\x0d\x0a\x1a\x0aであると定められている*2。 よって、PNGがどうかを判定するには次のようにすればよい。

import re
import typing


def is_png(b: bytes) -> bool:
    """バイナリの先頭部分からPNGファイルかどうかを判定する。"""
    return bool(re.match(b"^\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", b[:8]))

GIF

GIFは可逆圧縮の画像フォーマットである。 圧縮アルゴリズムのライセンスをめぐるゴタゴタがあったそうであるが私はその歴史について全く知らない。 私にとってGIFは画像ファイルというよりアニメーションのためのフォーマットである。

https://www.w3.org/Graphics/GIF/spec-gif89a.txtにあるGIFの仕様を眺めると、ヘッダーとして最初はGIF、つまり\x47\x49\x46となり、続いてバージョン番号である87a89aが続く。 よって、GIF8がヘッダにあるかどうかを調べればよい*3

import re
import typing


def is_gif(b: bytes) -> bool:
    """バイナリの先頭部分からGIFファイルかどうかを判定する。"""
    return bool(re.match(b"^\x47\x49\x46\x38", b[:4]))

PDF

PDFはAdobe社が開発したドキュメントフォーマットであるが、画像として送り付けられる場合もある。 ヘッダは%PDF-なので、それを調べればよい*4

import re
import typing


def is_pdf(b: bytes) -> bool:
    """バイナリの先頭部分からPDFファイルかどうかを判定する。"""
    return bool(re.match(b"^%PDF", b[:4]))

Windows Bitmap

Windows BitmapはIBMMicrosoftが開発した画像フォーマットである。 ここ最近、見かけた覚えがないがネットワークの向こう側にいる人間にとっては身近な画像フォーマットかもしれないのだ。 冒頭がBMつまり\x42\x4dから始まるのでそれをチェックすればよい。

import re
import typing


def is_bmp(b: bytes) -> bool:
    """バイナリの先頭部分からWindows Bitmapファイルかどうかを判定する。"""
    return bool(re.match(b"^\x42\x4d", b[:2]))

拡張子を信じることができる世界ならば

今までの前提の否定、つまり拡張子と実際のデータが一致している場合は既存のライブラリで間に合う。 それはmimetypesモジュールである。

19.5. mimetypes — ファイル名を MIME 型へマップする — Python 3.6.1 ドキュメント

最後に

よくあるフォーマットをまとめて、バイナリを入れるといい感じにContent-Typeを返してくれるライブラリが欲しいなあとか思ったり思わなかったりする。

*1:実際は「jpeg ヘッダ」で検索したのち、仕様を調べてで裏取りを行った。

*2:最初はWikipediaで知った

*3:これも最初に知ったのはWikipedia

*4:Vimで適当なPDFファイルを開いてバイナリを調べたのち、仕様を調べた。

『モンティ・パイソンができるまで ジョン・クリーズ自伝』を読んだ。

モンティ・パイソンができるまで ジョン・クリーズ自伝』を読んだ。

www.kinokuniya.co.jp

ジョン・クリーズはイギリスの著名なコメディアンでモンティ・パイソンのメンバーの一人。 幼少期からモンティ・パイソンにおける最初の撮影(空飛ぶ羊のスケッチ)までの訳者曰く「四半生記」。 クリーズの多才さが存分にわかる。

原著のオーディオブックも販売されている。 これは盟友グレアム・チャップマンに倣ったのであろうか。 死後にアニメ化されるかもしれない。

Amazon.co.jp: So, Anyway...: The Autobiography: John Cleese: 洋書

グレアム・チャップマンの自伝

A Liar's Autobiography : Graham Chapman, David Sherlock, Alex Martin, David Yallop, Douglas Adams : 洋書 : Amazon.co.jp

グレアム・チャップマンの自伝を元にしたアニメーション Amazon CAPTCHA

Pythonのソートキーに何を渡すべきか

Pythonのリストの中身をソートしたい場合、list.sort()sorted()を使う。 その際に何をキーとしてソートをするかをkey引数として渡すことができる。 よくある状況として、辞書を含むリストが存在し、辞書の特定のキーの値でソートしたい、という状況である。

import datetime
import random
import string

people = [
    {
        "name": "".join([random.choice(string.ascii_letters) for i in range(10)]),
        "birthday": datetime.date.fromordinal(random.randint(1, datetime.date.today().toordinal()))
     }
    for j in range(10**6)
]

今までは無名函数lambdaでキーを指定していた。

sorted(people, key=lambda x: x.get("birthday"))

最近、ソート HOW TO — Python 3.6.1 ドキュメントを読んでoperatorモジュールの函数を使うと高速にできるという情報を知った。

import operator

sorted(people, key=operator.itemgetter("birthday"))

長い物には巻かれろ、ということでソートHOW TOの記述を信じてソートキーにはoperatorモジュールの函数を使おう、と結論付けてもよいが、せっかくなので測定してみよう。 測定方法もtime.time()の差分ではなくtimeitを使ってみる。

key_lambda = timeit.timeit('sorted(people, key=lambda x: x.get("birthday"))', number=10, globals=globals())
key_operator = timeit.timeit('sorted(people, key=operator.itemgetter("birthday"))', number=10, globals=globals())

print("Lambda", key_lambda)
print("operator", key_operator)
Lambda 13.513277642467928
operator 11.774310869161361

確かにoperatorモジュールの函数の方が速い。 そういうことで、ソートキーにはoperatorモジュールを使おう。