Czytanie plików CSV przy użyciu C #

169

Piszę prostą aplikację do importowania i muszę odczytać plik CSV, pokazać wynik w a DataGridi pokazać uszkodzone linie pliku CSV w innej siatce. Na przykład pokaż linie, które są krótsze niż 5 wartości w innej siatce. Próbuję to zrobić w ten sposób:

StreamReader sr = new StreamReader(FilePath);
importingData = new Account();
string line;
string[] row = new string [5];
while ((line = sr.ReadLine()) != null)
{
    row = line.Split(',');

    importingData.Add(new Transaction
    {
        Date = DateTime.Parse(row[0]),
        Reference = row[1],
        Description = row[2],
        Amount = decimal.Parse(row[3]),
        Category = (Category)Enum.Parse(typeof(Category), row[4])
    });
}

ale w tym przypadku bardzo trudno jest operować na tablicach. Czy jest lepszy sposób na podzielenie wartości?

ilkin
źródło
Dziękuję za rozwiązanie. Rozważ opublikowanie go jako postu z odpowiedzią - uwzględnienie go w pytaniu nie poprawia jego czytelności.
BartoszKP

Odpowiedzi:

363

Nie wynajduj koła na nowo. Skorzystaj z tego, co już jest w .NET BCL.

  • dodaj odniesienie do Microsoft.VisualBasic (tak, mówi VisualBasic, ale działa równie dobrze w C # - pamiętaj, że na końcu to wszystko jest po prostu IL)
  • użyj Microsoft.VisualBasic.FileIO.TextFieldParserklasy do przeanalizowania pliku CSV

Oto przykładowy kod:

using (TextFieldParser parser = new TextFieldParser(@"c:\temp\test.csv"))
{
    parser.TextFieldType = FieldType.Delimited;
    parser.SetDelimiters(",");
    while (!parser.EndOfData) 
    {
        //Processing row
        string[] fields = parser.ReadFields();
        foreach (string field in fields) 
        {
            //TODO: Process field
        }
    }
}

Świetnie sprawdza się w moich projektach C #.

Oto więcej linków / informacji:

David Pokluda
źródło
18
NAPRAWDĘ chciałbym, żeby istniał sposób, który nie korzystałby z bibliotek VB, ale to zadziałało idealnie! Dziękuję Ci!
gillonba
5
+1: Właśnie zepsułem czytnik Lumenworks Fast CSV na pliku 53Mb. Wygląda na to, że buforowanie wiersza nie powiodło się po 43 000 wierszy i zaszyfrowało bufor. Wypróbowałem VB TextFieldParseri zadziałało. Dzięki
Gone Coding
10
+1 Świetna odpowiedź, ponieważ wiele osób nie wie, że ta klasa istnieje. Przyszli widzowie powinni zwrócić uwagę na to, że ustawienie parser.TextFieldType = FieldType.Delimited;nie jest konieczne w przypadku wywołania parser.SetDelimiters(",");, ponieważ metoda ustawia TextFieldTypewłaściwość za Ciebie.
Brian
10
Zobacz również: dotnetperls.com/textfieldparser . TextFieldParser ma gorszą wydajność niż String.Split i StreamReader. Istnieje jednak duża różnica między string.Split i TextFieldParser. TextFieldParser radzi sobie z dziwnymi przypadkami, takimi jak przecinek w kolumnie: możesz nazwać kolumnę, na przykład "text with quote"", and comma", i uzyskać poprawną wartość text with quote", and commazamiast błędnie rozdzielonych wartości. Więc możesz zdecydować się na String.Split, jeśli csv jest bardzo proste.
Yongwei Wu,
5
Zauważ, że może być konieczne dodanie odwołania do Microsoft.VisualBasic, aby z tego skorzystać. Kliknij prawym przyciskiem myszy projekt w programie Visual Studio, a następnie wybierz opcję Dodaj> Odniesienie i zaznacz pole wyboru Microsoft.VisualBasic.
Derek Kurth
37

Z mojego doświadczenia wynika, że ​​istnieje wiele różnych formatów CSV. W szczególności, jak radzą sobie ze ucieczką cudzysłowów i ograniczników w polu.

Oto warianty, z którymi się spotkałem:

  • cudzysłowy są cytowane i podwajane (excel), np. 15 "-> pole1," 15 "" ", pole3
  • cudzysłowy nie są zmieniane, chyba że pole jest cytowane z innego powodu. czyli 15 "-> pole1,15", pola3
  • cudzysłowy są poprzedzane \. tj. 15 "-> pole1," 15 \ "", pole3
  • cudzysłowy nie są w ogóle zmieniane (nie zawsze jest to możliwe do poprawnego przeanalizowania)
  • separator jest cytowany (excel). czyli a, b -> pole1, "a, b", pole3
  • separator jest poprzedzony znakiem \. czyli a, b -> pole1, a \, b, pole3

Wypróbowałem wiele istniejących parserów csv, ale nie ma ani jednego, który mógłby obsłużyć warianty, z którymi się spotkałem. Trudno jest również dowiedzieć się z dokumentacji, które uciekające warianty obsługują parsery.

W moich projektach używam teraz VB TextFieldParser lub niestandardowego rozdzielacza.

adrianm
źródło
1
Uwielbiam tę odpowiedź za dostarczone przez Ciebie przypadki testowe!
Matthew Rodatus
2
Główny problem polega na tym, że większość implementacji nie przejmuje się RFC 4180, który opisuje format CSV i sposób ucieczki ograniczników.
Jenny O'Reilly
RFC-4180 pochodzi z 2005 roku, co wydaje się teraz stare, ale pamiętaj: struktura .Net została wydana po raz pierwszy w 2001 roku. Poza tym specyfikacje RFC nie zawsze są oficjalnymi standardami iw tym przypadku nie mają takiej samej wagi jak, powiedzmy , ISO-8601 lub RFC-761.
Joel Coehoorn
23

Polecam CsvHelper firmy Nuget .

(Dodanie odniesienia do Microsoft.VisualBasic po prostu nie wydaje się właściwe, jest nie tylko brzydkie, prawdopodobnie nie jest nawet wieloplatformowe).

knocte
źródło
2
Jest dokładnie tak wieloplatformowy jak C #.
PRMan,
źle, Microsoft.VisualBasic.dll w Linuksie pochodzi ze źródeł Mono, które ma inną implementację niż Microsoft i jest kilka rzeczy, które nie są zaimplementowane, na przykład: stackoverflow.com/questions/6644165/ ...
knocte
(Ponadto język VB nigdy nie był przedmiotem zainteresowania firm, które były zaangażowane w tworzenie / rozwijanie projektu Mono, więc jest daleko w tyle pod względem wysiłków w porównaniu z ekosystemem / narzędziami C #.)
knocte,
1
Po zabawie z obydwoma dodałbym, że CsvHelperma wbudowany wiersz do mapowania klas; dopuszcza różnice w nagłówkach kolumn (jeśli są obecne), a nawet pozorne różnice w kolejności kolumn (chociaż sam nie testowałem tego ostatniego). W sumie wydaje się to o wiele bardziej „wysokiego poziomu” niż TextFieldParser.
David,
1
Tak, przestrzeń nazw Microsoft.VisualBasic nie jest dostępna w .NET Core 2.1
N4ppeL
13

Czasami korzystanie z bibliotek jest fajne, gdy nie chcesz wymyślać koła na nowo, ale w tym przypadku można wykonać tę samą pracę z mniejszą liczbą wierszy kodu i łatwiejszym do odczytania w porównaniu do korzystania z bibliotek. Oto inne podejście, które uważam za bardzo łatwe w użyciu.

  1. W tym przykładzie używam StreamReader do odczytu pliku
  2. Regex, aby wykryć separator z każdego wiersza (linii).
  3. Tablica do zbierania kolumn od indeksu 0 do n

using (StreamReader reader = new StreamReader(fileName))
    {
        string line; 

        while ((line = reader.ReadLine()) != null)
        {
            //Define pattern
            Regex CSVParser = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");

            //Separating columns to array
            string[] X = CSVParser.Split(line);

            /* Do something with X */
        }
    }
Mana
źródło
4
Z pewnością ma to problem z danymi, które same zawierają nowe wiersze?
Doogal
Teraz pliki danych CSV nie są znane z tego, że zawierają puste wiersze między danymi, ale jeśli masz źródło, które to robi, w takim przypadku po prostu wykonałbym prosty test wyrażenia regularnego, aby usunąć spacje lub linie nie zawierające niczego przed uruchomieniem czytnika. sprawdź tutaj różne przykłady: stackoverflow.com/questions/7647716/…
Mana
1
Z pewnością podejście oparte na znakach jest bardziej naturalne w przypadku tego rodzaju problemu niż wyrażenie regularne. W zależności od obecności cudzysłowów zachowanie ma być inne.
Casey
6

CSV można uzyskać skomplikowane prawdziwe szybko.

Użyj czegoś solidnego i dobrze przetestowanego:
FileHelpers: www.filehelpers.net

FileHelpers to bezpłatna i łatwa w użyciu biblioteka .NET do importowania / eksportowania danych z rekordów o stałej długości lub rozdzielonych w plikach, ciągach lub strumieniach.

Keith Blows
źródło
5
Myślę, że FileHelper stara się zrobić wiele za jednym razem. Analizowanie plików jest procesem dwuetapowym, w którym najpierw dzieli się wiersze na pola, a następnie analizuje je na dane. Połączenie funkcji utrudnia obsługę takich rzeczy, jak wzorzec-szczegół i filtrowanie linii.
adrianm
4

Kolejny na tej liście, Cinchoo ETL - biblioteka open source do odczytu i zapisu plików CSV

Przykładowy plik CSV poniżej

Id, Name
1, Tom
2, Mark

Szybko możesz je załadować za pomocą biblioteki, jak poniżej

using (var reader = new ChoCSVReader("test.csv").WithFirstLineHeader())
{
   foreach (dynamic item in reader)
   {
      Console.WriteLine(item.Id);
      Console.WriteLine(item.Name);
   }
}

Jeśli masz klasę POCO pasującą do pliku CSV

public class Employee
{
   public int Id { get; set; }
   public string Name { get; set; }
}

Możesz go użyć do załadowania pliku CSV, jak poniżej

using (var reader = new ChoCSVReader<Employee>("test.csv").WithFirstLineHeader())
{
   foreach (var item in reader)
   {
      Console.WriteLine(item.Id);
      Console.WriteLine(item.Name);
   }
}

Sprawdź artykuły w CodeProject jak z niego korzystać.

Zastrzeżenie: jestem autorem tej biblioteki

RajN
źródło
Cześć, czy możesz załadować csv do tabeli Sql - nie znam wcześniej nagłówka w tabeli CSV. Po prostu odzwierciedlaj zawartość tabeli csv w tabeli Sql
agresja
Tak, możesz. proszę zobaczyć ten link stackoverflow.com/questions/20759302/…
RajN
2
private static DataTable ConvertCSVtoDataTable(string strFilePath)
        {
            DataTable dt = new DataTable();
            using (StreamReader sr = new StreamReader(strFilePath))
            {
                string[] headers = sr.ReadLine().Split(',');
                foreach (string header in headers)
                {
                    dt.Columns.Add(header);
                }
                while (!sr.EndOfStream)
                {
                    string[] rows = sr.ReadLine().Split(',');
                    DataRow dr = dt.NewRow();
                    for (int i = 0; i < headers.Length; i++)
                    {
                        dr[i] = rows[i];
                    }
                    dt.Rows.Add(dr);
                }

            }

            return dt;
        }

        private static void WriteToDb(DataTable dt)
        {
            string connectionString =
                "Data Source=localhost;" +
                "Initial Catalog=Northwind;" +
                "Integrated Security=SSPI;";

            using (SqlConnection con = new SqlConnection(connectionString))
                {
                    using (SqlCommand cmd = new SqlCommand("spInsertTest", con))
                    {
                        cmd.CommandType = CommandType.StoredProcedure;

                        cmd.Parameters.Add("@policyID", SqlDbType.Int).Value = 12;
                        cmd.Parameters.Add("@statecode", SqlDbType.VarChar).Value = "blagh2";
                        cmd.Parameters.Add("@county", SqlDbType.VarChar).Value = "blagh3";

                        con.Open();
                        cmd.ExecuteNonQuery();
                    }
                }

         }
anongf4gsdfg54564533
źródło
skąd skopiowałeś to rozwiązanie?
MindRoasterMir
0

Przede wszystkim musisz zrozumieć, czym jest CSV i jak go napisać.

  1. Każdy następny ciąg ( /r/n) to kolejny wiersz „tabeli”.
  2. Komórki „Tabela” są oddzielone pewnym symbolem separatora. Najczęściej używanymi symbolami jest \tlub,
  3. Każda komórka może zawierać ten symbol separatora (komórka musi zaczynać się od symbolu cudzysłowu i kończyć się tym symbolem w tym przypadku)
  4. Każda komórka może zawierać /r/nsymbole (komórka musi zaczynać się od symbolu cudzysłowu i kończy się tym symbolem w tym przypadku)

Najłatwiejszym sposobem pracy C # / Visual Basic z plikami CSV jest użycie Microsoft.VisualBasicbiblioteki standardowej . Musisz tylko dodać potrzebne odniesienie i następujący ciąg do swojej klasy:

using Microsoft.VisualBasic.FileIO;

Tak, możesz go używać w C #, nie martw się. Ta biblioteka może czytać stosunkowo duże pliki i obsługuje wszystkie potrzebne reguły, więc będziesz mógł pracować ze wszystkimi plikami CSV.

Jakiś czas temu na podstawie tej biblioteki napisałem prostą klasę do odczytu / zapisu CSV. Używając tej prostej klasy będziesz w stanie pracować z CSV jak z tablicą 2 wymiarów. Możesz znaleźć moją klasę pod następującym linkiem: https://github.com/ukushu/DataExporter

Prosty przykład użycia:

Csv csv = new Csv("\t");//delimiter symbol

csv.FileOpen("c:\\file1.csv");

var row1Cell6Value = csv.Rows[0][5];

csv.AddRow("asdf","asdffffff","5")

csv.FileSave("c:\\file2.csv");
Andrzej
źródło
0

Aby uzupełnić poprzednie odpowiedzi, można potrzebować kolekcji obiektów z jego pliku CSV, przeanalizowanych albo metodą TextFieldParseralbo string.Split, a następnie każdej linii przekonwertowanej na obiekt przez odbicie. Oczywiście najpierw musisz zdefiniować klasę, która pasuje do wierszy pliku CSV.

Użyłem prostego Serializatora CSV od Michaela Kropata, który znalazł tutaj: Klasa ogólna do CSV (wszystkie właściwości) i ponownie użyłem jego metod, aby uzyskać pola i właściwości żądanej klasy.

Deserializuję plik CSV za pomocą następującej metody:

public static IEnumerable<T> ReadCsvFileTextFieldParser<T>(string fileFullPath, string delimiter = ";") where T : new()
{
    if (!File.Exists(fileFullPath))
    {
        return null;
    }

    var list = new List<T>();
    var csvFields = GetAllFieldOfClass<T>();
    var fieldDict = new Dictionary<int, MemberInfo>();

    using (TextFieldParser parser = new TextFieldParser(fileFullPath))
    {
        parser.SetDelimiters(delimiter);

        bool headerParsed = false;

        while (!parser.EndOfData)
        {
            //Processing row
            string[] rowFields = parser.ReadFields();
            if (!headerParsed)
            {
                for (int i = 0; i < rowFields.Length; i++)
                {
                    // First row shall be the header!
                    var csvField = csvFields.Where(f => f.Name == rowFields[i]).FirstOrDefault();
                    if (csvField != null)
                    {
                        fieldDict.Add(i, csvField);
                    }
                }
                headerParsed = true;
            }
            else
            {
                T newObj = new T();
                for (int i = 0; i < rowFields.Length; i++)
                {
                    var csvFied = fieldDict[i];
                    var record = rowFields[i];

                    if (csvFied is FieldInfo)
                    {
                        ((FieldInfo)csvFied).SetValue(newObj, record);
                    }
                    else if (csvFied is PropertyInfo)
                    {
                        var pi = (PropertyInfo)csvFied;
                        pi.SetValue(newObj, Convert.ChangeType(record, pi.PropertyType), null);
                    }
                    else
                    {
                        throw new Exception("Unhandled case.");
                    }
                }
                if (newObj != null)
                {
                    list.Add(newObj);
                }
            }
        }
    }
    return list;
}

public static IEnumerable<MemberInfo> GetAllFieldOfClass<T>()
{
    return
        from mi in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
        where new[] { MemberTypes.Field, MemberTypes.Property }.Contains(mi.MemberType)
        let orderAttr = (ColumnOrderAttribute)Attribute.GetCustomAttribute(mi, typeof(ColumnOrderAttribute))
        orderby orderAttr == null ? int.MaxValue : orderAttr.Order, mi.Name
        select mi;            
}
EricBDev
źródło
0

Gorąco polecam użycie CsvHelper.

Oto krótki przykład:

public class csvExampleClass
{
    public string Id { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
}

var items = DeserializeCsvFile<List<csvExampleClass>>( csvText );

public static List<T> DeserializeCsvFile<T>(string text)
{
    CsvReader csv = new CsvReader( new StringReader( text ) );
    csv.Configuration.Delimiter = ",";
    csv.Configuration.HeaderValidated = null;
    csv.Configuration.MissingFieldFound = null;
    return (List<T>)csv.GetRecords<T>();
}

Pełną dokumentację można znaleźć pod adresem : https://joshclose.github.io/CsvHelper

Kieran
źródło