将 StreamingHttpResponse 与 Django Rest Framework CSV 一起使用

Posted

技术标签:

【中文标题】将 StreamingHttpResponse 与 Django Rest Framework CSV 一起使用【英文标题】:Using StreamingHttpResponse with Django Rest Framework CSV 【发布时间】:2018-03-23 12:33:08 【问题描述】:

我有一个标准的 DRF Web 应用程序,它为其中一条路线输出 CSV 数据。渲染整个 CSV 表示需要一段时间。数据集非常大,所以我想要一个流式 HTTP 响应,这样客户端就不会超时。

但是,使用https://github.com/mjumbewu/django-rest-framework-csv/blob/2ff49cff4b81827f3f450fd7d56827c9671c5140/rest_framework_csv/renderers.py#L197 中提供的示例并不能完全实现这一点。数据仍然是一个大的有效载荷,而不是被分块,并且客户端最终在接收到字节之前等待响应。

结构类似如下:

models.py

class Report(models.Model):
  count = models.PostiveIntegerField(blank=True)
  ...

renderers.py

class ReportCSVRenderer(CSVStreamingRenderer):
  header = ['count']

serializers.py

class ReportSerializer(serializers.ModelSerializer):
  count = fields.IntegerField()

  class Meta:
    model = Report

views.py

class ReportCSVView(generics.Viewset, mixins.ListModelMixin):
  def get_queryset(self):
    return Report.objects.all()

  def list(self, request, *args, **kwargs):
    queryset = self.get_queryset()
    data = ReportSerializer(queryset, many=True)
    renderer = ReportCSVRenderer()

    response = StreamingHttpResponse(renderer.render(data), content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="f.csv"'

    return response

注意:必须注释掉或更改某些内容。

谢谢

【问题讨论】:

【参考方案1】:

对于小的响应,Django 的 StreamingHttpResponse 可能比传统的 HttpResponse 慢得多。

如果不需要,请不要使用它; Django Docs 实际上建议 StreamingHttpResponse 仅在绝对要求在将数据传输到客户端之前不迭代整个内容时才使用。”

对于您的问题,您可能会发现设置 chunk_size、切换到 FileResponse 或返回到正常响应(如果使用 REST 框架)或 HttpResponse 很有用。

编辑1:关于设置块大小:

在File api 中,您可以分块打开文件,因此并非所有文件都加载到内存中。

我希望你觉得这很有用。

【讨论】:

“对于您的问题,您可能会发现设置 chunk_size 很有用” - 在哪里设置 chunk_size 在File api 中,您可以分块打开文件,因此并非所有文件都加载到内存中。 感谢您的帮助! :)【参考方案2】:

所以我最终找到了一个我很高兴将Paginator 类与查询集一起使用的解决方案。首先,我编写了一个子类化 CSVStreamingRenderer 的渲染器,然后在我的 CSVViewset 的渲染器中使用它。

renderers.py

from rest_framework_csv.renderers import CSVStreamingRenderer

# *****************************************************************************
# BatchedCSVRenderer
# *****************************************************************************


class BatchedCSVRenderer(CSVStreamingRenderer):

    """
    a CSV renderer that works with large querysets returning a generator
    function. Used with a streaming HTTP response, it provides response bytes
    instead of the client waiting for a long period of time
    """

    def render(self, data, renderer_context=, *args, **kwargs):
        if 'queryset' not in data:
            return data

        csv_buffer = Echo()
        csv_writer = csv.writer(csv_buffer)

        queryset = data['queryset']
        serializer = data['serializer']

        paginator = Paginator(queryset, 50)

        #  rendering the header or label field was taken from the tablize
        #  method in django rest framework csv

        header = renderer_context.get('header', self.header)
        labels = renderer_context.get('labels', self.labels)

        if labels:
            yield csv_writer.writerow([labels.get(x, x) for x in header])
        else:
            yield csv_writer.writerow(header)

        for page in paginator.page_range:
            serialized = serializer(
                paginator.page(page).object_list, many=True
            ).data

            #  we use the tablize function on the parent class to get a
            #  generator that we can use to yield a row

            table = self.tablize(
                serialized,
                header=header,
                labels=labels,
            )

            #  we want to remove the header from the tablized data so we use
            #  islice to take from 1 to the end of generator

            for row in itertools.islice(table, 1, None):
                yield csv_writer.writerow(row)

# *****************************************************************************
# ReportsRenderer
# *****************************************************************************


class ReportsRenderer(BatchedCSVRenderer):

    """
    A render for returning CSV data for reports

    """

    header = [ ... ]
    labels =  ... 

views.py

from django.http import StreamingHttpResponse
from rest_framework import mixins, viewsets

# *****************************************************************************
# CSVViewSet
# *****************************************************************************


class CSVViewSet(
        mixins.ListModelMixin,
        viewsets.GenericViewSet,
):

    def list(self, request, *args, **kwargs):
        queryset = self.get_queryset()

        return StreamingHttpResponse(
            request.accepted_renderer.render(
                'queryset': queryset,
                'serializer': self.get_serializer_class(),
            )
)

# *****************************************************************************
# ReportsViewset
# *****************************************************************************


class ReportCSVViewset(CSVViewSet):

    """
    Viewset for report CSV output

    """

    renderer_classes = [ReportCSVRenderer]
    serializer_class = serializers.ReportCSVSerializer

    def get_queryset(self):
        queryset = Report.objects.filter(...)

这对于流式响应来说可能看起来很多,但我们在许多其他地方使用了BatchedCSVRenderCSVViewset。如果您在 nginx 后面运行您的服务器,那么调整那里的设置以允许流式响应也可能很有用。

希望这可以帮助任何有相同目标的人。如果有任何其他信息可以提供,请告诉我。

【讨论】:

【参考方案3】:

您需要在呈现数据时提供 CSV 标头(通过 header 参数):

renderer.render(data, renderer_context='header': ['header1', 'header2', 'header3'])

如果您不指定 header 参数,djangorestframework-csv 将尝试自行“猜测”CSV 标头。要“猜测”CSV 标头,djangorestframework-csv 会将您所有的data 加载到内存中,从而导致您遇到延迟。

【讨论】:

【参考方案4】:

受@3066d0 启发的更简单的解决方案:

renderers.py

class ReportsRenderer(CSVStreamingRenderer):
    header = [ ... ]
    labels =  ... 

views.py

class ReportCSVViewset(ListModelMixin, GenericViewSet):
    queryset = Report.objects.select_related('stuff')
    serializer_class = ReportCSVSerializer
    renderer_classes = [ReportsRenderer]
    PAGE_SIZE = 1000

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        response = StreamingHttpResponse(
            request.accepted_renderer.render(self._stream_serialized_data(queryset)),
            status=200,
            content_type="text/csv",
        )
        response["Content-Disposition"] = 'attachment; filename="reports.csv"'
        return response

    def _stream_serialized_data(self, queryset):
        serializer = self.get_serializer_class()
        paginator = Paginator(queryset, self.PAGE_SIZE)
        for page in paginator.page_range:
            yield from serializer(paginator.page(page).object_list, many=True).data

关键是您需要将生成序列化数据的生成器作为data 参数传递给渲染器,然后CSVStreamingRenderer 执行其操作并流式传输响应本身。我更喜欢这种方式,因为这样你就不需要重写第三方库的代码了。

【讨论】:

以上是关于将 StreamingHttpResponse 与 Django Rest Framework CSV 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

Django:在现有的 html 页面上返回 StreamingHttpResponse

仅在 Django 的 StreamingHttpResponse 中的模板上呈现当前状态

Django 1.5 - 使用新的 StreamingHttpResponse

Django 1.5 - 使用新的 StreamingHttpResponse

Django 3.1:带有异步生成器的 StreamingHttpResponse

当客户端断开连接时,如何在 django 中停止 StreamingHttpResponse?