1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】POSTされたmultipart/form-dataをmultipartモジュールでパースする

Posted at

記載の経緯

multipart/form-dataでPOSTされたデータをPythonでパースする方法として、Python標準モジュールのcgi.FieldStorageでパースする記事をいくつか見かけましたが、cgiモジュールは Python3.11で非推奨、Python3.13で削除されました

FieldStorageの代替方法として、公式ドキュメントにてmultipartモジュールが上げられており、multipartを使った記事があまりなかったため、記載します。

Deprecated since version 3.11, will be removed in version 3.13:
The cgi module is deprecated (see PEP 594 for details and alternatives).

The FieldStorage class can typically be replaced with urllib.parse.parse_qsl() for GET and HEAD requests, and the email.message module or multipart for POST and PUT. Most utility functions have replacements.

※公式ドキュメントから引用:https://docs.python.org/ja/3.12/library/cgi.html

multipartモジュール

確認したバージョンは、記載時最新のv1.1.0です。

ライセンス MIT License
依存するモジュール なし
Python バージョン 3.5以上

参考までに、snyk Adviserの結果は 82点 でした(2024/11/3時点)。
https://snyk.io/advisor/python/multipart

multipartモジュールは、multipart/form-dataでPOSTされたボディを直にパースする関数に加えて、WSGIアプリケーション向けの高レベルのヘルパー関数も用意されています。

本記事で紹介するのは前者のmultipart/form-dataのボディを直にパースする関数のサンプルです。
後者のWSGI向けのサンプルはモジュールのreadmeに記載があるのでそちらを参照ください。

サンプルコード

import io
from multipart import MultipartParser

multipart_body_data = """\
----------------------------892683388101143002038604
Content-Disposition: form-data; name="jsonfile"; filename="test.json"
Content-Type: application/json

{
    "testStr": "testValue",
    "testInt": 1
}
----------------------------892683388101143002038604
Content-Disposition: form-data; name="test_text"

This is test.
----------------------------892683388101143002038604--
""".replace("\n", "\r\n").encode("utf-8")


def parse_multipart(body):
    stream = io.BytesIO(body)
    boundary = "--------------------------892683388101143002038604"

    # MultipartParserクラスにパースした各データを返却する__iter__が実装されているので、ループで回せる。
    for part in MultipartParser(stream, boundary):
        print(
            {
                "size": part.size,
                "name": part.name,
                "filename": part.filename,
                "headerlist": part.headerlist,
                "payload": part.value,  # 文字デコードされたデータ
                "raw": part.raw,  # 生データ
            }
        )


parse_multipart(multipart_body_data)

※実行環境:python3.11.9

実行結果

{'size': 51, 'name': 'jsonfile', 'filename': 'test.json', 'headerlist': [('Content-Disposition', 'form-data; name="jsonfile"; filename="test.json"'), ('Content-Type', 'application/json')], 'payload': '{\r\n    "testStr": "testValue",\r\n    "testInt": 1\r\n}', 'raw': b'{\r\n    "testStr": "testValue",\r\n    "testInt": 1\r\n}'}
{'size': 13, 'name': 'test_text', 'filename': None, 'headerlist': [('Content-Disposition', 'form-data; name="test_text"')], 'payload': 'This is test.', 'raw': b'This is test.'}

解説

  • multipart_body_data
    multipart/form-dataで送信されたボディデータです。サンプルのためコード上に記載しています。
    実際のボディデータの改行コードはCRLFのため改行コードを変換しています。

  • parse_multipart()

    • MultipartParserクラスを使用してパースを行います。コンストラクタの引数にボディデータのストリーム1とバウンダリを指定します。
    • MultipartParserクラスにはパースした各データを返却する__iter__が実装されているので、for文で各要素をループすることができます。
    • パースされると、multipart/form-dataの各要素がMultipartPartクラスとして生成されます。メンバ変数にアクセスして各情報を取り出すことができます。

サンプルコード(APIGateway + Lambda)

参考までに、AWSのAPIGateway + Lambda(プロキシ統合)の構成で、multipart/form-dataでPOSTされたファイルをS3にアップロードするコードも掲載しておきます。

import base64
import io
import boto3
from multipart import MultipartParser


s3 = boto3.client("s3")
bucket_name = "bucket_name"


def lambda_handler(event, context):
    # リクエストボディがbase64エンコードされてるのでデコード(生のボディを受け取る場合は不要)
    body = base64.b64decode(event["body"])

    stream = io.BytesIO(body)
    boundary = event["headers"]["Content-Type"].split("boundary=")[1]

    # MultipartParserクラスにパースした各データを返却する__iter__が実装されているので、ループで回せる。
    for part in MultipartParser(stream, boundary):
        print(
            {
                "size": part.size,
                "name": part.name,
                "filename": part.filename,
                "headerlist": part.headerlist,
            }
        )
        if part.filename:
            # ファイルはS3アップロード(Content-DispositionヘッダーにfileNameがあるデータをアップロード)
            s3.upload_fileobj(part.file, bucket_name, part.filename)
        else:
            # part.valueによって文字デコードされたボディが返される(生データはpart.raw)
            print(f"This is not file-like data. Payload:{part.value}")

    ret = {
        "isBase64Encoded": "false",
        "statusCode": 200,
        "headers": {"test-header-result": "ok"},
        "body": "Files upload to s3 is complete.",
    }

    return ret

APIGatewayを用いた場合にLambdaに入力されるデータの構造は以下。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html

参考

  1. .read(size)が実装されていればよい
    https://github.com/defnull/multipart/blob/2c19b3961d180f22b5c9cfec5c51e260af92e515/multipart.py#L578

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?