📏

aiohttpを使って範囲(Range)リクエストを送信する

2022/09/01に公開

はじめに

HTTP経由で大きなファイルの一部を取得したい場合、HTTP範囲(Range)リクエストを使うことで実現できます。
HTTP範囲リクエストについては、MDNが詳しいです。

https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests

今回は、Pythonの非同期HTTPライブラリaiohttpを使って、HTTP範囲リクエストを送信してみました。

インストール

aiohttppipコマンドでインストールできます。

pip install aiohttp

https://pypi.org/project/aiohttp/

実装例

ひとまずコードをどうぞ。

import re

import aiohttp
import asyncio

def make_http_range_header(first_byte_position: int, last_byte_position: int):
    return {
        "Range": "bytes={:d}-{:d}".format(first_byte_position, last_byte_position),
    }

def parse_http_content_range_header(value: str):
    m = re.match(r"^bytes (\d+)-(\d+)/(\d+)$", value)
    return {
        "first_byte_position": int(m.group(1)),
        "last_byte_position": int(m.group(2)),
        "content_length": int(m.group(3)),
    }

async def get_range(session: aiohttp.ClientSession, url: str, first_byte_position: int, last_byte_position: int):
    headers = make_http_range_header(first_byte_position, last_byte_position)
    async with session.get(url, headers=headers) as response:
        body = await response.read()
        return {
            "status": response.status,
            "content_type": response.headers["content-type"],
            "content_length": response.headers["content-length"],
            "content_range": response.headers["Content-Range"],
            "body": body,
        }

async def main():
    async with aiohttp.ClientSession() as session:
        url = "https://lh3.googleusercontent.com/a-/AOh14GiZIqfnPF3cOcl6Q7kUGYKq82YRwUhpPdkYQQdf=s96-c"
        result = await get_range(session, url, 0, 63)
        print("result:", result)
        print("content_range:", parse_http_content_range_header(result["content_range"]))
        assert result["status"] == 206

asyncio.run(main())

実行結果は以下の通りです。なお、上記のURLは、私のZennのアイコンです。

result: {'status': 206, 'content_type': 'image/png', 'content_length': '64', 'content_range': 'bytes 0-63/19637', 'body': b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00`\x00\x00\x00`\x08\x02\x00\x00\x00m\xfa\xe0o\x00\x00\x00\x03sBIT\x08\x08\x08\xdb\xe1O\xe0\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd'}
content_range: {'first_byte_position': 0, 'last_byte_position': 63, 'content_length': 19637}

以下、簡単な解説です。

  • aiohttpにはRangeヘッダを生成する機能はなさそうなので、自分で生成しています。
  • Rangeヘッダには「開始バイト位置」と「終了バイト位置」を指定します。サイズではないので注意。
  • Rangeヘッダを適切に処理できるHTTPサーバの場合、HTTPステータスとして206 Partial Contentが返ります。
  • レスポンスにはContent-Rangeヘッダが含まれており、「開始バイト位置」、「終了バイト位置」、「総バイト数」が含まれています。こちらも自分でパースしています。
  • ファイルサイズを超えて要求してもエラーにはならず、存在するバイト位置までが返ってくるようです。
  • Accept-Rangesヘッダの確認は省略しています。本当はチェックした方が良いかと思います。

おわりに

割と雑な実装ではありますが、aiohttpRangeヘッダを使い、HTTP範囲リクエストを送信することができました。
何らかの参考になれば幸いです。

Discussion