2011-02-28 110 views
17

我正嘗試將csv文件作爲附件下載進行流式處理。 CSV文件大小已達到4MB或更多,我需要一種方式讓用戶主動下載文件,而無需等待所有數據被創建並首先提交到內存。在Django中流式傳輸CSV文件

我第一次使用我自己的文件包裝基於Django的FileWrapper類。那失敗了。然後我在這裏看到了一種使用發電機來流的響應: How to stream an HttpResponse with Django

當我在發電機內產生一個錯誤,我可以看到,我創建與get_row_data()功能正確的數據,但是當我嘗試返回它回覆空的響應。我也禁用了Django GZipMiddleware。有誰知道我做錯了什麼?

編輯:我遇到的問題是與ConditionalGetMiddleware。我不得不更換它,代碼在下面的答案。

這裏是視圖:

from django.views.decorators.http import condition 

@condition(etag_func=None) 
def csv_view(request, app_label, model_name): 
    """ Based on the filters in the query, return a csv file for the given model """ 

    #Get the model 
    model = models.get_model(app_label, model_name) 

    #if there are filters in the query 
    if request.method == 'GET': 
     #if the query is not empty 
     if request.META['QUERY_STRING'] != None: 
      keyword_arg_dict = {} 
      for key, value in request.GET.items(): 
       #get the query filters 
       keyword_arg_dict[str(key)] = str(value) 
      #generate a list of row objects, based on the filters 
      objects_list = model.objects.filter(**keyword_arg_dict) 
     else: 
      #get all the model's objects 
      objects_list = model.objects.all() 
    else: 
     #get all the model's objects 
     objects_list = model.objects.all() 
    #create the reponse object with a csv mimetype 
    response = HttpResponse(
     stream_response_generator(model, objects_list), 
     mimetype='text/plain', 
     ) 
    response['Content-Disposition'] = "attachment; filename=foo.csv" 
    return response 

這裏是我使用流式傳輸的響應發電機:

def stream_response_generator(model, objects_list): 
    """Streaming function to return data iteratively """ 
    for row_item in objects_list: 
     yield get_row_data(model, row_item) 
     time.sleep(1) 

,這裏是我如何創建CSV行數據:

def get_row_data(model, row): 
    """Get a row of csv data from an object""" 
    #Create a temporary csv handle 
    csv_handle = cStringIO.StringIO() 
    #create the csv output object 
    csv_output = csv.writer(csv_handle) 
    value_list = [] 
    for field in model._meta.fields: 
     #if the field is a related field (ForeignKey, ManyToMany, OneToOne) 
     if isinstance(field, RelatedField): 
      #get the related model from the field object 
      related_model = field.rel.to 
      for key in row.__dict__.keys(): 
       #find the field in the row that matches the related field 
       if key.startswith(field.name): 
        #Get the unicode version of the row in the related model, based on the id 
        try: 
         entry = related_model.objects.get(
          id__exact=int(row.__dict__[key]), 
          ) 
        except: 
         pass 
        else: 
         value = entry.__unicode__().encode("utf-8") 
         break 
     #if it isn't a related field 
     else: 
      #get the value of the field 
      if isinstance(row.__dict__[field.name], basestring): 
       value = row.__dict__[field.name].encode("utf-8") 
      else: 
       value = row.__dict__[field.name] 
     value_list.append(value) 
    #add the row of csv values to the csv file 
    csv_output.writerow(value_list) 
    #Return the string value of the csv output 
    return csv_handle.getvalue() 

回答

30

下面是一些簡單的代碼,可以流式傳輸CSV;你或許可以去從這個到任何你需要做的:

import cStringIO as StringIO 
import csv 

def csv(request): 
    def data(): 
     for i in xrange(10): 
      csvfile = StringIO.StringIO() 
      csvwriter = csv.writer(csvfile) 
      csvwriter.writerow([i,"a","b","c"]) 
      yield csvfile.getvalue() 

    response = HttpResponse(data(), mimetype="text/csv") 
    response["Content-Disposition"] = "attachment; filename=test.csv" 
    return response 

這只是每一行寫入內存中的文件,讀取該行並得到它。

這個版本是用於產生批量數據更高效的,但要確保在使用它之前,瞭解上述:

import cStringIO as StringIO 
import csv 

def csv(request): 
    csvfile = StringIO.StringIO() 
    csvwriter = csv.writer(csvfile) 

    def read_and_flush(): 
     csvfile.seek(0) 
     data = csvfile.read() 
     csvfile.seek(0) 
     csvfile.truncate() 
     return data 

    def data(): 
     for i in xrange(10): 
      csvwriter.writerow([i,"a","b","c"]) 
     data = read_and_flush() 
     yield data 

    response = HttpResponse(data(), mimetype="text/csv") 
    response["Content-Disposition"] = "attachment; filename=test.csv" 
    return response 
+0

我還沒有流數據的需求,但很高興知道獲得簡單而優雅的東西有多快。 –

+0

雖然我很喜歡這個答案,但事實證明,這不是我的問題。我從字面上使用了你寫的確切代碼,只是爲了看看它是否會產生響應,但響應回來爲0字節。所以我仍然堅持相同的結果。 – bfrederix

+0

此代碼正常工作,因此您的環境出現問題,需要進行故障排除。 –

2

我是有問題是與ConditionalGetMiddleware。我看到Django的活塞拿出一個替代中間件爲ConditionalGetMiddleware允許流:從一個聰明的回答

from django.middleware.http import ConditionalGetMiddleware 

def compat_middleware_factory(klass): 
    """ 
    Class wrapper that only executes `process_response` 
    if `streaming` is not set on the `HttpResponse` object. 
    Django has a bad habbit of looking at the content, 
    which will prematurely exhaust the data source if we're 
    using generators or buffers. 
    """ 
    class compatwrapper(klass): 
     def process_response(self, req, resp): 
      if not hasattr(resp, 'streaming'): 
       return klass.process_response(self, req, resp) 
      return resp 
    return compatwrapper 

ConditionalMiddlewareCompatProxy = compat_middleware_factory(ConditionalGetMiddleware) 

,那麼你將與你的ConditionalMiddlewareCompatProxy中間件更換ConditionalGetMiddleware,並在您的視圖(借來的代碼這個問題):

def csv_view(request): 
    def data(): 
     for i in xrange(10): 
      csvfile = StringIO.StringIO() 
      csvwriter = csv.writer(csvfile) 
      csvwriter.writerow([i,"a","b","c"]) 
      yield csvfile.getvalue() 

    #create the reponse object with a csv mimetype 
    response = HttpResponse(
     data(), 
     mimetype='text/csv', 
     ) 
    #Set the response as an attachment with a filename 
    response['Content-Disposition'] = "attachment; filename=test.csv" 
    response.streaming = True 
    return response 
11

中間件的問題已經得到解決的Django的1.5和StreamingHttpResponse已經出臺。下面應該做的:

import cStringIO as StringIO 
import csv 

def csv_view(request): 
    ... 
    # Assume `rows` is an iterator or lists 
    def stream(): 
     buffer_ = StringIO.StringIO() 
     writer = csv.writer(buffer_) 
     for row in rows: 
      writer.writerow(row) 
      buffer_.seek(0) 
      data = buffer_.read() 
      buffer_.seek(0) 
      buffer_.truncate() 
      yield data 
    response = StreamingHttpResponse(
     stream(), content_type='text/csv' 
    ) 
    disposition = "attachment; filename=file.csv" 
    response['Content-Disposition'] = disposition 
    return response 

有上how to output csv from Django一些文檔,但它並沒有充分利用StreamingHttpResponse所以我繼續opened a ticket in order to track it