Ответ 1
Вы пытались найти уже существующий .NET парсер CSV? Этот утверждает, что обрабатывает многострочные записи значительно быстрее, чем OLEDB.
Я просматриваю параметры разделенного файла (например, CSV, вкладка и т.д.), основанные на MS-стеке в целом и .net. Единственная технология, которую я исключаю, - это SSIS, потому что я уже знаю, что она не удовлетворит мои потребности.
Итак, мои параметры выглядят следующим образом:
У меня есть два критерия, которые я должен выполнить. Во-первых, учитывая следующий файл, который содержит две логические строки данных (и пять физических строк):
101, Bob, "Keeps his house ""clean"".
Needs to work on laundry."
102, Amy, "Brilliant.
Driven.
Diligent."
Анализируемые результаты должны приводить к двум логическим "строкам", состоящим из трех строк (или столбцов). Третья строка строки/столбца должна сохранять символы новой строки! Говоря иначе, анализатор должен распознавать, когда строки "продолжают" на следующую физическую строку из-за "незакрытого" текстового классификатора.
Второй критерий заключается в том, что разделитель и текстовый определитель должны быть настраиваемыми для каждого файла. Вот две строки, взятые из разных файлов, которые я должен разбирать:
var first = @"""This"",""Is,A,Record"",""That """"Cannot"""", they say,"","""",,""be"",rightly,""parsed"",at all";
var second = @"~This~|~Is|A|Record~|~ThatCannot~|~be~|~parsed~|at all";
Собственный синтаксический анализ строки "first" будет выглядеть следующим образом:
"_" просто означает, что был захвачен пробел - я не хочу, чтобы появился литерал.
Можно сделать одно важное предположение о анализируемых плоских файлах: будет фиксированное количество столбцов на файл.
Теперь для погружения в технические параметры.
REGEX
Во-первых, многие респонденты комментируют, что регулярное выражение "не лучший способ" для достижения цели. Однако я нашел комментатор который предложил превосходное регулярное выражение CSV:
var regex = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
var Regex.Split(first, regex).Dump();
Результаты, примененные к строке "first", весьма замечательны:
Было бы неплохо, если бы кавычки были очищены, но я могу легко справиться с этим как шаг после процесса. В противном случае этот подход может использоваться для синтаксического анализа строк выборки "первым" и "вторым" при условии, что регулярное выражение модифицировано для символов тильды и труб соответственно. Отлично!
Но реальная проблема относится к многострочным критериям. Прежде чем регулярное выражение может быть применено к строке, я должен прочитать полную логическую "строку" из файла. К сожалению, я не знаю, сколько физических строк нужно прочитать для завершения логической строки, если у меня нет регулярного выражения/конечного автомата.
Итак, это становится проблемой "курица и яйцо". Моим лучшим вариантом было бы прочитать весь файл в памяти как одну гигантскую строку, и пусть регулярное выражение будет сортировать несколько строк (я не проверял, может ли это обработать вышеописанное выражение). Если у меня 10-гигабайтный файл, это может быть немного опасно.
В следующий раз.
TextFieldParser
Три строки кода сделают проблему с этой возможностью очевидной:
var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser(stream);
reader.Delimiters = new string[] { @"|" };
reader.HasFieldsEnclosedInQuotes = true;
Конфигурация Delimiters выглядит неплохо. Однако "HasFieldsEnclosedInQuotes" - это "игра". Я ошеломлен тем, что разделители произвольно конфигурируются, но, напротив, у меня нет другого параметра выбора, кроме котировок. Помните, мне нужна настраиваемость над спецификатором текста. Итак, если кто-то не знает трюк конфигурации TextFieldParser, это игра.
OLEDB
Коллега говорит мне, что этот вариант имеет два основных недостатка. Во-первых, он имеет ужасную производительность для больших (например, 10-гигабайтных) файлов. Во-вторых, поэтому мне говорят, что он предпочел типы данных ввода данных, а не позволял вам указывать. Нехорошо.
HELP
Поэтому я хотел бы знать факты, в которых я ошибался (если они есть), и другие варианты, которые я пропустил. Возможно, кто-то знает способ для присяжных - TextFieldParser использовать произвольный разделитель. И, возможно, OLEDB разрешил указанные проблемы (или, возможно, никогда их не видел?).
Что вы говорите?
Вы пытались найти уже существующий .NET парсер CSV? Этот утверждает, что обрабатывает многострочные записи значительно быстрее, чем OLEDB.
Я написал это некоторое время назад как легкий, автономный синтаксический анализатор CSV. Я считаю, что он отвечает всем вашим требованиям. Дайте ему попробовать, зная, что он, вероятно, не пуленепробиваемый.
Если это сработает для вас, не стесняйтесь изменять пространство имен и использовать без ограничений.
namespace NFC.Portability
{
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
/// <summary>
/// Loads and reads a file with comma-separated values into a tabular format.
/// </summary>
/// <remarks>
/// Parsing assumes that the first line will always contain headers and that values will be double-quoted to escape double quotes and commas.
/// </remarks>
public unsafe class CsvReader
{
private const char SEGMENT_DELIMITER = ',';
private const char DOUBLE_QUOTE = '"';
private const char CARRIAGE_RETURN = '\r';
private const char NEW_LINE = '\n';
private DataTable _table = new DataTable();
/// <summary>
/// Gets the data contained by the instance in a tabular format.
/// </summary>
public DataTable Table
{
get
{
// validation logic could be added here to ensure that the object isn't in an invalid state
return _table;
}
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="path">The fully-qualified path to the file from which the instance will be populated.</param>
public CsvReader( string path )
{
if( path == null )
{
throw new ArgumentNullException( "path" );
}
FileStream fs = new FileStream( path, FileMode.Open );
Read( fs );
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="stream">The stream from which the instance will be populated.</param>
public CsvReader( Stream stream )
{
if( stream == null )
{
throw new ArgumentNullException( "stream" );
}
Read( stream );
}
/// <summary>
/// Creates a new instance of <c>CsvReader</c>.
/// </summary>
/// <param name="bytes">The array of bytes from which the instance will be populated.</param>
public CsvReader( byte[] bytes )
{
if( bytes == null )
{
throw new ArgumentNullException( "bytes" );
}
MemoryStream ms = new MemoryStream();
ms.Write( bytes, 0, bytes.Length );
ms.Position = 0;
Read( ms );
}
private void Read( Stream s )
{
string lines;
using( StreamReader sr = new StreamReader( s ) )
{
lines = sr.ReadToEnd();
}
if( string.IsNullOrWhiteSpace( lines ) )
{
throw new InvalidOperationException( "Data source cannot be empty." );
}
bool inQuotes = false;
int lineNumber = 0;
StringBuilder buffer = new StringBuilder( 128 );
List<string> values = new List<string>();
Action endSegment = () =>
{
values.Add( buffer.ToString() );
buffer.Clear();
};
Action endLine = () =>
{
if( lineNumber == 0 )
{
CreateColumns( values );
values.Clear();
}
else
{
CreateRow( values );
values.Clear();
}
values.Clear();
lineNumber++;
};
fixed( char* pStart = lines )
{
char* pChar = pStart;
char* pEnd = pStart + lines.Length;
while( pChar < pEnd ) // leave null terminator out
{
if( *pChar == DOUBLE_QUOTE )
{
if( inQuotes )
{
if( Peek( pChar, pEnd ) == SEGMENT_DELIMITER )
{
endSegment();
pChar++;
}
else if( !ApproachingNewLine( pChar, pEnd ) )
{
buffer.Append( DOUBLE_QUOTE );
}
}
inQuotes = !inQuotes;
}
else if( *pChar == SEGMENT_DELIMITER )
{
if( !inQuotes )
{
endSegment();
}
else
{
buffer.Append( SEGMENT_DELIMITER );
}
}
else if( AtNewLine( pChar, pEnd ) )
{
if( !inQuotes )
{
endSegment();
endLine();
pChar++;
}
else
{
buffer.Append( *pChar );
}
}
else
{
buffer.Append( *pChar );
}
pChar++;
}
}
// append trailing values at the end of the file
if( values.Count > 0 )
{
endSegment();
endLine();
}
}
/// <summary>
/// Returns the next character in the sequence but does not advance the pointer. Checks bounds.
/// </summary>
/// <param name="pChar">Pointer to current character.</param>
/// <param name="pEnd">End of range to check.</param>
/// <returns>
/// Returns the next character in the sequence, or char.MinValue if range is exceeded.
/// </returns>
private char Peek( char* pChar, char* pEnd )
{
if( pChar < pEnd )
{
return *( pChar + 1 );
}
return char.MinValue;
}
/// <summary>
/// Determines if the current character represents a newline. This includes lookahead for two character newline delimiters.
/// </summary>
/// <param name="pChar"></param>
/// <param name="pEnd"></param>
/// <returns></returns>
private bool AtNewLine( char* pChar, char* pEnd )
{
if( *pChar == NEW_LINE )
{
return true;
}
if( *pChar == CARRIAGE_RETURN && Peek( pChar, pEnd ) == NEW_LINE )
{
return true;
}
return false;
}
/// <summary>
/// Determines if the next character represents a newline, or the start of a newline.
/// </summary>
/// <param name="pChar"></param>
/// <param name="pEnd"></param>
/// <returns></returns>
private bool ApproachingNewLine( char* pChar, char* pEnd )
{
if( Peek( pChar, pEnd ) == CARRIAGE_RETURN || Peek( pChar, pEnd ) == NEW_LINE )
{
// technically this cheats a little to avoid a two char peek by only checking for a carriage return or new line, not both in sequence
return true;
}
return false;
}
private void CreateColumns( List<string> columns )
{
foreach( string column in columns )
{
DataColumn dc = new DataColumn( column );
_table.Columns.Add( dc );
}
}
private void CreateRow( List<string> values )
{
if( values.Where( (o) => !string.IsNullOrWhiteSpace( o ) ).Count() == 0 )
{
return; // ignore rows which have no content
}
DataRow dr = _table.NewRow();
_table.Rows.Add( dr );
for( int i = 0; i < values.Count; i++ )
{
dr[i] = values[i];
}
}
}
}
Взгляните на код, который я отправил на этот вопрос:
Он охватывает большинство ваших требований, и не потребуется многого для его обновления, чтобы поддерживать альтернативные разделители или текстовые классификаторы.