Реализация загрузки данных на сервер средствами Python

Раздел: Сетевое программирование -> HTTP

Загрузка файлов на сервер средствами Python

В веб-приложениях часто возникает необходимость передать файл с клиента на сервер. Ниже рассматриваются различные подходы к реализации этой задачи с использованием языка Python. Основное внимание уделяется библиотеке Flask для серверной части и requests для клиентской.

Основной подход: сервер на Flask и клиент на requests

Наиболее распространённый способ - создать эндпоинт на Flask, который принимает POST-запрос с файлом, и отправить этот файл с помощью библиотеки requests. Серверная часть использует объект request.files для доступа к переданным данным. Клиент формирует multipart/form-data запрос, передавая файл как часть словаря в параметр files.

Пример серверного кода (файл app.py):

from flask import Flask, request, jsonify
import os

app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part'}), 400
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    filename = secure_filename(file.filename)  # рекомендуется
    file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    return jsonify({'message': 'File uploaded', 'filename': filename}), 200

if __name__ == '__main__':
    app.run(debug=True)

Python requests get (get-запрос через requests в python)

Клиентская часть (файл client.py):

import requests

url = 'http://127.0.0.1:5000/upload'
files = {'file': open('image.png', 'rb')}
response = requests.post(url, files=files)
print(response.json())

Get html python (получение html-содержимого через http в python)

Пояснение шагов:

  • Сервер ожидает POST-запрос на адрес /upload.
  • Поле 'file' в request.files должно совпадать с именем ключа, переданного в клиентском словаре.
  • Функция secure_filename из модуля werkzeug.utils удаляет потенциально опасные символы из имени файла.
  • Клиент открывает файл в бинарном режиме и передаёт его как кортеж (имя файла, файловый объект) или просто файловый объект.

Типичные ошибки и их решения

  • Ошибка 400 Bad Request - сервер не видит поле file. Причина: неправильное имя поля в клиенте или сервер ожидает другое имя. Решение: проверить совпадение ключей.
  • Файл не сохраняется - не указан путь к папке загрузок или нет прав на запись. Решение: создать папку заранее и проверить права.
  • Ошибка Werkzeug BadRequest - превышен максимальный размер файла. Решение: настроить app.config['MAX_CONTENT_LENGTH'] и обработать исключение.

Альтернативные сценарии

Как загрузить несколько файлов одновременно?

Для отправки нескольких файлов используется один и тот же ключ для всех файлов, либо разные ключи. Сервер получает их через request.files.getlist('files').

# Сервер
from werkzeug.utils import secure_filename

@app.route('/upload_multiple', methods=['POST'])
def upload_multiple():
    if 'files' not in request.files:
        return 'No files', 400
    files = request.files.getlist('files')
    saved_names = []
    for f in files:
        if f.filename:
            filename = secure_filename(f.filename)
            f.save(os.path.join(UPLOAD_FOLDER, filename))
            saved_names.append(filename)
    return jsonify({'files': saved_names})

Url запрос python (работа с url в python)

# Клиент (отправка двух файлов)
import requests
url = 'http://127.0.0.1:5000/upload_multiple'
files = [('files', open('a.txt', 'rb')), ('files', open('b.txt', 'rb'))]
r = requests.post(url, files=files)
print(r.json())

Python urllib request (отправка запросов с помощью urllib.request в python)

Проблемы

Если ключи разные (file1, file2), сервер должен получать каждый отдельно. Использование getlist с одним ключом удобнее.

Как передать вместе с файлом дополнительные поля (например, описание)?

В клиенте передаются два отдельных словаря: files и data. Сервер получает поля из request.form.

# Клиент
files = {'file': open('photo.jpg', 'rb')}
data = {'description': 'Отпуск 2024'}
r = requests.post(url, files=files, data=data)

# Сервер
@app.route('/upload_with_desc', methods=['POST'])
def upload_with_desc():
    desc = request.form.get('description', '')
    file = request.files['file']
    # ... сохранение и возврат результата

Files upload python (загрузка файлов на сервер с помощью python (requests, flask))

Ошибки

При использовании data вместе с files библиотека requests автоматически формирует multipart/form-data, так что конфликтов не возникает. Однако имена полей не должны совпадать с именами файлов.

Как ограничить размер и тип загружаемого файла на сервере?

Flask позволяет установить MAX_CONTENT_LENGTH. Для проверки типа используется свойство content_type или библиотека python-magic для определения MIME-типа по содержимому.

app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16 MB

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload_restricted', methods=['POST'])
def upload_restricted():
    file = request.files.get('file')
    if not file or not allowed_file(file.filename):
        return 'Invalid file type', 400
    # дополнительная проверка MIME по магии
    import magic
    mime = magic.from_buffer(file.read(2048), mime=True)
    if mime not in ['image/png', 'image/jpeg']:
        return 'Content type not allowed', 400
    file.seek(0)  # вернуть указатель в начало
    # сохранение...

Python requests method (методы http-запросов в python (get, post, put, delete) с requests)

Ошибки

Проверка только по расширению небезопасна - злоумышленник может переименовать файл. Рекомендуется проверять MIME-тип по первым байтам. При использовании file.read нужно вернуть позицию через seek(0).

Как обрабатывать большие файлы без полной загрузки в память?

По умолчанию Flask загружает весь файл в память, если размер превышает порог (по умолчанию 500 KB). Для потоковой обработки можно работать с request.stream и сохранять данные частями.

@app.route('/upload_stream', methods=['POST'])
def upload_stream():
    # Используем request.stream для чтения по частям
    with open('output.bin', 'wb') as f:
        while True:
            chunk = request.stream.read(8192)
            if not chunk:
                break
            f.write(chunk)
    return 'Stream uploaded', 200

Python request data (извлечение данных из http-запроса (request) в python)

Клиент отправляет файл обычным способом, но сервер не вызывает file.save(), а читает сырой поток.

Проблемы

Такой подход теряет информацию об имени файла и MIME-типе, так как они передаются в заголовках multipart. Чтобы их получить, нужно разбирать multipart вручную (например, через werkzeug.formparser). Для большинства случаев достаточно стандартного request.files с последующей буферизацией на диск.

Как загрузить файл из браузера (HTML-форма) и обработать на сервере?

Сервер остаётся тем же. клиент - HTML-страница с формой и атрибутом enctype="multipart/form-data". Python requests может имитировать такую отправку.


На сервере обработка идентична примеру из базового решения.

Ошибки

Частая ошибка - забыть указать enctype. В этом случае браузер отправляет данные в формате application/x-www-form-urlencoded, и файл не передаётся. Рекомендуется проверять наличие файла в request.files.

- Python requests параметры (передача параметров в get/post запросах requests в python)
- Request session python (использование session в библиотеке requests (куки, заголовки) в python)
- Python requests timeout (установка таймаута для http-запроса (requests timeout) в python)

Расширенные примеры загрузки файлов

Пример 1: Загрузка с отображением прогресса (tqdm)

Для отслеживания прогресса при отправке большого файла можно обернуть чтение файла в итератор, сообщающий размер переданных данных.

Пример
import requests
from tqdm import tqdm

class ProgressFile:
    def __init__(self, file, total_size):
        self.file = file
        self.total_size = total_size
        self.seen = 0

    def __iter__(self):
        return self

    def __next__(self):
        chunk = self.file.read(8192)
        if not chunk:
            raise StopIteration
        self.seen += len(chunk)
        pbar.update(len(chunk))
        return chunk

file_size = os.path.getsize('large_file.iso')
with open('large_file.iso', 'rb') as f:
    with tqdm(total=file_size, unit='B', unit_scale=True) as pbar:
        progress_file = ProgressFile(f, file_size)
        response = requests.post(
            'http://example.com/upload',
            files={'file': progress_file}
        )
print(response.status_code)

Примечание

Библиотека requests ожидает, что файл будет передан как объект с методом read() или итератором. В данном случае создан итератор, читающий файл и обновляющий прогресс-бар. Убедитесь, что сервер поддерживает потоковую передачу.

Пример 2: Асинхронная загрузка с aiohttp и aiofiles

Для неблокирующей загрузки файлов на сервер Flask, работающий синхронно, не подходит. Можно использовать aiohttp на клиенте и, например, FastAPI или aiohttp на сервере.

Пример
# Клиент
import aiohttp
import aiofiles

async def upload_file(url, filepath):
    async with aiohttp.ClientSession() as session:
        async with aiofiles.open(filepath, 'rb') as f:
            data = aiohttp.FormData()
            data.add_field('file', await f.read(), filename='document.pdf',
                           content_type='application/pdf')
            async with session.post(url, data=data) as response:
                return await response.json()

# Запуск
import asyncio
result = asyncio.run(upload_file('http://127.0.0.1:8000/upload', 'test.pdf'))
print(result)
Вывод (если сервер возвращает JSON): {'message': 'File uploaded', 'filename': 'document.pdf'}

На стороне сервера (FastAPI):

Пример
from fastapi import FastAPI, UploadFile, File

app = FastAPI()

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    with open(f"uploads/{file.filename}", "wb") as f:
        f.write(contents)
    return {"message": "File uploaded", "filename": file.filename}

Проблемы

Асинхронное чтение файла целиком в память может быть проблемой для больших файлов. Для потоковой загрузки следует использовать file.file (асинхронный генератор).

Пример 3: Загрузка файла напрямую в Amazon S3 через multipart upload

Для больших файлов (более 5 GB) применяется multipart upload из boto3.

Пример
import boto3
from boto3.s3.transfer import TransferConfig

s3_client = boto3.client('s3')
config = TransferConfig(multipart_threshold=5 * 1024 * 1024 * 1024,  # 5 GB
                        max_concurrency=10,
                        multipart_chunksize=8 * 1024 * 1024)  # 8 MB

response = s3_client.upload_file(
    'large_file.bin',
    'my-bucket',
    'remote/large_file.bin',
    Config=config
)
print('Upload complete' if response else 'Error')
Вывод: Upload complete

Клиент Python не участвует напрямую, загрузка происходит средствами AWS SDK.

Примечание

Для использования boto3 необходимы учетные данные AWS. Multipart upload автоматически разбивает файл на части и загружает их параллельно, что уменьшает влияние задержек.

Пример 4: Загрузка с использованием Flask-Uploads (Flask-Reuploaded)

Расширение Flask-Uploads упрощает работу с файлами, предоставляя встроенную валидацию и сохранение.

Пример
from flask import Flask, request
from flask_uploads import UploadSet, configure_uploads, IMAGES

app = Flask(__name__)
app.config['UPLOADED_PHOTOS_DEST'] = 'static/uploads'
photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)

@app.route('/upload', methods=['POST'])
def upload():
    if 'photo' in request.files:
        filename = photos.save(request.files['photo'])
        return f'Saved as {filename}', 200
    return 'No file', 400

Клиентская часть остаётся неизменной.

Ошибки

Flask-Uploads по умолчанию проверяет расширение файла, но не содержимое. Для повышения безопасности рекомендуется дополнительная проверка MIME.

Загрузка файлов на сервер с помощью Python (requests, Flask) - comments

En
Files upload python (python)