HOWTO · Csharp

C# legge un file CSV e archivia i suoi valori in un array

Questo articolo illustrativo mostra diversi metodi per leggere un file CSV e archiviarne i valori in un array in C#. Introduce metodi come OpenRead(), ReadLine() e TextFieldParser

In questa pagina

Un file CSV è un file separato da virgole, utilizzato per memorizzare i dati in modo organizzato. Di solito memorizza i dati in forma tabulare. La maggior parte delle organizzazioni aziendali memorizza i propri dati in file CSV.

In C#, possiamo eseguire varie operazioni su un file CSV. Possiamo leggere, scrivere e modificare un file CSV. Questo articolo si concentra sulla lettura di un file CSV e sulla memorizzazione dei suoi valori in un array: leggere le righe una alla volta con StreamReader, caricare tutte le righe in una volta sola con File.ReadAllLines() e analizzare i campi con TextFieldParser.

I primi tre esempi utilizzano lo stesso file di esempio Data.csv:

Name,Age
Alice,30
Bob,25

Leggere un file CSV con StreamReader

La classe StreamReader legge un file come un flusso di testo. File.OpenRead() apre il file e ReadLine() restituisce una riga alla volta fino a raggiungere EndOfStream. Dividendo ogni riga sulla virgola si ottengono i singoli valori, che l’esempio memorizza in un elenco per colonna.

La sintassi corretta per utilizzare i metodi OpenRead() e ReadLine() è la seguente:

// OpenRead() Syntax
File.OpenRead(@"FilePath");
// ReadLine() Syntax
StreamReaderObject.ReadLine();

Codice di esempio:

using System.IO;
using System.Collections.Generic;
using System;

class ReadingCSV {
  static void Main(string[] args) {
    var reader = new StreamReader(File.OpenRead(@"D:\New folder\Data.csv"));
    List<string> listA = new List<string>();
    List<string> listB = new List<string>();
    while (!reader.EndOfStream) {
      var line = reader.ReadLine();
      var values = line.Split(',');

      listA.Add(values[0]);
      listB.Add(values[1]);
    }
    foreach (var column1 in listA) {
      Console.WriteLine(column1);
    }
    foreach (var column2 in listB) {
      Console.WriteLine(column2);
    }
  }
}

Uscita:

Name
Alice
Bob
Age
30
25

La divisione sulla virgola funziona quando nessun campo contiene esso stesso una virgola. Un campo tra virgolette come "Smith, John" verrebbe diviso nel punto sbagliato; per file di questo tipo utilizza TextFieldParser.

Leggere tutte le righe in una volta sola con File.ReadAllLines

Quando il file è abbastanza piccolo da stare in memoria, File.ReadAllLines() è la via più breve verso un array: legge l’intero file e restituisce ogni riga come elemento di un string[]. L’esempio seguente divide ogni riga, memorizza ogni riga di dati come proprio string[] e converte l’elenco delle righe in un array frastagliato con ToArray(), in modo che data[i] sia la riga completa i. Il ciclo inizia dall’indice 1 per saltare la riga di intestazione, salta le righe vuote e controlla il numero di campi prima dell’indicizzazione, così una riga corta o vuota non può mandarlo in errore.

Codice di esempio:

using System;
using System.Collections.Generic;
using System.IO;

class ReadingAllLines {
  static void Main() {
    string[] lines = File.ReadAllLines("Data.csv");
    List<string[]> rows = new List<string[]>();
    // Start at index 1 to skip the header row.
    for (int i = 1; i < lines.Length; i++) {
      if (string.IsNullOrWhiteSpace(lines[i])) {
        continue;
      }
      string[] values = lines[i].Split(',');
      if (values.Length < 2) {
        continue;
      }
      rows.Add(values);
    }
    string[][] data = rows.ToArray();
    foreach (string[] row in data) {
      Console.WriteLine(row[0] + ": " + row[1]);
    }
  }
}

Uscita:

Alice: 30
Bob: 25

Il compromesso è memoria contro semplicità. ReadAllLines() carica l’intero file prima che la prima riga venga elaborata e restituisce un array che puoi indicizzare, contare e rileggere subito. Il ciclo con StreamReader conserva una sola riga alla volta, quindi resta la scelta più sicura per file molto grandi le cui righe elabori una volta e poi scarti. Per i file piccoli che consulti più di una volta, caricare tutto in una volta sola è più semplice e abbastanza veloce.

Analizzare i campi CSV con TextFieldParser

La classe TextFieldParser è definita nello spazio dei nomi Microsoft.VisualBasic.FileIO. Analizza un file delimitato campo per campo e comprende i campi tra virgolette. Imposta i delimitatori con SetDelimiters(), quindi chiama ReadFields() per ottenere i campi della riga corrente come array di stringhe; l’analizzatore passa alla riga successiva a ogni chiamata. In un progetto .NET Framework, aggiungi un riferimento a Microsoft.VisualBasic prima di eseguire il programma seguente; nel .NET moderno, lo spazio dei nomi fa parte del framework condiviso.

La sintassi corretta per utilizzare questo analizzatore è la seguente:

TextFieldParser ParserName = new TextFieldParser(PathString);

Codice di esempio:

using System;
using System.Collections.Generic;
using Microsoft.VisualBasic.FileIO;

class ReadingCSV {
  public static void Main() {
    var path = @"D:\New folder\Data.csv";
    List<string> listA = new List<string>();
    List<string> listB = new List<string>();
    using (TextFieldParser csvReader = new TextFieldParser(path)) {
      csvReader.CommentTokens = new string[] { "#" };
      csvReader.SetDelimiters(new string[] { "," });
      csvReader.HasFieldsEnclosedInQuotes = true;

      // Skip the row with the column names
      csvReader.ReadLine();

      while (!csvReader.EndOfData) {
        // Read current line fields, pointer moves to the next line.
        string[] fields = csvReader.ReadFields();
        listA.Add(fields[0]);
        listB.Add(fields[1]);
      }
    }
    foreach (var column1 in listA) {
      Console.WriteLine(column1);
    }
    foreach (var column2 in listB) {
      Console.WriteLine(column2);
    }
  }
}

Uscita:

Alice
Bob
30
25

Questa volta la riga di intestazione viene saltata con il ReadLine() iniziale, e l’analizzatore, non una divisione manuale, decide dove termina ogni campo, quindi una virgola tra virgolette all’interno di un campo rimane intatta.

Gestire i campi tra virgolette e le virgolette di escape

Le esportazioni reali mettono i campi tra virgolette. Un campo che contiene il delimitatore viene racchiuso tra virgolette doppie, e una virgoletta dentro un campo tra virgolette viene scritta due volte. Il file di esempio People.csv seguente usa entrambe le regole:

Name,Note
"Smith, John","Likes ""quoted"" text"
Jones,Plain note

Split(',') non conosce queste regole: taglia la prima riga di dati in 3 pezzi in corrispondenza della virgola dentro il nome. TextFieldParser le applica: rimuove le virgolette che racchiudono il campo e unisce ogni virgoletta raddoppiata in una sola, così i campi tornano esattamente come erano previsti. Il programma stampa prima il numero di pezzi di Split(',') e poi i campi analizzati.

Codice di esempio:

using System;
using Microsoft.VisualBasic.FileIO;

class ReadingQuotedCsv {
  public static void Main() {
    string rawLine = "\"Smith, John\",\"Likes \"\"quoted\"\" text\"";
    Console.WriteLine("Split(',') pieces: " + rawLine.Split(',').Length);
    using (TextFieldParser parser = new TextFieldParser("People.csv")) {
      parser.SetDelimiters(new string[] { "," });
      parser.HasFieldsEnclosedInQuotes = true;

      // Skip the row with the column names
      parser.ReadLine();

      while (!parser.EndOfData) {
        string[] fields = parser.ReadFields();
        Console.WriteLine(fields[0] + " | " + fields[1]);
      }
    }
  }
}

Uscita:

Split(',') pieces: 3
Smith, John | Likes "quoted" text
Jones | Plain note

Se una divisione scritta a mano produce più campi di quante colonne ha l’intestazione, la causa abituale sono i campi tra virgolette. Per quel file passa a TextFieldParser invece di cercare di riparare i pezzi in un secondo momento.

Errori comuni nella lettura di un file CSV

  • IndexOutOfRangeException su values[1]: una riga finale vuota o una riga a cui manca una colonna si divide in meno campi di quanti il codice si aspetti. Salta le righe di soli spazi e controlla il numero di campi prima dell’indicizzazione, come fa l’esempio con ReadAllLines().

  • FileNotFoundException o DirectoryNotFoundException: il percorso non esiste sulla macchina che esegue il programma. Un percorso assoluto scritto nel codice come D:\New folder\Data.csv funziona solo sul computer dell’autore; preferisci un percorso relativo o costruisci il percorso con Path.Combine(), e verificalo con File.Exists() durante il debug.

  • La riga di intestazione compare tra i dati: il primo esempio con StreamReader memorizza Name e Age insieme alle righe reali. Quando un file ha un’intestazione, salta la prima riga prima del ciclo, come fanno gli altri esempi.

  • Il file resta bloccato: un StreamReader che non viene mai chiuso mantiene il suo handle finché non passa il garbage collector. Dichiara il lettore in un blocco using in modo che il file venga chiuso appena il ciclo finisce, anche quando un’eccezione lo interrompe.