2008-09-04 12 views
18

Questo è quello che ho. Funziona. Ma c'è un modo più semplice o migliore?Come si genera al meglio un CSV (file di testo delimitato da virgole) per il download con ASP.NET?

Uno una pagina ASPX, ho il link di download ...

<asp:HyperLink ID="HyperLinkDownload" runat="server" NavigateUrl="~/Download.aspx">Download as CSV file</asp:HyperLink> 

E poi ho il codice Download.aspx.vb Dietro ...

Public Partial Class Download 
    Inherits System.Web.UI.Page 

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 
     'set header 
     Response.Clear() 
     Response.ContentType = "text/csv" 
     Dim FileName As String = "books.csv" 
     Response.AppendHeader("Content-Disposition", "attachment;filename=" + FileName) 

     'generate file content 
     Dim db As New bookDevelopmentDataContext 
     Dim Allbooks = From b In db.books _ 
         Order By b.Added _ 
         Select b 
     Dim CsvFile As New StringBuilder 
     CsvFile.AppendLine(CsvHeader()) 
     For Each b As Book In Allbooks 
      CsvFile.AppendLine(bookString(b)) 
     Next 

     'write the file 
     Response.Write(CsvFile.ToString) 
     Response.End() 
    End Sub 

    Function CsvHeader() As String 
     Dim CsvLine As New StringBuilder 
     CsvLine.Append("Published,") 
     CsvLine.Append("Title,") 
     CsvLine.Append("Author,") 
     CsvLine.Append("Price") 
     Return CsvLine.ToString 
    End Function 

    Function bookString(ByVal b As Book) As String 
     Dim CsvLine As New StringBuilder 
     CsvLine.Append(b.Published.ToShortDateString + ",") 
     CsvLine.Append(b.Title.Replace(",", "") + ",") 
     CsvLine.Append(b.Author.Replace(",", "") + ",") 
     CsvLine.Append(Format(b.Price, "c").Replace(",", "")) 
     Return CsvLine.ToString 
    End Function 

End Class 

risposta

22

La formattazione CSV ha alcuni trucchi. Ti sei posto queste domande:

  • Qualcuno dei miei dati ha una virgola incorporata?
  • Qualcuno dei miei dati ha incorporato virgolette doppie?
  • Qualcuno dei miei dati ha una nuova riga?
  • Devo supportare le stringhe Unicode?

Vedo diversi problemi nel codice in alto. La cosa virgola prima di tutto ... si sta stripping virgole:

CsvLine.Append(Format(b.Price, "c").Replace(",", "")) 

Perché? In CSV, si dovrebbe essere tutto ciò che circonda, che ha le virgole con le citazioni:

CsvLine.Append(String.Format("\"{0:c}\"", b.Price)) 

(o qualcosa del genere ... il mio VB non è molto buono). Se non sei sicuro se ci sono delle virgole, ma metti le virgolette in giro. Se nella stringa sono presenti virgolette, è necessario sfuggirle raddoppiandole. " diventa "".

b.Title.Replace("\"", "\"\"") 

Quindi, racchiuderlo tra virgolette se lo si desidera. Se nella stringa sono presenti dei newline, è necessario circondare la stringa con le virgolette ... sì, i newline letterali sono consentiti nei file CSV. Sembra strano agli umani, ma va tutto bene.

Un buon scrittore CSV richiede un po 'di riflessione. Un buon lettore CSV (parser) è semplicemente semplice (e no, regex non è abbastanza buono per analizzare CSV ... ti farà solo il 95% circa del modo in cui ci sono).

E poi c'è Unicode ... o più in generale problemi I18N (Internazionalizzazione). Ad esempio, stai rimuovendo le virgole da un prezzo formattato. Ma supponendo che il prezzo sia formattato come ci si aspetta negli Stati Uniti. In Francia, la formattazione del numero è invertita (periodi utilizzati al posto di virgole e vice versa). In conclusione, usa la formattazione indipendente dalla cultura, laddove possibile.

Mentre il problema qui è generando CSV, inevitabilmente sarà necessario analizzare CSV. In .NET, il miglior parser che ho trovato (gratuito) è Fast CSV Reader su CodeProject. L'ho effettivamente usato nel codice di produzione ed è veramente molto veloce e molto facile da usare!

+0

I regex vanno bene - il fatto che le virgolette che circondano un campo possono essere considerati parte del delimitatore consentono un modello di ripetizione. La chiave più importante è assicurarsi di avere una linea completa, che puoi ottenere contando le virgolette. Forse questo è ciò che intendevi per il 95%? –

8

mi passano tutti i miei dati CSV attraverso una funzione come questa:

Function PrepForCSV(ByVal value As String) As String 
    return String.Format("""{0}""", Value.Replace("""", """""")) 
End Function 

Inoltre, se non stai servendo html probabilmente vuole un gestore HTTP (.come h x file) piuttosto che una pagina web completa. Se crei un nuovo gestore in Visual Studio, potresti semplicemente copiare il codice esistente nel metodo principale e funzionerà, con un piccolo incremento delle prestazioni per i tuoi sforzi.

1

Oltre a ciò che ha detto Simon, è possibile leggere lo CSV how-to guide e assicurarsi che l'output non venga eseguito su nessuno dei trucchi.

Per chiarire qualcosa Simon ha detto:

Poi circondano questo tra virgolette se si desidera

campi che contengono piegato in due doppi apici ("") dovranno essere completamente circondato da doppi apici . Non dovrebbe esserci alcun danno nel solo avvolgere tutti i campi con virgolette doppie, a meno che non si voglia specificamente che il parser rimuova gli spazi bianchi iniziali e finali (invece di tagliarli da soli).

3

Se si desidera un convertitore del valore delimitato da due punti, esiste un'origine open source di terze parti denominata FileHelpers. Non sono sicuro di quale licenza open source sia sotto, ma mi ha aiutato molto.

2

C'è un sovraccarico associato alla classe Page. Dato che stai solo sputando un file CSV e non hai bisogno di postback, controlli server, cache o il resto, dovresti renderlo un gestore con estensione .ashx. See here.

4

È possibile creare l'equivalente di bookString() nella query stessa. Ecco quello che penso sarebbe un modo più semplice.

protected void Page_Load(object sender, EventArgs e) 
{ 
    using (var db = new bookDevelopmentDataContext()) 
    { 
     string fileName = "book.csv"; 
     var q = from b in db.books 
       select string.Format("{0:d},\"{1}\",\"{2}\",{3:F2}", b.Published, b.Title.Replace("\"", "\"\""), b.Author.Replace("\"", "\"\""), t.price); 

     string outstring = string.Join(",", q.ToArray()); 

     Response.Clear(); 
     Response.ClearHeaders(); 
     Response.ContentType = "text/csv"; 
     Response.AppendHeader("Content-Disposition", string.Format("attachment;filename={0}", fileName)); 
     Response.Write("Published,Title,Author,Price," + outstring); 
     Response.End(); 
    } 
} 
+0

grazie per il post, ha aiutato a risolvere un problema con firefox [qui] (http://stackoverflow.com/a/32373057/2218697) è la soluzione – stom

1

Uso il seguente metodo quando si crea un file CSV da un DataTable. ControllerContext è solo l'oggetto flusso di risposta in cui è scritto il file. Per te sarà l'oggetto di risposta.

public override void ExecuteResult(ControllerContext context) 
     { 
      StringBuilder csv = new StringBuilder(10 * Table.Rows.Count * Table.Columns.Count); 

      for (int c = 0; c < Table.Columns.Count; c++) 
      { 
       if (c > 0) 
        csv.Append(","); 
       DataColumn dc = Table.Columns[c]; 
       string columnTitleCleaned = CleanCSVString(dc.ColumnName); 
       csv.Append(columnTitleCleaned); 
      } 
      csv.Append(Environment.NewLine); 
      foreach (DataRow dr in Table.Rows) 
      { 
       StringBuilder csvRow = new StringBuilder(); 
       for(int c = 0; c < Table.Columns.Count; c++) 
       { 
        if(c != 0) 
         csvRow.Append(","); 

        object columnValue = dr[c]; 
        if (columnValue == null) 
         csvRow.Append(""); 
        else 
        { 
         string columnStringValue = columnValue.ToString(); 


         string cleanedColumnValue = CleanCSVString(columnStringValue); 

         if (columnValue.GetType() == typeof(string) && !columnStringValue.Contains(",")) 
         { 
          cleanedColumnValue = "=" + cleanedColumnValue; // Prevents a number stored in a string from being shown as 8888E+24 in Excel. Example use is the AccountNum field in CI that looks like a number but is really a string. 
         } 
         csvRow.Append(cleanedColumnValue); 
        } 
       } 
       csv.AppendLine(csvRow.ToString()); 
      } 

      HttpResponseBase response = context.HttpContext.Response; 
      response.ContentType = "text/csv"; 
      response.AppendHeader("Content-Disposition", "attachment;filename=" + this.FileName); 
      response.Write(csv.ToString()); 
     } 

     protected string CleanCSVString(string input) 
     { 
      string output = "\"" + input.Replace("\"", "\"\"").Replace("\r\n", " ").Replace("\r", " ").Replace("\n", "") + "\""; 
      return output; 
     } 
1

Guardando per lo più buona, tranne nella funzione "BookString()" si dovrebbe passare tutte quelle stringhe attraverso una piccola funzione come questa:

Private Function formatForCSV(stringToProcess As String) As String 
    If stringToProcess.Contains("""") Or stringToProcess.Contains(",") Then 
     stringToProcess = String.Format("""{0}""", stringToProcess.Replace("""", """""")) 
    End If 
    Return stringToProcess 
End Function 

'So, lines like this: 
CsvLine.Append(b.Title.Replace(",", "") + ",") 
'would be lines like this instead: 
CsvLine.Append(formatForCSV(b.Title)) + ",") 

La funzione formattare le stringhe bene per CSV. Sostituisce le virgolette con virgolette doppie e aggiunge virgolette sulla stringa se nella stringa sono presenti virgolette o virgolette.

Nota che non tiene conto delle nuove righe, ma può garantire in modo sicuro un buon output CSV per quelle stringhe che si conoscono prive di newline (input da semplici moduli di testo a una riga, ecc.).