Django python — выгрузка результатов тяжелого запроса в CSV c разделителем

Исходные данные:

  • Джанго проект, база данных
  • Есть сложный запрос, с большим количеством данных. Часто запрос моно описать моделями, чтобы усложнить задачу, будем писать сырой запрос mysql
  • Необходимо надежно выгрузить в CSV. Может даже миллионы строк
  • первой строкой в выгрузке будет хедер с названием столбцов
  • кастомный разделитель полей, например точка с запятой

1) views.py

  • добавляем необходимые импорты в начале кода
from django.http import StreamingHttpResponse
import csv
  • файло-подобный объект, который понадобится для добавления строк запроса в выходной CSV
class Echo:
    def write(self, value):
        return value
  • сам метод выгрузки в csv
def download_csv(filename, rows):
    pseudo_buffer = Echo()
    # используем кастомный разделитель полей - точка с запятой, по умолчанию, разделитель - запятая
    writer = csv.writer(pseudo_buffer, delimiter=';')
    return StreamingHttpResponse(
        (
            writer.writerow(row) for row in rows
        ),
            content_type="text/csv",
            headers={'Content-Disposition': 'attachment; filename="' + filename + '.csv"'},
        )
  • и пример его использования:
def download_orders_for_user(request):
    # например, запрос всех заказов пользователей с айдишниками от 1 до 10000 и суммой больше 10
    query = """
        SELECT order.number, 
            order.date, 
            order.sum, 
            order.status
        FROM order
        LEFT JOIN user ON user.id = order.user_id
        WHERE user BETWEEN 1 and 10000
        AND order.sum > 10
    """

    with connections["default"].cursor() as cursor:
        cursor.execute(query)
        rows = cursor.fetchall()

    # формируем имя файла
    filename = 'orders_for_users'

    # первой строкой у нас будет хедер с названием столбцов
    header_row = (('Номер', 'Дата', 'Сумма', 'Статус'),)
    rows = header_row + rows
    return download_csv(filename, rows)

2) urls.py

Зададим 2 урла:

  • для страницы, со ссылкой на выгрузку
  • для запроса выгрузки (нажатие на ссылку выгрузки)
…
urlpatterns = [
…
path('download_page/', views.download_page, name='download_page'),
path('download_page/csv/', views.download_orders_for_user, name='download_orders_for_user'),
]

3) создадим темплейт с HTML страничкой

для размещения ссылки скачивания и проверки работы наших скриптов
download_page.html
со следующим содержимым:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Download SCV</title>
</head>
<body>
    <div>
        <h4>Скачать заказы пользователей: </h4>
        <a id="download" href="csv/">CSV</a>
    </div>
</body>
</html>

Это все!

Django python — загрузка csv и сохранение в базу данных

Это руководство для новичков в Django, которые прочитали начальное руководство по Django на официальном сайте и столкнулись с задачей загрузки csv файла с таблицей в базу данных

В этом случае, у вас есть приложение polls, с урлами, моделями, вьюшками для классов вопросов (Question) и ответов (Choices). И база данных с сгенерированными таблицами (polls_question, polls_choice)

Для примера, будем загружать в базу данных csv файл с вопросами (Questions). 

Назовем файл questions_01a.csv и поместим туда такое содержимое:

Why?,2022-11-14 10:00
For what?,2022-11-14 10:01
When?,2022-11-14 10:02
Where?,2022-11-14 10:03

Перенос строк — как разделитель информации по вопросам, и запятая — как разделитель ячеек данных

Изменения в /polls/urls.py:

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    …
    path('upload_csv/', views.upload_csv, name='upload_csv'),
]

Изменения в /polls/views.py:

from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.views import generic
from django.utils import timezone
from django.contrib import messages

from .models import Question, Choice

…

def upload_csv(request):
    data = {}
    if "GET" == request.method:
        return render(request, "polls/upload.html", data)
    # if not GET, then proceed
    try:
        csv_file = request.FILES["csv_file"]
        if not csv_file.name.endswith('.csv'):
            messages.error(request, 'File is not a CSV')
            return HttpResponseRedirect(reverse("polls:upload_csv"))
        # if file is too large - error
        if csv_file.multiple_chunks():
            messages.error(request, "Uploaded file is too big (%.2f MB). " % (csv_file.size/(1000*1000),))
            return HttpResponseRedirect(reverse("polls:upload_csv"))

        file_data = csv_file.read().decode("utf-8")

        lines = file_data.split("\n")
        # loop over the lines and save them to db via model
        for line in lines:
            fields = line.split(",")

            try:
                question = Question(
                    question_text=fields[0],
                    pub_date=fields[1],
                )
                question.save()

            except Exception as e:
                messages.error(request, "Unable to upload file. "+repr(e))
                pass

    except Exception as e:
        messages.error(request, "Unable to upload file. "+repr(e))

    return HttpResponseRedirect(reverse("polls:upload_csv"))

Что делаем в views.py:

  • Импортируем стандартный фреймворк сообщений (messages), чтобы можно было на страничке выводить ошибки
  • Файл будем посылать методом POST, если же запрос GET — открываем страницу аплода
  • Делаем простые проверки получаемого файла (тип файла и максимальный размер)
  • Читаем построчно и сохраняем в базу данных, через модель

Создаем страничку для загрузки файла — /polls/templates/polls/upload.html со следующим кодом:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload</title>
    <link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <form action="{% url "polls:upload_csv" %}" method="POST" enctype="multipart/form-data" class="form-horizontal">
        {% csrf_token %}
        <div class="form-group">
            <label for="name" class="col-md-3 col-sm-3 col-xs-12 control-label">File: </label>
            <div class="col-md-8">
                <input type="file" name="csv_file" id="csv_file" required="True" class="form-control">
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-3 col-sm-3 col-xs-12 col-md-offset-3" style="margin-bottom:10px;">
                 <button class="btn btn-primary"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span>Upload </button>
            </div>
        </div>
        {% if messages %}
        <ul class="messages">
            {% for message in messages %}
            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
    </form>
</body>
</html>

Что делаем в upload.html:

  • Минималистичная html страница
  • Подключаем стили бутстрап для более-менее симпатичного отображения элементов
  • Форма с кнопкой Загрузки
  • CSFR токен для защиты формы от размещения ее на внешних источниках, чтобы украсть ваши данные (стандартная защита)
  • Блок для отображения ошибок messages

Запускаем проект и пробуем открывать страничку 

http://127.0.0.1:8000/polls/upload_csv/

Загружаем наш заранее заготовленный файл questions_01a.csv и проверяем базу данных на наличие новых строк

Выглядит это так: