Jaki jest najlepszy sposób importowania pliku CSV do struktury danych o jednoznacznie określonym typie?
Jaki jest najlepszy sposób importowania pliku CSV do struktury danych o jednoznacznie określonym typie?
Odpowiedzi:
Microsoft TextFieldParser jest stabilny i zgodny z RFC 4180 dla plików CSV. Nie zniechęcaj się Microsoft.VisualBasic
przestrzenią nazw; jest to standardowy komponent w .NET Framework, wystarczy dodać odwołanie do Microsoft.VisualBasic
zestawu globalnego .
Jeśli kompilujesz dla systemu Windows (w przeciwieństwie do Mono) i nie przewidujesz konieczności analizowania „uszkodzonych” (niezgodnych z RFC) plików CSV, to byłby to oczywisty wybór, ponieważ jest darmowy, nieograniczony, stabilny, i aktywnie wspierane, o większości których nie można powiedzieć o FileHelpers.
Zobacz także: Porady: odczytywanie z plików tekstowych rozdzielanych przecinkami w języku Visual Basic, aby zapoznać się z przykładem kodu VB.
TextFieldParser
Praca będzie rozdzielany tabulatorami i inne dziwne Excel generowanego cruft też. Zdaję sobie sprawę, że Twoja poprzednia odpowiedź nie wskazywała, że biblioteka jest specyficzna dla VB, po prostu przyszła mi do głowy jako sugestia, że była naprawdę przeznaczona dla VB i nie jest przeznaczona do użycia z C #, co nie wydaje mi się przypadek - w MSVB jest kilka naprawdę przydatnych klas.
Użyj połączenia OleDB.
String sConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\InputDirectory\\;Extended Properties='text;HDR=Yes;FMT=Delimited'";
OleDbConnection objConn = new OleDbConnection(sConnectionString);
objConn.Open();
DataTable dt = new DataTable();
OleDbCommand objCmdSelect = new OleDbCommand("SELECT * FROM file.csv", objConn);
OleDbDataAdapter objAdapter1 = new OleDbDataAdapter();
objAdapter1.SelectCommand = objCmdSelect;
objAdapter1.Fill(dt);
objConn.Close();
Jeśli spodziewasz się dość złożonych scenariuszy analizowania CSV, nawet nie myśl o toczeniu naszego własnego parsera . Istnieje wiele doskonałych narzędzi, takich jak FileHelpers , a nawet te z CodeProject .
Chodzi o to, że jest to dość powszechny problem i można się założyć, że wielu programistów już przemyślało i rozwiązało ten problem.
Brian daje fajne rozwiązanie do konwersji go na kolekcję silnie wpisaną.
Większość podanych metod analizy CSV nie bierze pod uwagę pól ucieczki ani niektórych innych subtelności plików CSV (takich jak pola przycinania). Oto kod, którego osobiście używam. Jest trochę szorstki na krawędziach i prawie nie ma raportowania błędów.
public static IList<IList<string>> Parse(string content)
{
IList<IList<string>> records = new List<IList<string>>();
StringReader stringReader = new StringReader(content);
bool inQoutedString = false;
IList<string> record = new List<string>();
StringBuilder fieldBuilder = new StringBuilder();
while (stringReader.Peek() != -1)
{
char readChar = (char)stringReader.Read();
if (readChar == '\n' || (readChar == '\r' && stringReader.Peek() == '\n'))
{
// If it's a \r\n combo consume the \n part and throw it away.
if (readChar == '\r')
{
stringReader.Read();
}
if (inQoutedString)
{
if (readChar == '\r')
{
fieldBuilder.Append('\r');
}
fieldBuilder.Append('\n');
}
else
{
record.Add(fieldBuilder.ToString().TrimEnd());
fieldBuilder = new StringBuilder();
records.Add(record);
record = new List<string>();
inQoutedString = false;
}
}
else if (fieldBuilder.Length == 0 && !inQoutedString)
{
if (char.IsWhiteSpace(readChar))
{
// Ignore leading whitespace
}
else if (readChar == '"')
{
inQoutedString = true;
}
else if (readChar == ',')
{
record.Add(fieldBuilder.ToString().TrimEnd());
fieldBuilder = new StringBuilder();
}
else
{
fieldBuilder.Append(readChar);
}
}
else if (readChar == ',')
{
if (inQoutedString)
{
fieldBuilder.Append(',');
}
else
{
record.Add(fieldBuilder.ToString().TrimEnd());
fieldBuilder = new StringBuilder();
}
}
else if (readChar == '"')
{
if (inQoutedString)
{
if (stringReader.Peek() == '"')
{
stringReader.Read();
fieldBuilder.Append('"');
}
else
{
inQoutedString = false;
}
}
else
{
fieldBuilder.Append(readChar);
}
}
else
{
fieldBuilder.Append(readChar);
}
}
record.Add(fieldBuilder.ToString().TrimEnd());
records.Add(record);
return records;
}
Zauważ, że nie obsługuje to skrajnych przypadków pól, które nie są oddzielone podwójnymi cudzysłowami, ale meerley ma w sobie ciąg znaków w cudzysłowie. Zobacz ten post, aby uzyskać lepsze rozszerzenie, a także kilka linków do niektórych odpowiednich bibliotek.
Zgadzam się z @ NotMyself . FileHelpers jest dobrze przetestowany i obsługuje wszystkie rodzaje skrajnych przypadków, z którymi w końcu będziesz musiał sobie poradzić, jeśli zrobisz to sam. Spójrz na to, co robi FileHelpers i napisz własne tylko wtedy, gdy jesteś absolutnie pewien, że albo (1) nigdy nie będziesz musiał zajmować się skrajnymi przypadkami, które robi FileHelpers, albo (2) uwielbiasz pisać tego rodzaju rzeczy i zamierzasz ciesz się, gdy musisz analizować takie rzeczy:
1, „Bill”, „Smith”, „Supervisor”, „No Comment”
2, „Drake”, „O'Malley”, „Woźny,
Ups, nie jestem cytowany i jestem na nowej linii!
Byłem znudzony, więc zmodyfikowałem kilka rzeczy, które napisałem. Próbuje hermetyzować parsowanie w sposób OO, jednocześnie zmniejszając liczbę iteracji w pliku, a iteruje tylko raz na początku.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// usage:
// note this wont run as getting streams is not Implemented
// but will get you started
CSVFileParser fileParser = new CSVFileParser();
// TO Do: configure fileparser
PersonParser personParser = new PersonParser(fileParser);
List<Person> persons = new List<Person>();
// if the file is large and there is a good way to limit
// without having to reparse the whole file you can use a
// linq query if you desire
foreach (Person person in personParser.GetPersons())
{
persons.Add(person);
}
// now we have a list of Person objects
}
}
public abstract class CSVParser
{
protected String[] deliniators = { "," };
protected internal IEnumerable<String[]> GetRecords()
{
Stream stream = GetStream();
StreamReader reader = new StreamReader(stream);
String[] aRecord;
while (!reader.EndOfStream)
{
aRecord = reader.ReadLine().Split(deliniators,
StringSplitOptions.None);
yield return aRecord;
}
}
protected abstract Stream GetStream();
}
public class CSVFileParser : CSVParser
{
// to do: add logic to get a stream from a file
protected override Stream GetStream()
{
throw new NotImplementedException();
}
}
public class CSVWebParser : CSVParser
{
// to do: add logic to get a stream from a web request
protected override Stream GetStream()
{
throw new NotImplementedException();
}
}
public class Person
{
public String Name { get; set; }
public String Address { get; set; }
public DateTime DOB { get; set; }
}
public class PersonParser
{
public PersonParser(CSVParser parser)
{
this.Parser = parser;
}
public CSVParser Parser { get; set; }
public IEnumerable<Person> GetPersons()
{
foreach (String[] record in this.Parser.GetRecords())
{
yield return new Person()
{
Name = record[0],
Address = record[1],
DOB = DateTime.Parse(record[2]),
};
}
}
}
}
Istnieją dwa artykuły na temat CodeProject, które zawierają kod rozwiązania, jeden wykorzystujący StreamReader i jeden importujący dane CSV przy użyciu sterownika Microsoft Text Driver .
Dobrym, prostym sposobem jest otwarcie pliku i wczytanie każdego wiersza do tablicy, listy połączonej, wybranej struktury danych. Uważaj jednak na pierwszą linię.
Może to być ponad twoją głową, ale wydaje się, że istnieje bezpośredni sposób na uzyskanie do nich dostępu za pomocą parametrów połączenia .
Dlaczego nie spróbować użyć Pythona zamiast C # lub VB? Ma ładny moduł CSV do zaimportowania, który wykonuje wszystkie ciężkie prace za Ciebie.
Tego lata musiałem użyć parsera CSV w .NET do projektu i zdecydowałem się na sterownik Microsoft Jet Text Driver. Określasz folder za pomocą parametrów połączenia, a następnie odpytuj plik za pomocą instrukcji SQL Select. Silne typy można określić za pomocą pliku schema.ini. Na początku tego nie zrobiłem, ale potem otrzymywałem złe wyniki, gdy typ danych nie był od razu widoczny, na przykład numery IP lub wpis typu „XYQ 3.9 SP1”.
Jedynym ograniczeniem, na które natknąłem się, jest to, że nie obsługuje nazw kolumn powyżej 64 znaków; to obcina. Nie powinno to stanowić problemu, poza tym, że miałem do czynienia z bardzo źle zaprojektowanymi danymi wejściowymi. Zwraca ADO.NET DataSet.
To było najlepsze rozwiązanie, jakie znalazłem. Byłbym ostrożny, jeśli chodzi o rozwijanie własnego parsera CSV, ponieważ prawdopodobnie przegapiłbym niektóre przypadki końcowe, a nie znalazłem żadnych innych bezpłatnych pakietów parsujących CSV dla .NET.
EDYCJA: Ponadto w każdym katalogu może istnieć tylko jeden plik schema.ini, więc dodałem do niego dynamicznie, aby silnie wpisać potrzebne kolumny. Będzie tylko silnie wpisywać określone kolumny i wywnioskować dla dowolnego nieokreślonego pola. Naprawdę to doceniam, ponieważ miałem do czynienia z importowaniem płynnego pliku CSV z kolumnami 70+ i nie chciałem określać każdej kolumny, tylko te źle działające.
Wpisałem jakiś kod. Wynik w datagridviewer wyglądał dobrze. Przetwarza pojedynczy wiersz tekstu do tablicy obiektów.
enum quotestatus
{
none,
firstquote,
secondquote
}
public static System.Collections.ArrayList Parse(string line,string delimiter)
{
System.Collections.ArrayList ar = new System.Collections.ArrayList();
StringBuilder field = new StringBuilder();
quotestatus status = quotestatus.none;
foreach (char ch in line.ToCharArray())
{
string chOmsch = "char";
if (ch == Convert.ToChar(delimiter))
{
if (status== quotestatus.firstquote)
{
chOmsch = "char";
}
else
{
chOmsch = "delimiter";
}
}
if (ch == Convert.ToChar(34))
{
chOmsch = "quotes";
if (status == quotestatus.firstquote)
{
status = quotestatus.secondquote;
}
if (status == quotestatus.none )
{
status = quotestatus.firstquote;
}
}
switch (chOmsch)
{
case "char":
field.Append(ch);
break;
case "delimiter":
ar.Add(field.ToString());
field.Clear();
break;
case "quotes":
if (status==quotestatus.firstquote)
{
field.Clear();
}
if (status== quotestatus.secondquote)
{
status =quotestatus.none;
}
break;
}
}
if (field.Length != 0)
{
ar.Add(field.ToString());
}
return ar;
}
Jeśli możesz zagwarantować, że w danych nie ma przecinków, najprostszym sposobem byłoby prawdopodobnie użycie String.split .
Na przykład:
String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);
Mogą istnieć biblioteki, których możesz użyć, aby pomóc, ale prawdopodobnie jest to tak proste, jak tylko możesz. Po prostu upewnij się, że nie możesz mieć przecinków w danych, w przeciwnym razie będziesz musiał lepiej je przeanalizować.