Работа с запятыми в файле CSV - PullRequest
442 голосов
/ 20 апреля 2009

Я ищу предложения о том, как обрабатывать CSV-файл, который создается, затем загружается нашими клиентами и может иметь запятую в значении, таком как название компании.

Вот некоторые идеи, которые мы рассматриваем: цитируемые идентификаторы (значение, значения, и т. Д.) Или использование | вместо запятой. Самая большая проблема заключается в том, что нам нужно сделать это проще, иначе клиент этого не сделает.

Ответы [ 24 ]

379 голосов
/ 20 апреля 2009

Для 2017 года указывается csv - RFC 4180.

Это очень распространенная спецификация, полностью охватываемая многими библиотеками ( пример ).

Просто используйте любую легкодоступную библиотеку CSV - то есть RFC 4180.


На самом деле есть спецификация для формата CSV и способ обработки запятых:

Поля, содержащие разрывы строк (CRLF), двойные кавычки и запятые, должны быть заключены в двойные кавычки.

http://tools.ietf.org/html/rfc4180

Итак, чтобы иметь значения foo и bar,baz, вы делаете это:

foo,"bar,baz"

Еще одно важное требование для рассмотрения (также из спецификации):

Если для заключения полей используются двойные кавычки, то двойные кавычки появляться внутри поля необходимо экранировать, предшествуя ему еще одна двойная цитата. Например:

"aaa","b""bb","ccc"
213 голосов
/ 20 апреля 2009

Как уже говорили другие, вам нужно избегать значений, которые включают кавычки. Вот небольшой CSV-ридер на C♯, который поддерживает значения в кавычках, включая встроенные кавычки и возврат каретки.

Кстати, это код, проверенный модулем. Я публикую его сейчас, потому что этот вопрос, кажется, часто возникает, и другие могут не захотеть целую библиотеку, когда подойдет простая поддержка CSV.

Вы можете использовать его следующим образом:

using System;
public class test
{
    public static void Main()
    {
        using ( CsvReader reader = new CsvReader( "data.csv" ) )
        {
            foreach( string[] values in reader.RowEnumerator )
            {
                Console.WriteLine( "Row {0} has {1} values.", reader.RowIndex, values.Length );
            }
        }
        Console.ReadLine();
    }
}

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

using System.IO;
using System.Text.RegularExpressions;

public sealed class CsvReader : System.IDisposable
{
    public CsvReader( string fileName ) : this( new FileStream( fileName, FileMode.Open, FileAccess.Read ) )
    {
    }

    public CsvReader( Stream stream )
    {
        __reader = new StreamReader( stream );
    }

    public System.Collections.IEnumerable RowEnumerator
    {
        get {
            if ( null == __reader )
                throw new System.ApplicationException( "I can't start reading without CSV input." );

            __rowno = 0;
            string sLine;
            string sNextLine;

            while ( null != ( sLine = __reader.ReadLine() ) )
            {
                while ( rexRunOnLine.IsMatch( sLine ) && null != ( sNextLine = __reader.ReadLine() ) )
                    sLine += "\n" + sNextLine;

                __rowno++;
                string[] values = rexCsvSplitter.Split( sLine );

                for ( int i = 0; i < values.Length; i++ )
                    values[i] = Csv.Unescape( values[i] );

                yield return values;
            }

            __reader.Close();
        }
    }

    public long RowIndex { get { return __rowno; } }

    public void Dispose()
    {
        if ( null != __reader ) __reader.Dispose();
    }

    //============================================


    private long __rowno = 0;
    private TextReader __reader;
    private static Regex rexCsvSplitter = new Regex( @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))" );
    private static Regex rexRunOnLine = new Regex( @"^[^""]*(?:""[^""]*""[^""]*)*""[^""]*$" );
}

public static class Csv
{
    public static string Escape( string s )
    {
        if ( s.Contains( QUOTE ) )
            s = s.Replace( QUOTE, ESCAPED_QUOTE );

        if ( s.IndexOfAny( CHARACTERS_THAT_MUST_BE_QUOTED ) > -1 )
            s = QUOTE + s + QUOTE;

        return s;
    }

    public static string Unescape( string s )
    {
        if ( s.StartsWith( QUOTE ) && s.EndsWith( QUOTE ) )
        {
            s = s.Substring( 1, s.Length - 2 );

            if ( s.Contains( ESCAPED_QUOTE ) )
                s = s.Replace( ESCAPED_QUOTE, QUOTE );
        }

        return s;
    }


    private const string QUOTE = "\"";
    private const string ESCAPED_QUOTE = "\"\"";
    private static char[] CHARACTERS_THAT_MUST_BE_QUOTED = { ',', '"', '\n' };
}
73 голосов
/ 20 апреля 2009

Формат CSV использует запятые для разделения значений, значения, содержащие возврат каретки, переводы строки, запятые или двойные кавычки, заключаются в двойные кавычки. Значения, содержащие двойные кавычки, заключаются в кавычки, и каждая буквальная кавычка экранируется непосредственно предшествующей кавычкой: например, 3 значения:

test
list, of, items
"go" he said

будет закодировано как:

test
"list, of, items"
"""go"" he said"

Любое поле может быть заключено в кавычки, но только поля, которые содержат запятые, CR / NL или кавычки должны заключаться в кавычки.

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

Причиной, которую многие CSV-модули, которые я видел, не приспосабливают, является тот факт, что несколько строк могут быть закодированы в одном поле, что означает, что вы не можете предполагать, что каждая строка является отдельной записью, либо вам не нужно новые строки в ваших данных или будьте готовы с этим справиться.

39 голосов
/ 20 апреля 2009

Поместите двойные кавычки вокруг строк. Это обычно , что делает Excel .

Ала Эли,

вы избежите двойной кавычки как два двойные кавычки. Например. "Test1", "Foo" "бар", "test2"

9 голосов
/ 20 апреля 2009

Вы можете поставить двойные кавычки вокруг полей. Мне не нравится такой подход, так как он добавляет еще один специальный символ (двойная кавычка). Просто определите escape-символ (обычно с обратной косой чертой) и используйте его везде, где вам нужно что-то экранировать:

data,more data,more data\, even,yet more

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

7 голосов
/ 20 сентября 2012

Существует библиотека, доступная через nuget для работы с практически любым правильно сформированным CSV (.net) - CsvHelper

Пример для сопоставления с классом:

var csv = new CsvReader( textReader );
var records = csv.GetRecords<MyClass>();

Пример чтения отдельных полей:

var csv = new CsvReader( textReader );
while( csv.Read() )
{
    var intField = csv.GetField<int>( 0 );
    var stringField = csv.GetField<string>( 1 );
    var boolField = csv.GetField<bool>( "HeaderName" );
}

Разрешение клиенту управлять форматом файла:
, - стандартный разделитель полей, " - стандартное значение, используемое для экранирования полей, содержащих разделитель, кавычку или окончание строки.

Для использования (например) # для полей и ' для экранирования:

var csv = new CsvReader( textReader );
csv.Configuration.Delimiter = "#";
csv.Configuration.Quote = ''';
// read the file however meets your needs

Дополнительная документация

4 голосов
/ 11 апреля 2012

Добавьте ссылку на Microsoft.VisualBasic (да, там написано VisualBasic, но она работает и в C # - помните, что в конце это всего лишь IL)

Используйте класс Microsoft.VisualBasic.FileIO.TextFieldParser для разбора файла CSV. Вот пример кода:

 Dim parser As TextFieldParser = New TextFieldParser("C:\mar0112.csv")
 parser.TextFieldType = FieldType.Delimited
 parser.SetDelimiters(",")      

   While Not parser.EndOfData         
      'Processing row             
      Dim fields() As String = parser.ReadFields         
      For Each field As String In fields             
         'TODO: Process field                   

      Next      
      parser.Close()
   End While 
4 голосов
/ 29 сентября 2013

Вы можете использовать альтернативные "разделители", такие как ";" или "|" но самым простым может быть просто цитирование, которое поддерживается большинством (приличных) библиотек CSV и большинством приличных электронных таблиц.

Подробнее о о разделителях CSV и спецификации стандартного формата для описания разделителей и цитирования см. Эту веб-страницу

4 голосов
/ 24 января 2014

Если вы используете * nix-систему , имеете доступ к sed и может быть один или несколько нежелательных запятые только в определенном поле вашего CSV, вы можете использовать следующую однострочную строку для включения их в ", как предлагает RFC4180, раздел 2 :

sed -r 's/([^,]*,[^,]*,[^,]*,)(.*)(,.*,.*)/\1"\2"\3/' inputfile

В зависимости от того, в каком поле могут находиться нежелательные запятые, вам придется изменить / расширить группы захвата регулярного выражения (и подстановки).
В приведенном выше примере четвертое поле (из шести) будет заключено в кавычки.

enter image description here

В сочетании с опцией --in-place вы можете применить эти изменения непосредственно к файлу.

Чтобы «построить» правильное регулярное выражение, нужно следовать простому принципу:

  1. Для каждого поля в вашем CSV, которое находится за до поля с нежелательной запятой (ями), вы пишете одну [^,]*, и складываете их все вместе в группу захвата.
  2. Для поля, содержащего нежелательную запятую (ы), вы пишете (.*).
  3. Для каждого поля после поля с нежелательной запятой (-ями) вы пишете одну ,.* и складываете их все вместе в группу захвата.

Вот краткий обзор различных возможных регулярных выражений / замен в зависимости от конкретной области. Если не дано, замена будет \1"\2"\3.

([^,]*)(,.*)                     #first field, regex
"\1"\2                           #first field, substitution

(.*,)([^,]*)                     #last field, regex
\1"\2"                           #last field, substitution


([^,]*,)(.*)(,.*,.*,.*)          #second field (out of five fields)
([^,]*,[^,]*,)(.*)(,.*)          #third field (out of four fields)
([^,]*,[^,]*,[^,]*,)(.*)(,.*,.*) #fourth field (out of six fields)

Если вы хотите удалить ненужные запятые с помощью sed вместо того, чтобы заключать их в кавычки, обратитесь к этому ответу .

4 голосов
/ 30 ноября 2016

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

Это связано с тем, что строка Regex неожиданно ведет себя как строка vertabim. Для правильного поведения всех «символов» в строке регулярного выражения необходимо экранировать вручную, не используя escape-код vertabim.

Т.е.. Регулярное выражение должно быть таким, используя ручные экранированные символы:

",(?=(?:[^\"\"]*\"\"[^\"\"]*\"\")*(?![^\"\"]*\"\"))"

, что переводится как ",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))"

При использовании строки vertabim @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))" она ведет себя следующим образом, как вы можете увидеть, если отлаживаете регулярное выражение:

",(?=(?:[^"]*"[^"]*")*(?![^"]*"))"

Итак, в заключение, я рекомендую решение Harpo, но остерегайтесь этой маленькой ошибки!

Я включил в CsvReader немного необязательный отказоустойчивый, чтобы уведомить вас, если эта ошибка возникает (если у вас есть заранее известное количество столбцов):

if (_expectedDataLength > 0 && values.Length != _expectedDataLength) 
throw new DataLengthException(string.Format("Expected {0} columns when splitting csv, got {1}", _expectedDataLength, values.Length));

Это может быть введено через конструктор:

public CsvReader(string fileName, int expectedDataLength = 0) : this(new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
    _expectedDataLength = expectedDataLength;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...