Lidando com vírgulas em um arquivo CSV

Eu estou procurando sugestões sobre como lidar com um arquivo csv que está sendo criado e, em seguida, carregado por nossos clientes, e que pode ter uma vírgula em um valor, como um nome de empresa.

Algumas das idéias que estamos vendo são: Identificadores (valor “,” valores “,” etc) ou usando um | em vez de uma vírgula. O maior problema é que temos que facilitar, ou o cliente não fará isso.

Como outros já disseram, você precisa escaping valores que incluem citações. Aqui está um pequeno leitor de CSV em C♯ que suporta valores entre aspas, incluindo cotações incorporadas e retornos de carro.

By the way, este é o código testado pela unidade. Estou postando agora porque esta pergunta parece vir muito e outros podem não querer uma biblioteca inteira quando o suporte a CSV simples funcionar.

Você pode usá-lo da seguinte maneira:

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

Aqui estão as classs. Observe que você pode usar a function Csv.Escape para gravar CSV válido também.

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

Para 2017, o csv é totalmente especificado – RFC 4180.

É uma especificação muito comum e é completamente coberta por muitas bibliotecas ( exemplo ).

Basta usar qualquer biblioteca csv facilmente disponível – ou seja, a RFC 4180.


Na verdade, há uma especificação para o formato CSV e como lidar com vírgulas:

Campos contendo quebras de linha (CRLF), aspas duplas e vírgulas devem ser colocados entre aspas duplas.

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

Então, para ter valores foo e bar,baz , você faz isso:

 foo,"bar,baz" 

Outro requisito importante a considerar (também da especificação):

Se aspas duplas forem usadas para include campos, uma aspa dupla entre um campo deve ser precedida por uma outra aspa dupla. Por exemplo:

 "aaa","b""bb","ccc" 

O formato CSV usa vírgulas para separar valores, valores que contêm retornos de carro, alimentações de linha, vírgulas ou aspas duplas estão entre aspas duplas. Os valores que contêm aspas duplas são citados e cada citação literal é escapada por uma citação imediatamente anterior: por exemplo, os 3 valores:

 test list, of, items "go" he said 

seria codificado como:

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

Qualquer campo pode ser citado, mas apenas os campos que contêm vírgulas, CR / NL ou citações devem ser citados.

Não existe um padrão real para o formato CSV, mas quase todos os aplicativos seguem as convenções documentadas aqui . O RFC que foi mencionado em outro lugar não é um padrão para CSV, é um RFC para usar o CSV dentro do MIME e contém algumas limitações não convencionais e desnecessárias que o tornam inútil fora do MIME.

Uma pegadinha que muitos módulos CSV que eu vi não acomodam é o fato de que várias linhas podem ser codificadas em um único campo, o que significa que você não pode presumir que cada linha é um registro separado, ou você não precisa permitir novas linhas na sua dados ou estar preparado para lidar com isso.

Coloque aspas duplas em torno de seqüências de caracteres. Geralmente, isso é o que o Excel faz .

Ala Eli,

você escapa uma aspa dupla como duas aspas duplas. Por exemplo, “test1”, “foo” “bar”, “test2”

Você pode colocar aspas duplas nos campos. Eu não gosto dessa abordagem, pois adiciona outro caractere especial (as aspas duplas). Basta definir um caractere de escape (geralmente barra invertida) e usá-lo onde quer que você precise para escaping de algo:

  dados, mais dados, mais dados \, ainda mais 

Você não precisa tentar corresponder aspas e tem menos exceções para analisar. Isso simplifica seu código também.

Existe uma biblioteca disponível através do nuget para lidar com praticamente qualquer CSV bem formado (.net) – CsvHelper

Exemplo para mapear para uma class:

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

Exemplo para ler campos individuais:

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

Deixando o cliente dirigir o formato de arquivo:
, é o delimitador de campo padrão, " é o valor padrão usado para escaping de campos que contêm um delimitador, uma citação ou uma linha final.

Para usar (por exemplo) # para campos e ' para escape:

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

Mais documentação

Adicionar uma referência ao Microsoft.VisualBasic (sim, ele diz VisualBasic mas funciona em C # tão bem – lembre-se que no final é tudo apenas IL).

Use a class Microsoft.VisualBasic.FileIO.TextFieldParser para analisar o arquivo CSV Aqui está o código de exemplo:

  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 

Caso você esteja em um sistema * nix , tenha access a sed e possa haver uma ou mais vírgulas indesejadas somente em um campo específico de seu CSV, você pode usar o seguinte one-liner para incluí-las em " como RFC4180 Seção 2 propõe:

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

Dependendo de qual campo a (s) vírgula (s) indesejada (s) pode (m) estar, você deve alterar / estender os grupos de captura da regex (e a substituição).
O exemplo acima includeá o quarto campo (entre seis) entre aspas.

insira a descrição da imagem aqui

Em combinação com a opção –in --in-place você pode aplicar essas alterações diretamente no arquivo.

Para “construir” o regex certo, há um princípio simples a seguir:

  1. Para cada campo em seu arquivo CSV que vem antes do campo com as vírgulas indesejadas, você escreve um [^,]*, e os coloca todos juntos em um grupo de captura.
  2. Para o campo que contém as vírgulas indesejadas que você escreve (.*) .
  3. Para cada campo após o campo com a (s) vírgula (s) indesejada (s) você escreve um ,.* E os coloca todos juntos em um grupo de captura.

Aqui está uma breve visão geral de diferentes regexes / substituições possíveis, dependendo do campo específico. Se não for dada, a substituição é \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) 

Se você deseja remover as vírgulas indesejadas com sed vez de colocá-las entre aspas, consulte esta resposta .

Você pode usar “delimitadores” alternativos, como “;” ou “|” mas o mais simples pode estar apenas citando o que é suportado pela maioria das bibliotecas CSV (decentes) e pela maioria das planilhas decentes.

Para saber mais sobre delimitadores de CSV e uma especificação para um formato padrão para descrever delimitadores e citações, consulte esta página da Web.

Como mencionado no meu comentário à resposta de harpo, sua solução é boa e funciona na maioria dos casos, no entanto, em alguns cenários, quando aspas são diretamente adjacentes, elas não se dividem nas vírgulas.

Isso ocorre porque a sequência Regex está se comportando inesperadamente como uma cadeia vertabim. Para conseguir que isso se comporte corretamente, todos os “caracteres na string regex precisam ser escapados manualmente sem usar o escape vertabim.

Ie. O regex deve estar usando escape manual:

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

que se traduz em ",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))"

Ao usar uma string vertabim @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))" Ela se comporta como seguindo como você pode ver se você depurar o regex:

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

Então, em resumo, eu recomendo a solução do harpo, mas cuidado com essa gotcha!

Eu incluí no CsvReader um pequeno opcional à prova de falhas para notificá-lo se esse erro ocorrer (se você tiver um número de colunas pré-conhecido):

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

Isso pode ser injetado através do construtor:

 public CsvReader(string fileName, int expectedDataLength = 0) : this(new FileStream(fileName, FileMode.Open, FileAccess.Read)) { _expectedDataLength = expectedDataLength; } 

Se você está interessado em um exercício mais educativo sobre como analisar arquivos em geral (usando o CSV como exemplo), você pode conferir este artigo por Julian Bucknall. Eu gosto do artigo porque ele divide as coisas em problemas muito menores que são muito menos intransponíveis. Primeiro você cria uma gramática e, depois de ter uma boa gramática, é um processo relativamente fácil e metódico para converter a gramática em código.

O artigo usa C # e tem um link na parte inferior para fazer o download do código.

Se você sentir vontade de reinventar a roda, o seguinte pode funcionar para você:

 public static IEnumerable SplitCSV(string line) { var s = new StringBuilder(); bool escaped = false, inQuotes = false; foreach (char c in line) { if (c == ',' && !inQuotes) { yield return s.ToString(); s.Clear(); } else if (c == '\\' && !escaped) { escaped = true; } else if (c == '"' && !escaped) { inQuotes = !inQuotes; } else { escaped = false; s.Append(c); } } yield return s.ToString(); } 

Na Europa, temos esse problema deve mais cedo do que esta questão. Na Europa, usamos todas as vírgulas para um ponto decimal. Veja estes números abaixo:

 | American | Europe | | ------------- | ------------- | | 0.5 | 0,5 | | 3.14159265359 | 3,14159265359 | | 17.54 | 17,54 | | 175,186.15 | 175.186,15 | 

Portanto, não é possível usar o separador de vírgula para arquivos CSV. Por esse motivo, os arquivos CSV na Europa são separados por um ponto-e-vírgula ( ; ) .

Programas como o Microsoft Excel podem ler arquivos com um ponto-e-vírgula e é possível alternar do separador. Você pode até usar uma tabulação ( \t ) como separador. Veja esta resposta do Supper User .

Como se trata de práticas gerais, vamos começar pelas regras do polegar:

  1. Não use CSV, use XML com uma biblioteca para ler e gravar o arquivo xml.

  2. Se você precisar usar o CSV. Faça isso corretamente e use uma biblioteca gratuita para analisar e armazenar os arquivos CSV.

Para justificar 1), a maioria dos analisadores de CSV não está ciente da codificação, portanto, se você não estiver lidando com o US-ASCII, estará solicitando problemas. Por exemplo, o Excel 2002 está armazenando o CSV na codificação local sem nenhuma nota sobre a codificação. O padrão CSV não é amplamente adotado :(. Por outro lado, o padrão xml é bem adotado e lida muito bem com codificações.

Para justificar 2), há toneladas de analisadores de csv em torno de quase todas as linguagens, então não há necessidade de reinventar a roda, mesmo se as soluções parecerem bem simples.

Para citar alguns:

  • para python use build no módulo csv

  • para perl verificar CPAN e Text :: CSV

  • para php use build em funções fgetcsv / fputcsv

  • para biblioteca SuperCVS de verificação de java

Realmente não há necessidade de implementar isso manualmente se você não for analisá-lo no dispositivo incorporado.

Você pode ler o arquivo csv como este.

isso faz uso de divisões e cuida de espaços.

 ArrayList List = new ArrayList(); static ServerSocket Server; static Socket socket; static ArrayList list = new ArrayList(); public static void ReadFromXcel() throws FileNotFoundException { File f = new File("Book.csv"); Scanner in = new Scanner(f); int count =0; String[] date; String[] name; String[] Temp = new String[10]; String[] Temp2 = new String[10]; String[] numbers; ArrayList List = new ArrayList(); HashMap m = new HashMap(); in.nextLine(); date = in.nextLine().split(","); name = in.nextLine().split(","); numbers = in.nextLine().split(","); while(in.hasNext()) { String[] one = in.nextLine().split(","); List.add(one); } int xount = 0; //Making sure the lines don't start with a blank for(int y = 0; y< = date.length-1; y++) { if(!date[y].equals("")) { Temp[xount] = date[y]; Temp2[xount] = name[y]; xount++; } } date = Temp; name =Temp2; int counter = 0; while(counter < List.size()) { String[] list = List.get(counter); String sNo = list[0]; String Surname = list[1]; String Name = list[2]; for(int x = 3; x < list.length; x++) { m.put(numbers[x], list[x]); } Object newOne = new newOne(sNo, Name, Surname, m, false); StudentList.add(s); System.out.println(s.sNo); counter++; } 

Eu acho que a solução mais fácil para este problema é ter o cliente para abrir o csv no excel e, em seguida, ctrl + r para replace todas as vírgulas com qualquer identificador que você deseja. Isso é muito fácil para o cliente e requer apenas uma alteração em seu código para ler o delimitador de sua escolha.

Primeiro, vamos nos perguntar: “Por que sentimos a necessidade de lidar com vírgulas de forma diferente para arquivos CSV?”

Para mim, a resposta é: “Porque quando eu exporto dados para um arquivo CSV, as vírgulas em um campo desaparecem e meu campo é separado em vários campos onde as vírgulas aparecem nos dados originais.” (Isso porque a vírgula é o caractere separador de campo CSV.)

Dependendo da sua situação, os pontos e vírgulas podem também ser usados ​​como separadores de campos CSV.

Dadas as minhas necessidades, posso usar um caractere, por exemplo, uma aspas simples com 9 a menos, que se parece com uma vírgula.

Então, aqui está como você pode fazer isso em Go:

 // Replace special CSV characters with single low-9 quotation mark func Scrub(a interface{}) string { s := fmt.Sprint(a) s = strings.Replace(s, ",", "‚", -1) s = strings.Replace(s, ";", "‚", -1) return s } 

O segundo caractere de vírgula procurando na function Replace é decimal 8218.

Esteja ciente de que, se você tiver clientes que possam ter leitores de texto somente ascii, este caractere decima 8218 não parecerá uma vírgula. Se esse for o seu caso, recomendo cercar o campo com a vírgula (ou ponto-e-vírgula) com aspas duplas por RFC 4128: https://tools.ietf.org/html/rfc4180

Eu geralmente codifico com URL os campos que podem ter qualquer vírgula ou qualquer caractere especial. E depois decodificá-lo quando estiver sendo usado / exibido em qualquer meio visual.

(vírgulas se torna% 2C)

Todo idioma deve ter methods para codificar e decodificar strings.

por exemplo, em java

 URLEncoder.encode(myString,"UTF-8"); //to encode URLDecoder.decode(myEncodedstring, "UTF-8"); //to decode 

Eu sei que esta é uma solução muito geral e pode não ser ideal para a situação em que o usuário deseja visualizar o conteúdo do arquivo csv manualmente.

Eu costumo fazer isso em minhas rotinas de análise de arquivos CSV. Suponha que a variável ‘line’ seja uma linha dentro de um arquivo CSV e que todos os valores das colunas estejam entre aspas duplas. Depois que as duas linhas abaixo forem executadas, você receberá colunas CSV na coleção ‘values’.

 // The below two lines will split the columns as well as trim the DBOULE QUOTES around values but NOT within them string trimmedLine = line.Trim(new char[] { '\"' }); List values = trimmedLine.Split(new string[] { "\",\"" }, StringSplitOptions.None).ToList(); 

A solução mais simples que encontrei é aquela que o LibreOffice usa:

  1. Substituir todo o literal " por
  2. Coloque aspas duplas em volta da sua string

Você também pode usar o que o Excel usa:

  1. Substitua todo literal " por ""
  2. Coloque aspas duplas em volta da sua string

Observe que outras pessoas recomendam fazer apenas a etapa 2 acima, mas isso não funciona com linhas em que " é seguido por a, como em um CSV onde você deseja ter uma única coluna com a string hello",world , como O CSV leria:

 "hello",world" 

Que é interpretado como uma linha com duas colunas: hello e world"

  public static IEnumerable LineSplitter(this string line, char separator, char skip = '"') { var fieldStart = 0; for (var i = 0; i < line.Length; i++) { if (line[i] == separator) { yield return line.Substring(fieldStart, i - fieldStart); fieldStart = i + 1; } else if (i == line.Length - 1) { yield return line.Substring(fieldStart, i - fieldStart + 1); fieldStart = i + 1; } if (line[i] == '"') for (i++; i < line.Length && line[i] != skip; i++) { } } if (line[line.Length - 1] == separator) { yield return string.Empty; } } 

Use um caractere de tabulação (\ t) para separar os campos.