2024년 1월 8일 월요일

requests post 에서 streaming 사용방법

requests streaming 의 필요성

requests에서 streaming이란 조금씩 buffering 하는것을 의미합니다. 일반적으로 파일을 upload하게 된다면 requests 모듈안에서는 파일 통째로 메모리에 올려서 upload하게 됩니다.

만약 파일의 크기가 크다면 어떻게 될까요? 많은 메모리가 필요하게 됩니다. 심지어는 메모리 부족 사태까지 발생될 수 있습니다.


requests streaming 사용법

가이드 문서에서는 아래 링크를 참고 하면 됩니다.

https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads

Streaming Uploads

Requests supports streaming uploads, which allow you to send large streams or files without reading them into memory. To stream and upload, simply provide a file-like object for your body:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Warning

It is strongly recommended that you open files in binary mode. This is because Requests may attempt to provide the Content-Length header for you, and if it does this value will be set to the number of bytes in the file. Errors may occur if you open the file in text mode.

위 용법을 보면 크게 기존 사용하는 방법과 큰 차이가 없어보입니다.

그렇다면 이미 streaming 하고 있는걸까요?

실제 테스트해보니 동작하고 있지 않았습니다.


코드 안쪽으로 디버깅 해보기

일반적으로 requests를 사용해서 파일을 업로드시 아래와 같은 방식으로 사용하게 됩니다.

https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file

POST a Multipart-Encoded File

Requests makes it simple to upload Multipart-encoded files:

>>> url = 'https://httpbin.org/post'
>>> files = {'file': open('report.xls', 'rb')}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
  "files": {
    "file": "<censored...binary...data>"
  },
  ...
}

차이가 뭘까요? 

정답은 data=f , files=files 넘어가는 인자가 틀립니다. 

그렇습니다. 

!중요! requests에서 files로 넘어가는 인자에 대해서는 streaming 을 지원하지 않습니다.

엥 이게 무슨말이냐고요? 거짓말 아닌지 문의하실것 같은데요.

코드에서 로그를 넣어서 확인이 가능했습니다.

requets/models.py라는 코드를 살펴보다보면 아래와 같은 부분이 보입니다.

def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""

# Check if file, fo, generator, iterator.
# If not, run through normal process.

# Nottin' on you.
body = None
content_type = None

if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
content_type = "application/json"

try:
body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
raise InvalidJSONError(ve, request=self)

if not isinstance(body, bytes):
body = body.encode("utf-8")

is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

if is_stream:
try:
length = super_len(data)
except (TypeError, AttributeError, UnsupportedOperation):
length = None

body = data

if getattr(body, "tell", None) is not None:
# Record the current file position before reading.
# This will allow us to rewind a file in the event
# of a redirect.
try:
self._body_position = body.tell()
except OSError:
# This differentiates from None, allowing us to catch
# a failed `tell()` later when trying to rewind the body
self._body_position = object()

if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


    is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

is_stream 이 True되는 조건자체가 data인자만 확인하도록 되어 있습니다. 즉 streaming은 file쪽은 확인하지도 않게되고, 만약 is_stream 으로 들어가서도 files가 존재하게된다면 아래 코드에 의해서 exception이 발생하도록 되어있습니다.

        if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


해결 방법

이미 다른 분들이 구현해 놓은 모듈이 존재합니다.

https://github.com/requests/toolbelt

사용법은 files로 넘어가는 부분을 data로 옮기는 작업을 해야합니다.

github의 예제를 참고해서 data+files를 MultipartEncoder 넣어서 작업하면 됩니다.

from requests_toolbelt import MultipartEncoder
import requests

m = MultipartEncoder(
    fields={'field0': 'value', 'field1': 'value',
            'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
    )

r = requests.post('http://httpbin.org/post', data=m,
                  headers={'Content-Type': m.content_type})



댓글 없음:

댓글 쓰기