Реализация загрузки данных на сервер средствами Python
Загрузка файлов на сервер средствами 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', 200Python 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.
Расширенные примеры загрузки файлов
Пример 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.