Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

フロントエンドからファイルを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

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

smarthr
社会の非合理を、ハックする。
https://smarthr.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした