7839

雑草魂エンジニアブログ

【Python】API を用いてファイルを分割アップロードする

PythonDjango)のアプリケーションを開発している中で、クラウドストレージに、写真 や Excel 、PDF などのデータを API 経由でアップロードを行ったので、備忘録として残しておく。

今回データをアップロードする API では、バイナリーデータに 512KB の制限があったので、分割してアップロードを行えるように実装を行った。また、今回使用したpythonのバージョンは以下の通りである。

Python 3.9.0 (default, Oct 22 2020, 05:03:39) 

APIリクエスト〜urllibライブラリ

docs.python.org

urllib ライブラリは「URLを扱うモジュールを集めたパッケージ」である。

  • urllib.request:HTTPリクエストを行う
  • urllib.error:HTTPリクエストのエラーハンドリングを行う
  • urllib.parse:URLに日本語が含まれていた場合に、URLエンコード/デコードを行う
  • urllib.robotparser:robots.txt ファイルをパースする

今回も、上記の 3 つのライブラリを用いて、API リクエスト(POST)を行う場合は以下のように実装できる。

import urllib.request
import urllib.parse

def post_api(Blob_data, upload_path):
    url = f"http://api.com/{urllib.parse.quote(upload_path)}"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/octet-stream",
    }
    data = Blob_data
    req = urllib.request.Request(
        url,
        data,
        headers,
    )
    try:
        with urllib.request.urlopen(req) as res:
            body = res.read()
            return body
    except urllib.error.URLError as e:
        print(e)

URLに、クラウドストレージのフォルダパスを含める必要があり、「フォルダパス」に日本語が含まれることがあるため、urllib.parse.quote でURLエンコードを行っている。

例)テスト → %E3%83%86%E3%82%B9%E3%83%88

URLエンコードに関しては、以下の2つのメソッドがある。(違いは、空文字の取り扱いと、変換しない除外文字(safe)のデフォルト指定である。)

1. urllib.parse.quote(string, safe='/', encoding='UTF-8')

  • 空文字(スペース): %20 に変換する
  • 変換しない文字(safe):デフォルトが / となっている

2. urllib.parse.quote_plus(string, safe='', encoding='UTF-8')

  • 空文字(スペース): + に変換する
  • 変換しない文字(safe):デフォルトは設定されていない

実際に、HTTPリクエストは、 urllib.request.urlopen() メソッドで行われ、引数に Request オブジェクト urllib.request.Request を設定する。(urlopen の引数は、Request オブジェクトではなく、直接 url を指定することで単純なリクエストであれば可能である。ただし、headerなどの詳細な設定が必要な場合は、Request オブジェクトを指定する必要がある。)また、urlopen の返り値は HTTPResponse オブジェクト なので、res.read() メソッドを使うことでレスポンスのボディの中身を確認したり、res. headers() でヘッダー情報を確認することができる。

エラーハンドリングに関しては、二種類の例外を投げることができる。

1. urllib.error.URLError :HTTP通信に失敗した場合

2. urllib.error.HTTPError :HTTP ステータスコードが4xxまたは5xxだった場合

ファイル分割

Pythonでファイルの読み込みをする場合は、 open() を用いる。引数としては、読み込みたいファイルのパスとモードを設定する。モードは以下の通りである。

  • r:読み込み用(デフォルト)
  • w:書き込み用
  • x :新規作成用

末尾に、「b」をつけることでバイナリファイルとして取り扱うことができる
例)「rb」: バイナリファイル読み込み

以下が、ファイルをバイナリファイルとして読み込むコードである。

with open(file_path, "rb") as f:  // ファイルを開く
        data = f.read(partialSize)  // ファイルの内容を読み出す

withブロックを使うことで、ブロックの終了時に自動でファイルオブジェクトをクローズすることができるので、 f.close() を記載する必要がなくなり、便利である。

また、今回ファイルを分割して読み込む必要があり、以下のメソッドを用いる。

f.read(size)

引数にsizeに従い、ファイルの内容をテキストまたはBlobオブジェクトとして読み出す。これを使うことで、ファイルの中身を分割して読み込むことができる。ただ、これだけでは先頭からのみしか取得できなくなってしまうので、ファイルオブジェクトのファイル位置を変更するために、seek を用いる。

f.seek(offset, whence)

whenceで設定した基準点からのオフセット値(offset)でファイル位置を変更することができる。

whence値 設定
0 ファイルの先頭から(デフォルト)
1 現在のファイルの位置から
2 ファイルの末端から

よって、以下のようなファイルを分割するメソッドを実装することができる。

def divideFile(file_path, start, end):
    partialSize = end - start + 1
    try:
        with open(file_path, "rb") as f:
            f.seek(start)
            data = f.read(partialSize)
            return data
    except Exception as e:
        print('Error' + e)

サンプルコード

上記をまとめることによって、ファイルを分割して API を用いてアップロードすることができた。

import urllib.request
import urllib.parse
import hashlib
import os
from copy import copy
import math

BLOCK_SIZE = 524288


def getDividedFile(file_path, start, end):
    partialSize = end - start + 1
    try:
        with open(file_path, "rb") as f:
            f.seek(start)
            data = f.read(partialSize)
            return data
    except Exception as e:
        print("Error" + e)


def uploadFile(file_path, upload_path):
    file_size = os.path.getsize(file_path)

    h = hashlib.new("md5")
    with open(file_path, "rb") as f:
        binary_data = f.read()
        h.update(binary_data)

    url = f"http://api.com/{urllib.parse.quote(upload_path)}?md5={h.hexdigest()}&size={file_size}"

    req_header = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/octet-stream",
    }

    start_byte = 0
    end_byte = BLOCK_SIZE - 1
    loop = math.ceil(file_size / BLOCK_SIZE)
    count = 0

    while count < loop:
        headers = copy(req_header)
        start_byte = count * BLOCK_SIZE
        end_byte = (count + 1) * BLOCK_SIZE - 1
        if end_byte > file_size:
            end_byte = file_size - 1
        count += 1

        headers["Content-Length"] = end_byte - start_byte + 1
        headers["file-range"] = f"{start_byte}-{end_byte}"

        req = urllib.request.Request(
            url=url,
            data=getDividedFile(file_path, start_byte, end_byte),
            method="POST",
            headers=headers,
        )

        try:
            urllib.request.urlopen(req)
            print(f"success upload file volume:{start_byte}-{end_byte}")
        except urllib.error.URLError as e:
            print(e)

サンプルでは、MD5ハッシュ値も計算して付与して、分割後のファイルと一致しているか確認ができるようリクエストに付加している。

まとめ

今回は、Pythonの標準ライブラリのみを用いて、ファイルを分割してアップロードする処理を実装できた。

それでは、ステキな開発ライフを。