CSV dosyasını .Net'teki kesin olarak yazılmış veri yapısına içe aktarın [kapalı]


106

Bir CSV dosyasını kesin olarak yazılmış bir veri yapısına aktarmanın en iyi yolu nedir?




7
Bunun 1103495'ten bir yıl önce yaratıldığını düşünürsek, bu sorunun bunun bir kopyası olduğunu düşünüyorum.
MattH

2
Teşekkürler Matt. Sadece onları birbirine bağlamaya çalışıyordum, hangisinin önce geldiğini belirtmiyorum. Bunu işaret eden diğer soruda da tamamen aynı metne sahip olduğumu göreceksiniz. İki soruyu birbirine bağlamanın daha iyi bir yolu var mı?
Mark Meuer

Yanıtlar:


74

Microsoft'un TextFieldParser'ı kararlıdır ve CSV dosyaları için RFC 4180'i izler . Microsoft.VisualBasicAd alanı tarafından ertelenmeyin; .NET Framework'teki standart bir bileşendir, yalnızca genel Microsoft.VisualBasicderlemeye bir referans ekleyin .

Windows için derliyorsanız (Mono'nun aksine) ve "bozuk" (RFC uyumlu olmayan) CSV dosyalarını ayrıştırmak zorunda kalacağınızı tahmin etmiyorsanız, ücretsiz, kısıtlamasız, kararlı olduğu için bu bariz bir seçim olacaktır. ve aktif olarak desteklenir, çoğu FileHelpers için söylenemez.

Ayrıca bkz: Nasıl yapılır: VB kodu örneği için Visual Basic'te Virgülle Ayrılmış Metin Dosyalarından Okuma .


2
Aslında bu sınıf hakkında ne yazık ki adlandırılmış ad alanı dışında VB'ye özgü hiçbir şey yoktur. Yalnızca "basit" bir CSV ayrıştırıcısına ihtiyacım olsaydı, kesinlikle bu kitaplığı seçerdim, çünkü genel olarak indirecek, dağıtacak veya endişelenecek bir şey yok. Bu amaçla, bu cevabın VB odaklı ifadesini düzenledim.
Aaronaught

@Aaronaught Bence düzenlemeleriniz çoğunlukla bir gelişme. Bu RFC mutlaka yetkili olmasa da, birçok CSV yazarı buna uymadığından, örneğin Excel "CSV" dosyalarında her zaman virgül kullanmaz . Ayrıca önceki cevabım, sınıfın C # 'dan kullanılabileceğini söylememiş miydi?
MarkJ

TextFieldParserÇok sekme ile sınırlandırılmış ve diğer garip Excel oluşturulan cruft için irade çalışması. Ben önceki cevabı kütüphanesi VB-özgü olduğunu iddia olmadığını fark, sadece gerçekten olduğunu ima olarak benim için rastladı demek VB için değil, amaçlanan ben sanmıyorum ki, C # kullanılacak durum - MSVB'de gerçekten kullanışlı bazı sınıflar var.
Aaronaught

21

OleDB bağlantısı kullanın.

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();

Bu, dosya sistemi erişimi gerektirir. OLEDB'yi bellek içi akışlarla çalıştırmanın bir yolu olmadığını bildiğim kadarıyla :(
UserControl

3
@UserControl, tabii ki dosya sistemi erişimi gerektirir. Bir CSV dosyasını içe aktarmayı sordu
Kevin

1
Şikayet etmiyorum. Aslında geri kalanı yerine OLEDB çözümünü tercih ederdim, ancak CSV'yi ASP.NET uygulamalarında ayrıştırmak gerektiğinde o kadar çok hayal kırıklığına uğradım ki bunu not etmek istedim.
UserControl

12

CSV ayrıştırması için oldukça karmaşık senaryolar bekliyorsanız, kendi ayrıştırıcımızı yuvarlamayı düşünmeyin bile . FileHelpers gibi birçok mükemmel araç var, hatta CodeProject'ten olanlar .

Mesele şu ki, bu oldukça yaygın bir sorundur ve birçok yazılım geliştiricisinin bu sorunu zaten düşündüğüne ve çözdüğüne bahse girebilirsiniz .


Bu bağlantı soruyu cevaplayabilirken, cevabın temel kısımlarını buraya eklemek ve referans için bağlantıyı sağlamak daha iyidir. Bağlantılı sayfa değişirse, yalnızca bağlantı yanıtları geçersiz hale gelebilir. - Yorumdan
techspider

Teşekkürler @techspider Umarım bu yazının StackOverflow'un beta döneminden olduğunu not etmişsinizdir: D Günümüzde CSV araçlarının Nuget paketlerinden daha iyi elde edildiği söyleniyor - bu yüzden bağlantı yanıtlarının bile 8 yıllık muaf olup olmadığından emin değilim
Jon Limjap

9

Brian, onu güçlü bir şekilde yazılmış bir koleksiyona dönüştürmek için güzel bir çözüm sunuyor.

Verilen CSV ayrıştırma yöntemlerinin çoğu, kaçan alanları veya CSV dosyalarının diğer bazı inceliklerini (alanları kırpma gibi) hesaba katmaz. İşte kişisel olarak kullandığım kod. Uçları biraz kaba ve neredeyse hiç hata bildirimi yok.

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;
}

Bunun, çift tırnaklarla sınırlandırılmayan alanların uç durumunu ele almadığını, ancak içinde tırnak işareti olan bir dize olduğunu unutmayın. Bkz Bu yayını daha iyi bunların açıklaması biraz yanı sıra bazı uygun kütüphanelere bazı bağlantılar.


9

@ NotMyself'e katılıyorum . FileHelpers iyi test edilmiştir ve kendi başınıza yaparsanız sonunda uğraşmanız gereken her türlü uç durumu ele alır. FileHelpers'ın ne yaptığına bir göz atın ve yalnızca (1) FileHelpers'ın yaptığı son durumlarla hiçbir zaman başa çıkmanıza gerek kalmayacağından veya (2) bu tür şeyleri yazmayı seviyorsanız ve bunun gibi şeyleri ayrıştırmanız gerektiğinde çok sevin:

1, "Bill", "Smith", "Süpervizör", "Yorum Yok"

2, "Drake", "O'Malley", "Kapıcı,

Oops, alıntı yapılmadı ve yeni bir satırdayım!


6

Sıkılmıştım, bu yüzden yazdığım bazı şeyleri değiştirdim. Dosya boyunca yineleme miktarını azaltırken, ayrıştırmayı OO tarzında kapsüllemeye çalışır, her biri en üstte yalnızca bir kez yineler.

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]),
                };
            }
        }
    }
}


2

Bunu yapmanın iyi bir basit yolu, dosyayı açmak ve her satırı bir diziye, bağlantılı listeye, seçtiğiniz veri yapısına göre okumaktır. Yine de ilk satırı tutarken dikkatli olun.

Bu başınızı aşmış olabilir, ancak bir bağlantı dizesi kullanarak bunlara erişmenin doğrudan bir yolu var gibi görünüyor .

Neden C # veya VB yerine Python kullanmayı denemiyorsunuz? İçe aktarmak için tüm ağır işleri sizin için yapan güzel bir CSV modülüne sahiptir.


1
CSV ayrıştırıcısı uğruna VB'den python'a atlamayın. VB'de bir tane var. Garip bir şekilde bu sorunun yanıtlarında göz ardı edilmiş görünüyor. msdn.microsoft.com/en-us/library/…
MarkJ

1

Bu yaz bir proje için .NET'te bir CSV ayrıştırıcısı kullanmak zorunda kaldım ve Microsoft Jet Metin Sürücüsüne yerleştim. Bir bağlantı dizesi kullanarak bir klasör belirtirsiniz, ardından bir SQL Select deyimini kullanarak bir dosyayı sorgulayabilirsiniz. Bir schema.ini dosyası kullanarak güçlü türler belirtebilirsiniz. İlk başta bunu yapmadım, ancak daha sonra IP numaraları veya "XYQ 3.9 SP1" gibi bir giriş gibi veri türünün hemen görünmediği kötü sonuçlar alıyordum.

Karşılaştığım bir sınırlama, 64 karakterin üzerindeki sütun adlarını işleyememesidir; keser. Çok zayıf tasarlanmış girdi verileriyle uğraşmam dışında bu bir sorun olmamalı. Bir ADO.NET DataSet döndürür.

Bulduğum en iyi çözüm buydu. Muhtemelen bazı son durumları kaçıracağım ve .NET için başka ücretsiz CSV ayrıştırma paketleri bulamadığım için kendi CSV ayrıştırıcımı yuvarlamak konusunda dikkatli olurdum.

DÜZENLEME: Ayrıca, dizin başına yalnızca bir schema.ini dosyası olabilir, bu nedenle gerekli sütunları güçlü bir şekilde yazmak için ona dinamik olarak ekledim. Yalnızca belirtilen sütunları güçlü bir şekilde yazacak ve belirtilmemiş herhangi bir alanı çıkaracaktır. Bunu gerçekten takdir ettim, çünkü 70+ sütunlu akışkan CSV'yi içe aktarmakla uğraşıyordum ve her bir sütunu belirtmek istemiyordum, sadece hatalı olanları belirtmek istiyordum.


VB.NET neden CSV ayrıştırıcısında yerleşik değil? msdn.microsoft.com/en-us/library/…
MarkJ

1

Bir kod yazdım. Datagridviewer'daki sonuç iyi görünüyordu. Tek bir metin satırını nesnelerin bir dizi listesine ayrıştırır.

    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;
    }

0

Verilerde virgül olmadığını garanti edebiliyorsanız, en basit yol muhtemelen String.split kullanmak olacaktır .

Örneğin:

String[] values = myString.Split(',');
myObject.StringField = values[0];
myObject.IntField = Int32.Parse(values[1]);

Yardım etmek için kullanabileceğiniz kütüphaneler olabilir, ancak bu muhtemelen alabileceğiniz kadar basittir. Verilerde virgül bulunmamasına dikkat edin, aksi takdirde daha iyi ayrıştırmanız gerekir.


bu optimal bir çözüm değil
roundcrisis

bellek kullanımı ve çok fazla ek yük açısından çok kötü. Küçük, birkaç kilobayt için daha az olmalıdır. 10 mb csv için kesinlikle iyi değil!
Piotr Kula

Belleğinizin ve dosyanın boyutuna bağlıdır.
tonymiao
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.