👌

【Python】mimetypes.guess_type()は環境依存があって困った

2024/09/04に公開

はじめに

拡張子からcontent-typeを判定したいなということで、Python標準ライブラリであるmimetypes.guess_type()を使用しましたが、ハマった箇所があったので記事に残します。

起きた事象

content_type, _ = mimetypes.guess_type(file_name)
if content_type is None:
    logger.error("Unsupported content_type: %s", file_name)
    raise HTTPException(
        status_code=400, detail="Unsupported content_type")

上記のように、ファイル名からcontent-typeを求め、
不正な拡張子だった場合、400番エラーにする処理を書いていました。

400番エラーになることを確かめるために、
file_name="aaa.abc"にしてテストコードを書きました。

ローカル環境(Mac)ではmimetypes.guess_type(”aaa.abc”)の戻り値はNoneで、テストが通りました。

しかし、CI環境(Linux)ではなぜかテストが通らない、、、
テストログを見ると、mimetypes.guess_type(”aaa.abc”)の戻り値がtext/vnd.abcになっていました。

なぜこのようなことが起きるのか

ローカルとCI環境で同じPythonのバージョン使ってるのに、なんで???
と思ったので調べました。

原因その1

.abcという拡張子は存在する
でたらめな拡張子としてaaa.abcとしたのですが、自分の知識不足でした。
abc拡張子は存在するようです。
以下はchatGPTの回答です。

.abc拡張子は存在します。これは、ABC記法と呼ばれる音楽の譜面を表すためのテキストベースのフォーマットに使われています。ABC記法は、主にフォークソングのメロディーをメールなどで簡単に共有するために1990年代に広まりました。.abcファイルは、テキスト形式で音楽を表現でき、さまざまなソフトウェアで楽譜に変換したり、音声として再生したりすることができます。
また、IANA (Internet Assigned Numbers Authority) は1997年にtext/vnd.abcを公式なMIMEタイプとして登録しました。この形式は特に伝統的な音楽のコミュニティで利用され続けており、簡単な音楽の共有手段として今でも使われています。

.abcなんて拡張子ないやろという考えが浅はかだったと反省です。

しかし、まだ疑問が残ります。
なぜローカル環境ではNoneになったのに、CI環境ではtext/vnd.abcになるのか

原因その2

MIMEタイプデータベース[1]の内容は環境に依存する

mimetypes.types_mapで拡張子とMIMEタイプの一覧を出してみました。

import mimetypes
import pprint
print(len(mimetypes.types_map))
pprint(mimetypes.types_map)
ローカル環境(Mac)結果

130
{'.a': 'application/octet-stream',
'.ai': 'application/postscript',
'.aif': 'audio/x-aiff',
'.aifc': 'audio/x-aiff',
'.aiff': 'audio/x-aiff',
'.au': 'audio/basic',
'.avi': 'video/x-msvideo',
'.bat': 'text/plain',
'.bcpio': 'application/x-bcpio',
'.bin': 'application/octet-stream',
'.bmp': 'image/x-ms-bmp',
'.c': 'text/plain',
'.cdf': 'application/x-netcdf',
'.cpio': 'application/x-cpio',
'.csh': 'application/x-csh',
'.css': 'text/css',
'.csv': 'text/csv',
'.dll': 'application/octet-stream',
'.doc': 'application/msword',
'.dot': 'application/msword',
'.dvi': 'application/x-dvi',
'.eml': 'message/rfc822',
'.eps': 'application/postscript',
'.etx': 'text/x-setext',
'.exe': 'application/octet-stream',
'.gif': 'image/gif',
'.gtar': 'application/x-gtar',
'.h': 'text/plain',
'.hdf': 'application/x-hdf',
'.htm': 'text/html',
'.html': 'text/html',
'.ico': 'image/vnd.microsoft.icon',
'.ief': 'image/ief',
'.jpe': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.js': 'application/javascript',
'.json': 'application/json',
'.ksh': 'text/plain',
'.latex': 'application/x-latex',
'.m1v': 'video/mpeg',
'.m3u': 'application/vnd.apple.mpegurl',
'.m3u8': 'application/vnd.apple.mpegurl',
'.man': 'application/x-troff-man',
'.me': 'application/x-troff-me',
'.mht': 'message/rfc822',
'.mhtml': 'message/rfc822',
'.mif': 'application/x-mif',
'.mjs': 'application/javascript',
'.mov': 'video/quicktime',
'.movie': 'video/x-sgi-movie',
'.mp2': 'audio/mpeg',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.mpa': 'video/mpeg',
'.mpe': 'video/mpeg',
'.mpeg': 'video/mpeg',
'.mpg': 'video/mpeg',
'.ms': 'application/x-troff-ms',
'.nc': 'application/x-netcdf',
'.nws': 'message/rfc822',
'.o': 'application/octet-stream',
'.obj': 'application/octet-stream',
'.oda': 'application/oda',
'.p12': 'application/x-pkcs12',
'.p7c': 'application/pkcs7-mime',
'.pbm': 'image/x-portable-bitmap',
'.pdf': 'application/pdf',
'.pfx': 'application/x-pkcs12',
'.pgm': 'image/x-portable-graymap',
'.pl': 'text/plain',
'.png': 'image/png',
'.pnm': 'image/x-portable-anymap',
'.pot': 'application/vnd.ms-powerpoint',
'.ppa': 'application/vnd.ms-powerpoint',
'.ppm': 'image/x-portable-pixmap',
'.pps': 'application/vnd.ms-powerpoint',
'.ppt': 'application/vnd.ms-powerpoint',
'.ps': 'application/postscript',
'.pwz': 'application/vnd.ms-powerpoint',
'.py': 'text/x-python',
'.pyc': 'application/x-python-code',
'.pyo': 'application/x-python-code',
'.qt': 'video/quicktime',
'.ra': 'audio/x-pn-realaudio',
'.ram': 'application/x-pn-realaudio',
'.ras': 'image/x-cmu-raster',
'.rdf': 'application/xml',
'.rgb': 'image/x-rgb',
'.roff': 'application/x-troff',
'.rtx': 'text/richtext',
'.sgm': 'text/x-sgml',
'.sgml': 'text/x-sgml',
'.sh': 'application/x-sh',
'.shar': 'application/x-shar',
'.snd': 'audio/basic',
'.so': 'application/octet-stream',
'.src': 'application/x-wais-source',
'.sv4cpio': 'application/x-sv4cpio',
'.sv4crc': 'application/x-sv4crc',
'.svg': 'image/svg+xml',
'.swf': 'application/x-shockwave-flash',
'.t': 'application/x-troff',
'.tar': 'application/x-tar',
'.tcl': 'application/x-tcl',
'.tex': 'application/x-tex',
'.texi': 'application/x-texinfo',
'.texinfo': 'application/x-texinfo',
'.tif': 'image/tiff',
'.tiff': 'image/tiff',
'.tr': 'application/x-troff',
'.tsv': 'text/tab-separated-values',
'.txt': 'text/plain',
'.ustar': 'application/x-ustar',
'.vcf': 'text/x-vcard',
'.wasm': 'application/wasm',
'.wav': 'audio/x-wav',
'.webm': 'video/webm',
'.webmanifest': 'application/manifest+json',
'.wiz': 'application/msword',
'.wsdl': 'application/xml',
'.xbm': 'image/x-xbitmap',
'.xlb': 'application/vnd.ms-excel',
'.xls': 'application/vnd.ms-excel',
'.xml': 'text/xml',
'.xpdl': 'application/xml',
'.xpm': 'image/x-xpixmap',
'.xsl': 'application/xml',
'.xwd': 'image/x-xwindowdump',
'.zip': 'application/zip'}

dockerで構築したUbuntu環境

149
{'.3g2': 'audio/3gpp2',
'.3gp': 'audio/3gpp',
'.3gpp': 'audio/3gpp',
'.3gpp2': 'audio/3gpp2',
'.a': 'application/octet-stream',
'.aac': 'audio/aac',
'.adts': 'audio/aac',
'.ai': 'application/postscript',
'.aif': 'audio/x-aiff',
'.aifc': 'audio/x-aiff',
'.aiff': 'audio/x-aiff',
'.ass': 'audio/aac',
'.au': 'audio/basic',
'.avi': 'video/x-msvideo',
'.avif': 'image/avif',
'.bat': 'text/plain',
'.bcpio': 'application/x-bcpio',
'.bin': 'application/octet-stream',
'.bmp': 'image/bmp',
'.c': 'text/plain',
'.cdf': 'application/x-netcdf',
'.cpio': 'application/x-cpio',
'.csh': 'application/x-csh',
'.css': 'text/css',
'.csv': 'text/csv',
'.dll': 'application/octet-stream',
'.doc': 'application/msword',
'.dot': 'application/msword',
'.dvi': 'application/x-dvi',
'.eml': 'message/rfc822',
'.eps': 'application/postscript',
'.etx': 'text/x-setext',
'.exe': 'application/octet-stream',
'.gif': 'image/gif',
'.gtar': 'application/x-gtar',
'.h': 'text/plain',
'.h5': 'application/x-hdf5',
'.hdf': 'application/x-hdf',
'.heic': 'image/heic',
'.heif': 'image/heif',
'.htm': 'text/html',
'.html': 'text/html',
'.ico': 'image/vnd.microsoft.icon',
'.ief': 'image/ief',
'.jpe': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.js': 'application/javascript',
'.json': 'application/json',
'.ksh': 'text/plain',
'.latex': 'application/x-latex',
'.loas': 'audio/aac',
'.m1v': 'video/mpeg',
'.m3u': 'application/vnd.apple.mpegurl',
'.m3u8': 'application/vnd.apple.mpegurl',
'.man': 'application/x-troff-man',
'.me': 'application/x-troff-me',
'.mht': 'message/rfc822',
'.mhtml': 'message/rfc822',
'.mif': 'application/x-mif',
'.mjs': 'application/javascript',
'.mov': 'video/quicktime',
'.movie': 'video/x-sgi-movie',
'.mp2': 'audio/mpeg',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.mpa': 'video/mpeg',
'.mpe': 'video/mpeg',
'.mpeg': 'video/mpeg',
'.mpg': 'video/mpeg',
'.ms': 'application/x-troff-ms',
'.n3': 'text/n3',
'.nc': 'application/x-netcdf',
'.nq': 'application/n-quads',
'.nt': 'application/n-triples',
'.nws': 'message/rfc822',
'.o': 'application/octet-stream',
'.obj': 'application/octet-stream',
'.oda': 'application/oda',
'.opus': 'audio/opus',
'.p12': 'application/x-pkcs12',
'.p7c': 'application/pkcs7-mime',
'.pbm': 'image/x-portable-bitmap',
'.pdf': 'application/pdf',
'.pfx': 'application/x-pkcs12',
'.pgm': 'image/x-portable-graymap',
'.pl': 'text/plain',
'.png': 'image/png',
'.pnm': 'image/x-portable-anymap',
'.pot': 'application/vnd.ms-powerpoint',
'.ppa': 'application/vnd.ms-powerpoint',
'.ppm': 'image/x-portable-pixmap',
'.pps': 'application/vnd.ms-powerpoint',
'.ppt': 'application/vnd.ms-powerpoint',
'.ps': 'application/postscript',
'.pwz': 'application/vnd.ms-powerpoint',
'.py': 'text/x-python',
'.pyc': 'application/x-python-code',
'.pyo': 'application/x-python-code',
'.qt': 'video/quicktime',
'.ra': 'audio/x-pn-realaudio',
'.ram': 'application/x-pn-realaudio',
'.ras': 'image/x-cmu-raster',
'.rdf': 'application/xml',
'.rgb': 'image/x-rgb',
'.roff': 'application/x-troff',
'.rtx': 'text/richtext',
'.sgm': 'text/x-sgml',
'.sgml': 'text/x-sgml',
'.sh': 'application/x-sh',
'.shar': 'application/x-shar',
'.snd': 'audio/basic',
'.so': 'application/octet-stream',
'.src': 'application/x-wais-source',
'.srt': 'text/plain',
'.sv4cpio': 'application/x-sv4cpio',
'.sv4crc': 'application/x-sv4crc',
'.svg': 'image/svg+xml',
'.swf': 'application/x-shockwave-flash',
'.t': 'application/x-troff',
'.tar': 'application/x-tar',
'.tcl': 'application/x-tcl',
'.tex': 'application/x-tex',
'.texi': 'application/x-texinfo',
'.texinfo': 'application/x-texinfo',
'.tif': 'image/tiff',
'.tiff': 'image/tiff',
'.tr': 'application/x-troff',
'.trig': 'application/trig',
'.tsv': 'text/tab-separated-values',
'.txt': 'text/plain',
'.ustar': 'application/x-ustar',
'.vcf': 'text/x-vcard',
'.vtt': 'text/vtt',
'.wasm': 'application/wasm',
'.wav': 'audio/x-wav',
'.webm': 'video/webm',
'.webmanifest': 'application/manifest+json',
'.wiz': 'application/msword',
'.wsdl': 'application/xml',
'.xbm': 'image/x-xbitmap',
'.xlb': 'application/vnd.ms-excel',
'.xls': 'application/vnd.ms-excel',
'.xml': 'text/xml',
'.xpdl': 'application/xml',
'.xpm': 'image/x-xpixmap',
'.xsl': 'application/xml',
'.xwd': 'image/x-xwindowdump',
'.zip': 'application/zip'}

このようにローカル環境(Mac)では130種類、Dockerで構築したUbuntu環境では149種類のMIMEタイプの一覧が存在することがわかりました。

これらのことから、mimetypes.guess_type()が返す結果は、システムに依存していることがわかります。

もう少し深掘ります。
mimetypes.pyの中身を読んでみると、
MIME タイプを読み込むファイルのパスを指定しているknownfilesが見つかりました。
knownfilesには、システム上の標準的なMIMEタイプ定義ファイルが列挙されているようです。

knownfiles = [
    "/etc/mime.types",
    "/etc/httpd/mime.types",  # Mac OS X
    "/etc/httpd/conf/mime.types",  # Apache
    "/etc/apache/mime.types",  # Apache 1
    "/etc/apache2/mime.types",  # Apache 2
    "/usr/local/etc/httpd/conf/mime.types",  # Apache 1.2
    "/usr/local/etc/mime.types",  # Apache 1.3
]

......
中略
......

if files is None:
    files = knownfiles
else:
    files = knownfiles + list(files)

for file in files:
    if os.path.isfile(file):
        db.read(file)

mimetypesモジュールは、各環境にある特定のパスに配置されたmime.typesファイルを参照してMIMEタイプデータベースを作成しているようです。

まとめ

システム設定ファイルを参照して、MIMEタイプデータベースを作成しているため、mimetypes.guess_type() の結果が、ローカル環境(Mac)と CI 環境(Linux)で異なる現象が発生していたとわかりました。

脚注
  1. 拡張子とMIMEタイプの一覧をMIMEタイプデータベースと呼んでいます。 ↩︎

NCDCエンジニアブログ

Discussion