個人用の備忘録ですが、解決したのでご参考に。
目次
事象
Djangoでアップロードされた画像ファイルに対して、フォーマットの種類を調べようとimfhdr.what()
を実行したら、None
が返り取得できなかった。
うまくいかなかったコードは以下のようなもの。
from django.views.generic import View
import imghdr,hashlib
class XXXView(View):
def post(self, request, *args, **kwargs):
# リクエストフォームから画像ファイルを取得
image_file_dict = self.request.FILES.dict()
# ハッシュを計算
image_file_hash = hashlib.sha256(image_file_dict['image_file'].file.read()).hexdigest()
# 画像ファイルのフォーマットを取得
format_type = imghdr.what(image_file_dict['image_file'])
print(format_type) # Noneが返る
image_file
はDjangoのFormで決めたフィールド名です。
原因
公式のimghdrのページには全然情報がないため、いろいろいじっていたら、、、imghdr.what()
は指定されたファイルに対して、tell()
をしているようだ。
File "C:\ProgramData\Anaconda3\Lib\imghdr.py", line 19, in what
location = file.tell()
AttributeError: 'bytes' object has no attribute 'tell'
このtell()
は現在のストリーム位置を返すもの(詳しくはこちら)であるようなので、指定された画像ファイルのストリーム位置がおかしいとフォーマットを検出できないのではないのだろうか。
で、もう一度コードをみると、imghdr.what()
する前にハッシュ計算で同じ参照元のイメージファイルに対して、file.read()
をしている。
このfile.read()
だが、公式ドキュメントを見ると以下のように記載されていた。
read
(size=-1)最大で size バイト読み込んで返します。 引数が省略されるか、
https://docs.python.org/ja/3/library/io.html#io.BufferedIOBase.readNone
か、または負の値であった場合、 データは EOF に到達するまで読み込まれます。 ストリームが既に EOF に到達していた場合は空のbytes
オブジェクトが返されます。
これにより、ストリーム位置がEOFまで移動しているのが原因ではないのだろうか?
試しに、以下コードを実行してみた。
from django.views.generic import View
import imghdr,hashlib
class XXXView(View):
def post(self, request, *args, **kwargs):
# リクエストフォームから画像ファイルを取得
image_file_dict = self.request.FILES.dict()
# ハッシュ計算前の状態
print("file.read()実行前のフォーマット:{}".format(imghdr.what(image_file_dict['fk_image_file'])))
print("file.read()実行前のストリーム位置:{}".format(image_file_dict['image_file'].tell()))
# ハッシュを計算
image_file_hash = hashlib.sha256(image_file_dict['image_file'].file.read()).hexdigest()
# ハッシュ計算後の状態
print("file.read()実行後のフォーマット:{}".format(imghdr.what(image_file_dict['fk_image_file'])))
print("file.read()実行後のストリーム位置:{}".format(image_file_dict['image_file'].tell()))
print("ファイルのサイズ:{}".format(image_file_dict['image_file'].size))
【実行結果】
file.read()実行前のフォーマット:png
file.read()実行前のストリーム位置:0
file.read()実行後のフォーマット:None
file.read()実行後のストリーム位置:749207
ファイルのサイズ:749207
- ハッシュを計算するために、
file.read()
をしているが、その実行前後でimghdr.what()
によるフォーマット取得が失敗していることが分かる。 - また、
file.read()
の実行前後で、ストリームの位置がファイルの最後に移動しているのが分かる。
上で記載した通り、imghdr.what()
内部ではfile.tell()
を使ってストリーム位置を取得しており、本来はファイルの先頭にあるべきが、ファイルの末端にストリーム位置が移動しているため、そこで不具合が発生したようだ。
修正案
バイトストリームには、公式ドキュメント(詳しくはこちら)を見ると他にもメソッドがある。画像を送信するデバイスによってどのクラスで送られてくるか変わってくるようなので、基底クラス「io.IOBase
」で定義されたメソッドを使うことにする。
※自分の環境では画像ファイルをPCから送ると「io.BytesIO
」、スマホから送ると「io.BufferedRandom
」と異なるクラスだった。
ストリームの位置がずれているのが原因だったため、基底クラス「io.IOBase
」のメソッドのなかで、ストリームの位置を変更できるseek()
を使うことにした。
seek
(offset, whence=SEEK_SET)ストリーム位置を指定された offset バイトに変更します。offset は whence で指定された位置からの相対位置として解釈されます。 whence のデフォルト値は
https://docs.python.org/ja/3/library/io.html#io.IOBase.seekSEEK_SET
です。 whence に指定できる値は:
・SEEK_SET
または0
-- ストリームの先頭 (デフォルト)。 offset は 0 もしくは正の値でなければなりません。
・SEEK_CUR
または1
-- 現在のストリーム位置。 offset は負の値も可能です。
・SEEK_END
または2
-- ストリームの末尾。 offset は通常負の値です。
新しい絶対位置を返します。
上で試しに実行したコードに対して、image_file_dict['image_file'].seek(0)
をfile.read()
の後に追加することで、以下のような結果となった。
file.read()実行前のフォーマット:png
file.read()実行前のストリーム位置:0
file.read()実行後のフォーマット:png
file.read()実行後のストリーム位置:0
ファイルのサイズ:749207
- フォーマットも取得できているし、ストリーム位置もファイルの先頭から変わっていない。
修正したコードは以下のようになった。
from django.views.generic import View
import imghdr,hashlib
class XXXView(View):
def post(self, request, *args, **kwargs):
# リクエストフォームから画像ファイルを取得
image_file_dict = self.request.FILES.dict()
# ハッシュを計算
image_file_hash = hashlib.sha256(image_file_dict['image_file'].file.read()).hexdigest()
# ストリーム位置を先頭に戻す ★これを追加★
image_file_dict['image_file'].seek(0)
# 画像ファイルのフォーマットを取得
format_type = imghdr.what(image_file_dict['image_file'])
print(format_type) # フォーマットが返った
関連情報
ConoHa上でDockerを導入したり、Webアプリを立ち上げたりしました。 以下の記事でまとめたので、よろしければご覧ください。
宣伝
Djangoをやるなら以下の書籍がオススメです。
DjangoでWebアプリを開発するときの要点が分かりやすくまとめられています。
ConoHa VPS
は初期費用不要で月に数百円で利用できる仮想サーバのサービスです。以下のような方には非常にオススメのサービスとなっています!
- 勉強がてらLinuxの環境をちょっと触ってみたい
⇒管理者権限が実行可能なLinuxサーバ環境が構築可能です! - スモールスタートでサービスを提供して、うまくいったら規模をスケールアップしたい
⇒後からメモリサイズやCPU数などのスケールアップ/スケールダウン可能です! - AWSやGCPなどのクラウドサービスは高いので、もっと安くサーバ構築したい
⇒初期費用なし、月数百円(1時間単位も可)で利用可能です!
以上!
コメント