Извлечение данных из веб-ресурсов. Формат CSV

четверг, 24 февраля 2011, Александр Краковецкий

В продолжении темы «Извлечение данных и из-ресурсов» поговорим о таком формате данных, как CSV, а также об инструментах и способах обработки.

Название CSV расшифровывается как comma-separated values – т.е. значения, разделенные запятой. На самом деле, разделитесь (delimeter) может быть абсолютно любой.

Пример CSV файла:

Id, Name

1, Ukraine

2, Russia

3, Angola

Базовая реализация CSV парсера достаточно банальная:

namespace CsvParser
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            string fileName = "C://temp/data.csv";
            string separator = ", ";

            foreach (var line in System.IO.File.ReadAllLines(fileName))
            {
                string[] values = line.Split(new string[] { separator }, 
                    StringSplitOptions.RemoveEmptyEntries);

                foreach (var value in values)
                {
                    Console.WriteLine(value);
                }
            }
            Console.ReadKey();
        }
    }
}

На экране увидим такой результат:

Id

Name

1

Ukraine

2

Russia

3

Angola

Но на самом деле простота решения в данном случае – обманчивая.

Рассмотрим проблемы, с которыми может столкнуться разработчик в процессе парсинга CSV файлов.

Файл может иметь большой размер. Это может привести к тому, что File.ReadAllLines() придется заменить на StreamReader для построчного чтения (так как при использовании File.ReadAllLines() весь файл может не поместиться в памяти):

namespace CsvParser
{
    using System;
    using System.IO;

    class Program
    {
        static void Main(string[] args)
        {
            string fileName = "C://temp/data.csv";
            string separator = ", ";

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

                while ((line = reader.ReadLine()) != null) 
                {
                    string[] values = line.Split(new string[] { separator },
                        StringSplitOptions.RemoveEmptyEntries);

                    foreach (var value in values)
                    {
                        Console.WriteLine(value);
                    }
                }
            }

            Console.ReadKey();
        }
    }
}

На самом деле, такой вариант тоже не дает 100% гарантию, так как отдельные строчки также могут быть большого размера. В таком случае необходимо использовать буфер при чтении файла и читать файл «порциями».

Следующая проблема, которая может возникнуть – сами данные. Например, если у нас запятая выступает в качестве разделителя, а одно (или более) значений содержат запятые, то при разборе количество элементов будет отличаться от необходимого.

Например:

Id, Name

1, Ukraine

2, Russia

3, Angola

4, Gambia, The

В таких случаях принято данные брать в скобки (так делает, например, Microsoft Excel):

4, “Gambia, The”

Таким образом, необходимо обрабатывать также и скобки, если такие будут найдены.

Следующий момент – перенос новой строки. Очень часто вместо «\n» для переноса строки используется «\r\n». Проще всего в коде использовать выражение System.Environment.NewLine.

Очень часто данные, хотелось бы получать не только по индексу, что неудобно (представьте, что у вас 100 полей), а также по имени столбца, например:

values[“Id”] и values[“Name”]

Нужно помнить, что файл не всегда содержит строку с названиями столбцов.

Наконец, если вы обрабатываете большой файл, в котором всего несколько строчек с неправильными данными и вам не хотелось бы останавливать весь процесс ради этих нескольких строк, то, скорее всего, правильным решением будет игнорировать такие строки (реализовать логику по принципу SkipLineOnError = true).

На самом деле есть и другие сложности, связанные с платформами, где выполняются задачи, кодировками и т.д. Более детально можно почитать здесь.

Убедились, что разбор CSV файла – не тривиальная задача? Тогда рассмотрим уже написанные средства для работы с CSV файлами.

Парсить CSV файлы можно с помощью ODBC Microsoft Text Driver :

string path = @"C:\";

using (OdbcConnection conn = new OdbcConnection(
    "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=" +
    path + ";Extensions=asc,csv,tab,txt"))
{
    using (OdbcCommand cmd = 
        new OdbcCommand("SELECT * FROM verylarge.csv", conn))
    {
        conn.Open();

        using (OdbcDataReader dr =
            cmd.ExecuteReader(CommandBehavior.SequentialAccess))
        {
            while (dr.Read())
            {
                int test1 = dr.GetInt32(0);
                int test2 = dr.GetInt32(1);
                int test3 = dr.GetInt32(2);
                int test4 = dr.GetInt32(3);
                int test5 = dr.GetInt32(4);
            }
        }
    }
}

А также с помощью Microsoft Jet OLE Driver:

string path = @"C:\";

using (OleDbConnection conn =
    new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +
    path + @";Extended Properties=""Text;HDR=No;FMT=Delimited"""))
{
    using (OleDbCommand cmd = 
        new OleDbCommand("SELECT * FROM verylarge.csv", conn))
    {
        conn.Open();

        using (OleDbDataReader dr = 
            cmd.ExecuteReader(CommandBehavior.SequentialAccess))
        {
            while (dr.Read())
            {
                int test1 = dr.GetInt32(0);
                int test2 = dr.GetInt32(1);
                int test3 = dr.GetInt32(2);
                int test4 = dr.GetInt32(3);
                int test5 = dr.GetInt32(4);
            }
        }
    }
}

Это не самые быстрые способы, поэтому использовать их вряд ли стоит.

Регулярные выражения (Regex) Почему то все считают, что если есть задача распарсить какие-то данные, то регулярные выражения – это идеальный инструмент. Боюсь разочаровать, но это не так. Регулярные выражения работают долго, и ими можно покрыть только базовые сценарии.

Класс TextFieldParser в Microsoft.VisualBasic.FileIO. Этот класс находится в сборке Microsoft.VisualBasic.dll, т.е. в C# проектах этого класса нет по умолчанию.

Работает таким образом:

Using MyReader As New Microsoft.VisualBasic.FileIO.
    TextFieldParser("c:\logs\bigfile")

    MyReader.TextFieldType = 
        Microsoft.VisualBasic.FileIO.FieldType.Delimited
    MyReader.Delimiters = New String() {vbTab}
    Dim currentRow As String()
    'Loop through all of the fields in the file. 
    'If any lines are corrupt, report an error and continue parsing. 
    While Not MyReader.EndOfData
        Try
            currentRow = MyReader.ReadFields()
            ' Include code here to handle the row.
        Catch ex As Microsoft.VisualBasic.FileIO.MalformedLineException
            MsgBox("Line " & ex.Message & 
            " is invalid.  Skipping")
        End Try
    End While
End Using

Вроде все просто и понятно. Остается загадкой, почему такой же функционал не был включен по умолчанию в System.IO.

FileHelpers Library

Пусть у нас есть файл в таком формате:

1732,Juan Perez,435.00,11-05-2002

554,Pedro Gomez,12342.30,06-02-2004

112,Ramiro Politti,0.00,01-02-2000

924,Pablo Ramirez,3321.30,24-11-2002

...............

Создаем класс, который будет описывать структуру этих данных:

[DelimitedRecord(",")]
public class Customer
{
    public int CustId;

    public string Name;

    public decimal Balance;

    [FieldConverter(ConverterKind.Date, "dd-MM-yyyy")]
    public DateTime AddedDate;

}

Дальше необходимо создать объект FileHelperEngine и открыть файл:

FileHelperEngine engine = new FileHelperEngine(typeof(Customer));

// To Read Use:
Customer[] res = engine.ReadFile("FileIn.txt") as Customer[];

// To Write Use:
engine.WriteFile("FileOut.txt", res);
Дальнейшее использование уже заполненных объектов очень простое:
foreach (Customer cust in res)
 {
    Console.WriteLine("Customer Info:");
    Console.WriteLine(cust.Name + " - " +
                      cust.AddedDate.ToString("dd/MM/yy"));
 }

Очень удобно, согласитесь. Библиотека распространяется бесплатно с исходными кодами под лицензией LGPL. Скачать можно на сайте http://www.filehelpers.com/.

A Fast CSV Reader

Сайт: http://www.codeproject.com/KB/database/CsvReader.aspx

Библиотека поддерживает все описанные мной выше сценарии, использование библиотеки достаточно простое:

using System.IO;
using LumenWorks.Framework.IO.Csv;

void ReadCsv()
{
    // open the file "data.csv" which is a CSV file with headers
    using (CsvReader csv =
           new CsvReader(new StreamReader("data.csv"), true))
    {
        int fieldCount = csv.FieldCount;
        string[] headers = csv.GetFieldHeaders();

        while (csv.ReadNextRecord())
        {
            for (int i = 0; i < fieldCount; i++)
                Console.Write(string.Format("{0} = {1};",
                              headers[i], csv[i]));

            Console.WriteLine();
        }
    }
}

В ASP.NET приложениях использование библиотеки может быть таким:

using System.IO;
using LumenWorks.Framework.IO.Csv;

void ReadCsv()
{
    // open the file "data.csv" which is a CSV file with headers
    using (CsvReader csv = new CsvReader(
                           new StreamReader("data.csv"), true))
    {
        myDataRepeater.DataSource = csv;
        myDataRepeater.DataBind();
    }
}

Библиотека поддерживает события обработки ошибок: DefaultParseErrorAction и ParseError, правила обработки данных, например:

csv.MissingFieldAction = MissingFieldAction.ReplaceByNull;

Эту библиотеку я активно использую в своем проекте и нахожу ее очень удобным инструментом.

Если вам необходимо распарсить CSV файл прямо в базу данных, то необходимо использовать утилиту bcp.

На сегодня все.

Компании из статьи


Microsoft Украина


Сайт:
http://www.microsoft.com/ukr/ua/

Microsoft Украина Украинское подразделение компании Microsoft.

Ищите нас в интернетах!

Комментарии

Свежие вакансии