我有一个标准的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
注意:必须注释或更改某些内容。
谢谢
答案 0 :(得分:2)
对于较小的响应,Django的StreamingHttpResponse
可能比传统的HttpResponse
慢得多。
如果不需要,请不要使用它; Django Docs实际上建议StreamingHttpResponse
仅在绝对需要在将数据传输到客户端之前不重复整个内容的情况下使用。”
同样,对于您的问题,您可能会发现设置“ chunk_size”,切换到FileResponse或返回正常的Response(如果使用REST框架)或HttpResponse很有用。
编辑1:关于设置块大小:
在File api中,您可以分块打开文件,因此并非所有文件都已加载到内存中。
我希望您觉得这有用。
答案 1 :(得分:2)
一个更简单的解决方案,灵感来自@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
执行其操作并传输响应本身。我更喜欢这种方法,因为这样你就不需要覆盖第三方库的代码。
答案 2 :(得分:0)
因此,我最终找到了一个对将Paginator
类与queryset结合使用感到满意的解决方案。首先,我编写了一个渲染器,将CSVStreamingRenderer
子类化,然后在CSVViewset的Renderer中使用了该渲染器。
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(...)
对于流式响应,这似乎很多,但是我们在其他许多地方使用了BatchedCSVRender
和CSVViewset
。如果您要在nginx后面运行服务器,那么调整那里的设置以允许流式响应也可能很有用。
希望这对拥有相同目标的人有所帮助。让我知道我是否还能提供其他信息。
答案 3 :(得分:0)
渲染数据时,您需要提供CSV标头(通过header
参数)
renderer.render(data, renderer_context={'header': ['header1', 'header2', 'header3']})
如果您未指定header
参数,则djangorestframework-csv
会尝试自行“猜测” CSV标头。要“猜测” CSV标头,djangorestframework-csv
会将所有data
加载到内存中,从而导致您遇到延迟。