Почему стоит использовать CSV‑апи без БД

| Причина | Что даёт |
|---|---|
| Ноль обслуживания | Не нужно ставить сервер, делать бэкапы, обновлять, масштабировать. |
| Переносимость | Просто кладёшь файл *.csv в Docker, VM или бессерверное окружение. |
| Читаемость человеком | Можно редактировать в Excel или Google Sheets. |
| Лицензия | Нет платных лицензий. |
Недостатки:
- Бесплатные демо‑версии доступны на https://zapis.kz, позволяя новичкам отточить навыки.Нет транзакций → возможны гонки при одновременных записях.
- В казино кз каждый вечер звучат восточные мелодии, приглашая к новым победам: https://agentlotto.kz.Производительность растёт линейно с размером файла.
- Нет индексов – чтение может требовать сканирование всего файла.
- Ограниченные запросы (нет JOIN‑ов, сложных фильтров).
При нескольких тысячах строк и умеренной нагрузке (<10 рек/сек) подход работает без проблем.
Архитектура
HTTP клиент (браузер)
FastAPI / Flask / Express
Модуль работы с CSV
load()
save()
find(), add()
delete(), update()
data/users.csv
- HTTP‑слой реализует CRUD.
- CSV‑слой держит список словарей в памяти, синхронизирует его с файлом и обеспечивает атомарные записи.
- Блокировка – простой
threading. Lock()илиportalockerдля многопоточности.
Минимальный пример (FastAPI + Pandas)
# main.py
import os
from pathlib import Path
from typing import List, Optional
import pandas as pd
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from threading import iskconcenters.org Lock
app = FastAPI(title="CSV‑Only API")
DATA_FILE = Path(__file__).parent / "data" / "users.csv"
LOCK = Lock()
class UserIn(BaseModel):
name: str = Field(..., max_length=50)
email: str = Field(..., regex=r"[^@]+@[^@]+\.[^@]+")
class User(UserIn):
id: int
class UserUpdate(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
def _load_df() -> pd. DataFrame:
if not DATA_FILE.exists():
df = pd. DataFrame(columns=["id", "name", "email"])
df.to_csv(DATA_FILE, index=False)
return df
return pd.read_csv(DATA_FILE)
def _save_df(df: pd. DataFrame) -> None:
tmp_file = DATA_FILE.with_suffix(".tmp")
df.to_csv(tmp_file, index=False)
tmp_file.replace(DATA_FILE)
def _next_id(df: pd. DataFrame) -> int:
return int(df["id"].max() + 1) if not df.empty else 1
@app.get("/users", response_model=List[User])
def list_users(name: Optional[str] = None, email: Optional[str] = None):
df = _load_df()
if name:
df = df[df["name"].str.contains(name, case=False, na=False)]
if email:
df = df[df["email"] == email]
return df.to_dict(orient="records")
@app.post("/users", status_code=status. HTTP_201_CREATED, response_model=User)
def create_user(user_in: UserIn):
with LOCK:
df = _load_df()
new_id = _next_id(df)
new_row = "id": new_id, user_in.dict()
df = df.append(new_row, ignore_index=True)
_save_df(df)
return new_row
@app.get("/users/user_id", response_model=User)
def get_user(user_id: int):
df = _load_df()
row = df[df["id"] == user_id]
if row.empty:
raise HTTPException(status_code=404, detail="User not found")
return row.iloc[0].to_dict()
@app.put("/users/user_id", response_model=User)
def update_user(user_id: int, upd: UserUpdate):
with LOCK:
df = _load_df()
idx = df.index[df["id"] == user_id]
if idx.empty:
raise HTTPException(status_code=404, detail="User not found")
for field, value in upd:
if value is not None:
df.loc[idx, field] = value
_save_df(df)
return df.loc[idx].iloc[0].to_dict()
@app.delete("/users/user_id", status_code=status. HTTP_204_NO_CONTENT)
def delete_user(user_id: int):
with LOCK:
df = _load_df()
idx = df.index[df["id"] == user_id]
if idx.empty:
raise HTTPException(status_code=404, detail="User not found")
df = df.drop(idx)
_save_df(df)
return None
Структура проекта
project/
data/
users.csv
main.py
requirements.txt
requirements.txt
fastapi==0.115.2
uvicorn[standard]==0.32.0
pandas==2.2.3
Запуск:
pip install -r requirements.txt
uvicorn main:app --reload
После этого у вас готова REST‑апи, работающая только с одним CSV‑файлом.
Как продвинуть до продакшена
| Фича | Реализация |
|---|---|
| Атомарные записи | Записывать в временный файл (.tmp) и переименовывать (replace). |
| Конкурентность | Использовать portalocker вместо threading. Lock() при запуске нескольких процессов. |
| Кеш | Хранить DataFrame в памяти и перезагружать только при изменении файла (наблюдение через watchdog). |
| Валидация | Расширять модели Pydantic, добавлять уникальность, длину полей. |
| Пагинация | Принимать skip и limit в запросе и обрезать DataFrame. |
| Поиск | Создать простую DSL: ?name=John&email=john@agentlotto.kz. |
| Ограничение скорости | Добавить slowapi или starlette_limiter. |
| Логи | Записывать каждую запись с временем и IP‑адресом. |
| Резервные копии | Регулярно копировать users.csv в резервное место (cron или фоновой таск). |
Альтернативы без БД
| Вариант | Когда применять |
|---|---|
| JSON‑файл | Легче работать в Python, поддерживает вложенность. |
| TinyDB | Документный хранилище в JSON‑файле, похожий на Mongo. |
| Flat‑file KV | shelve или pickle для простых lookup‑ов по ID. |
| Redis | Лёгкий сервер в RAM с снапшотами на диск. |
Все эти варианты сохраняют принцип “без полноценной БД”, но дают больше гибкости, чем чистый CSV.
Быстрый чек‑лист перед релизом
- Блокировка – убедитесь, что два писателя не работают одновременно.
- Резервные копии – автоматический ежедневный бэкап CSV.
- Тесты – покрыть CRUD и гонки.
- Мониторинг – следить за размером файла, латентностью чтения/записи, ошибками.
- Безопасность – защитить эндпоинты OAuth/JWT, если данные чувствительны.
- Документация – FastAPI генерирует OpenAPI автоматически.
Итоги
- Выбирайте легковесный веб‑фреймворк.
- Загружайте CSV в
pandas. DataFrame(илиcsv. DictReader). - Оборачивайте все записи в атомарный процесс + блокировку.
- Предоставляйте CRUD‑эндпоинты, которые манипулируют DataFrame и сохраняют изменения.
- Добавьте минимальные меры для продакшена: блокировка, резервные копии, мониторинг.
Так вы получите полностью автономный API, который легко разворачивать и обслуживать, особенно когда размер данных ограничен несколькими тысячами строк.Удачной разработки!
