43
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Python] POSTされたmultipart/form-dataをFieldStorageでパースする

Last updated at Posted at 2018-09-17

フロントエンドからファイルをmultipart/form-data形式でPOSTするためのWeb APIをサーバーレスで開発していて、データの取り出しに少しハマって調べたので書いておきます。

multipart/form-data

multipart/form-dataは、一回のPOSTリクエストで複数のデータやファイルをサーバーに送信するときに使うリクエストヘッダーです。
それぞれのファイルやデータをboundaryという文字列で区切ってリクエストボディに入れます。boundaryは多くのWebリクエストクライアントで自動的に生成してくれます。

curlを使ってmultipart/form-dataをリクエストする場合は下記のようになります。


curl -X POST http://localhost:3000 \
-F "data=@/path/to/sample.txt;type=text/plain" \
-F "csvdata=@/path/to/HelloWorld.csv;type=text/csv"

リクエストボディはこんな感じになります。

--------------------------5534b722b1776e54
Content-Disposition: form-data; name="data"; filename="sample.txt"
Content-Type: text/plain

This is Sample Text!!

--------------------------5534b722b1776e54
Content-Disposition: form-data; name="csvdata"; filename="HelloWorld.csv"
Content-Type: text/csv

foo,bar,baz
hoge,piyo,moge

--------------------------5534b722b1776e54--

boundaryは--------------------------5534b722b1776e54の部分です。
ボディデータ内のどれがboundaryなのかをサーバーに教えて上げる必要があるのでヘッダーにその情報が入っています。

{'Content-Type': 'multipart/form-data; boundary=------------------------5534b722b1776e54'}

このヘッダー情報を使ってデータをパースしていく処理を自前で実装するのめちゃ大変・・・。

調べたところFieldStorageを使ってねということでした。

FieldStorage

FieldStorageはPython標準ライブラリに含まれるcgiモジュールのクラスです。FieldStorageにリクエストボディとヘッダー情報を引数として渡すと、下記の情報が取得できます。

  • フィールド名(リクエスト時のcurl ... -F data=@hoge.txtdataの部分)
  • ファイル名(例:hoge.txt
  • ファイルタイプ(例:text/plain
  • データ(例:hello world

FieldStorageに渡す引数

fp

fpはfile pointerのことで、ボディ本体を渡します。渡すボディはTextIOWrapperクラスのオブジェクトである必要があるのでio.BytexIO()で読み込みます。今回の場合はリクエストボディデータがBase64エンコードされた状態で送られてくるのでBase64デコード処理も入れています。

headers

content-typecontent-lengthを渡します。リクエストで受け取ったヘッダーはContent-TypeContent-Lengthになっていて、それをそのまま渡すとFieldStorageクラス内で読み込めないため、キーをlower caseにしてあげてます。(ここでめっちゃハマった・・)

environ

これはPOSTメソッドの操作だよと、REQUEST_METHODに入れてあげる必要があります。

コード

最終的にはこのような形になりました。 fs.listをfor文で回して、それぞれのデータを取り出して処理しています。


import base64
import io
from cgi import FieldStorage

def parse_multipart_form(headers, body):
    fp = io.BytesIO(base64.b64decode(body))
    environ = {'REQUEST_METHOD': 'POST'}
    headers = {
        'content-type': headers['Content-Type'],
        'content-length': headers['Content-Length']
    }

    fs = cgi.FieldStorage(fp=fp, environ=environ, headers=headers)

    for f in fs.list:
        print(f.name, f.filename, f.type, f.value)

出力

data sample.txt text/plain b'This is Sample Text!!\n'
csvdata HelloWorld.csv text/csv b'foo,bar,baz\nhoge,piyo,moge\n'

以上です。

今回はFieldStorageをこういう使い方をしている例があまり見つからず、cgiのコードを読み込みました。いい勉強になります。

本当は、せっかくサーバーレスのようなクラウドネイティブなアプリケーションを構築しているのであれば、フロントからのファイルアップロードは、S3に直接アップロードするほうがいろいろと楽ですよね。

最後に、

  • API Gatewayを使うのであればバイナリサポートを有効にすること
  • Lambdaファンクションに渡せるイベントデータの上限は6MB

この部分は原因の発見が難しいので要注意です。

43
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?