2011-02-28 19 views
17

Sto tentando di eseguire lo streaming di un file csv come download di allegati. I file CSV hanno dimensioni pari o superiori a 4 MB e ho bisogno di un modo per consentire all'utente di scaricare attivamente i file senza attendere che tutti i dati vengano creati e memorizzati prima sulla memoria.Streaming di un file CSV in Django

Ho usato per la prima volta il mio file wrapper basato sulla classe FileWrapper di Django. Questo fallì. Poi ho visto un metodo qui per l'utilizzo di un generatore per lo streaming la risposta: How to stream an HttpResponse with Django

Quando alzo un errore all'interno del generatore, posso vedere che sto creando i dati corretti con la funzione get_row_data(), ma quando provo a restituire la risposta torna vuota. Ho anche disabilitato il Django GZipMiddleware. Qualcuno sa cosa sto sbagliando?

Modifica: il problema riscontrato riguardava lo ConditionalGetMiddleware. Ho dovuto sostituirlo, il codice è in una risposta qui sotto.

Qui è la vista:

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 

Ecco il generatore che uso per lo streaming la risposta:

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) 

Ed ecco come creo i dati fila 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() 

risposta

30

Ecco un semplice codice che trasmetterà in streaming un CSV; probabilmente si può andare da questo a tutto ciò che dovete fare:

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 

Questo scrive semplicemente ogni riga in un file in memoria, si legge la riga e cede.

Questa versione è più efficiente per la generazione di dati di massa, ma essere sicuri di comprendere quanto sopra prima di utilizzarlo:

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

Non ho ancora avuto la necessità di eseguire lo streaming dei dati, ma è bello sapere quanto sia veloce ottenere qualcosa che sia semplice ed elegante. –

+0

Anche se mi piace molto questa risposta, si scopre che questo non è un mio problema. Ho letteralmente usato questo codice esatto che hai scritto, solo per vedere se avrebbe generato una risposta, ma la risposta ritorna come 0 byte. Quindi sono ancora bloccato con lo stesso risultato. – bfrederix

+0

Questo codice funziona bene, quindi c'è qualcosa di sbagliato nel tuo ambiente che dovrai risolvere. –

2

Il problema che stava avendo era col ConditionalGetMiddleware. Ho visto django-pistone venire con un middleware sostituzione per il ConditionalGetMiddleware che permette lo streaming:

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) 

Allora si sostituirà ConditionalGetMiddleware con il middleware ConditionalMiddlewareCompatProxy, e secondo lei (codice preso in prestito da una risposta intelligente a questa domanda):

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

Il problema è stato risolto middleware come di Django 1,5 e un StreamingHttpResponse è stato introdotto. Il seguente dovrebbe fare:

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 

C'è un po 'di documentazione su how to output csv from Django ma non approfitta del StreamingHttpResponse così sono andato avanti e opened a ticket in order to track it.