Março de 2016

Volume 31 Número 3

Aplicativos modernos - análise de arquivos CSV em aplicativos UWP

Por Frank La La

A análise de um arquivo de valores separados por vírgula (.csv) parece bastante fácil inicialmente. Rapidamente, no entanto, a tarefa se torna mais e mais complexa conforme os problemas encontrados em arquivos CSV se tornam mais claros. Se você não estiver familiarizado com o formato, os arquivos CSV armazenam dados em texto sem formatação. Cada linha no arquivo constitui um registro. Cada registro tem seus campos tipicamente delineados por uma vírgula, daí o nome.

Hoje, os desenvolvedores apreciam os padrões entre os formatos de troca de dados. O “formato” de arquivo CSV remete a um momento anterior na indústria do software antes do JSON e do XML. Enquanto houver um RFC (Request for Comments) para arquivos CSV (bit.ly/1NsQlvw), ele não aproveitará o status oficial. Adicionalmente, ele foi criado em 2005, décadas após a aparição dos arquivos CSV, nos anos 70. Como resultado, existe uma variação de arquivos CSV e as regras são um pouco obscuras. Por exemplo, um arquivo CSV pode ter campos separados por guias, ponto-e-vírgula ou outro caractere.

Em termos práticos, a implementação do Excel de importação e de exportação de CSV tornou-se o padrão de fato e é vista de forma mais abrangente no setor, mesmo fora do ecossistema da Microsoft. As suposições que fiz neste artigo sobre o que constitui uma análise e uma formatação “corretas” se baseiam em como o Excel importa/exporta arquivos CSV. Embora a maioria dos arquivos CSV esteja alinhada à implementação do Excel, nem todos os arquivos estarão. Até o fim desta coluna, apresentarei uma estratégia para lidar com tais incertezas.

Uma pergunta justa a se fazer é: “Por que gravar um analisador em um formato bastante antigo em uma plataforma totalmente nova?” A resposta é simples: Vários organizadores têm sistemas de dados herdados. Graças ao longo tempo de vida do formato de arquivo, quase todos os sistemas de dados herdados podem exportar para CSV. Além disso, exportar dados para CSV custa muito pouco em termos de tempo e esforço. Portanto, há muitos arquivos formatados para CSV nos maiores conjuntos de dados de empresas e do governo.

Projeto de um analisador CSV para vários fins

Além da falta de um padrão oficial, os arquivos CSV geralmente compartilham algumas características comuns.

Em termos gerais, os arquivos CSV: são texto sem formatação, têm um registro por linha, possuem registros em cada linha separados por um delimitador, têm delimitadores de um caractere e apresentam campos na mesma ordem.

Essas características comuns referem-se a um algoritmo geral, que pode consistir em três etapas:

  1. Dividir uma cadeia de caracteres junto com o delimitador de linha.
  2. Dividir cada linha junto com o delimitador de linha.
  3. Atribuir cada valor do campo a uma variável.

Isso seria bastante fácil de implementar. O código na Figura 1 analisa a cadeia de caracteres de entrada em uma List<Dictionary<cadeia de caracteres, cadeia de caracteres>>.

A Figura 1 está analisando a cadeia de caracteres de entrada em List<Dictionary<string,string>>

var parsedResult = new List<Dictionary<string, string>>();
var records = RawText.Split(this.LineDelimiter);
foreach (var record in records)
  {
    var fields = record.Split(this.Delimiter);
    var recordItem = new Dictionary<string, string>();
    var i = 0;
    foreach (var field in fields)
    {
      recordItem.Add(i.ToString(), field);
      i++;
    }
    parsedResult.Add(recordItem);
  }

Esta abordagem funciona bem usando um exemplo como os seguintes departamentos de escritório e seus números de vendas:

East, 73, 8300
South, 42, 3000
West, 35, 4250
Mid-West, 18, 1200

Para recuperar os valores da cadeia de caracteres, você deveria iterar na lista e retirar os valores do Dictionary usando o índice de campo de base zero. A recuperação de um campo de departamento de escritório, por exemplo, seria simples assim:

foreach (var record in parsedData)
{
  string fieldOffice = record["0"];
}

Embora isso funcione, o código não está tão legível quanto poderia ser.

Um dicionário melhor

Muitos arquivos CSV incluem uma linha de cabeçalho como o nome do campo. Seria mais fácil para os desenvolvedores consumir o analisador se ele usasse o nome do campo como a chave para o dicionário. Como os arquivos CSV fornecidos talvez não tenham uma linha de cabeçalho, você deve adicionar uma propriedade para transmitir essa informação:

public bool HasHeaderRow { get; set; }

Por exemplo, um arquivo CSV de exemplo com uma linha de cabeçalho pode se parecer com o seguinte:

Office Division, Employees, Unit Sales
East, 73, 8300
South, 42, 3000
West, 35, 4250
Mid-West, 18, 1200

Idealmente, o analisador de CSV seria capaz de aproveitar esses metadados. Isso tornaria o código mais legível. A recuperação do campo de departamento de escritório teria esta aparência:

foreach (var record in parsedData)
{
  string fieldOffice = record["Office Division"];
}

Campos em branco

Os campos em branco são comuns em conjuntos de dados. Em arquivos CSV, os campos em branco são representados por um campo vazio em um registro. O delimitador ainda é necessário. Por exemplo, se não houver dados do funcionário para o escritório do leste, o registro terá esta aparência:

East,,8300

Se não houver dados de vendas da unidade, e se não houver dados do funcionário, o registro terá esta aparência:

East,,

Cada organização tem seus próprios padrões de qualidade de dados. Algumas podem escolher optar por um valor padrão em um campo em branco para tornar o arquivo CSV mais legível. Os valores padrão normalmente seriam 0 ou NULO para números e “” ou NULO para cadeias de caracteres.

Como manter a flexibilidade

Devido a todas as ambiguidades em torno do formato de arquivo CSV, o código não pode fazer suposições. Não há garantia de que o delimitador de campo será uma vírgula e não há garantia de que o delimitador de registro será uma nova linha.

Sendo assim, ambas serão propriedades da classe CSVParser:

public char Delimiter { get; set; }
public char LineDelimiter { get; set; }

Para ficar mais fácil para os desenvolvedores utilizarem este componente, crie as configurações padrão que serão aplicadas na maioria dos casos:

private const char DEFAULT_DELIMITER = ',';
private const char DEFAULT_LINE_DELIMITER = '\n';

Se alguém desejar alterar o delimitador padrão para um caractere de tabulação, o código é bastante simples:

CsvParser csvParser = new CsvParser();
csvParser.Delimiter = '\t';

Caracteres de escape

O que aconteceria se o campo tivesse o caractere delimitador, como uma vírgula? Por exemplo, em vez de se referir às vendas por região, e se os dados tivessem cidade e estado? Normalmente, os arquivos CSV têm uma solução alternativa, colocam os campos inteiros entre aspas, desta forma:

Office Division, Employees, Unit Sales
"New York, NY", 73, 8300
"Richmond, VA", 42, 3000
"San Jose, CA", 35, 4250
"Chicago, IL", 18, 1200

Este algoritmo faria com que o valor de um campo único “Nova York, NY” se transformasse em dois campos discretos com os valores divididos na vírgula, “Nova York” e “NY”.

Nesse caso, separar os valores da cidade e estado pode não ser restritivo, mas ainda há caracteres de aspas extra poluindo os dados. Embora seja fácil removê-los daqui, pode não ser tão fácil limpar os dados mais complexos.

Agora ficou complicado

Este método de vírgulas de escape dentro dos campos introduz outro caractere de escape: o caractere de aspas. E se, por exemplo, houvesse aspas nos dados originais, como mostrado na Figura 2?

Figura 2 Dados originais entre aspas

Departamento do escritório Funcionários Vendas da unidade Lema do escritório
Nova York, NY 73 8300 “Nós vendemos ótimos produtos”
Richmond, VA 42 3000 “Experimente e compre”
São José, CA 35 4250 “Poder ao Vale do Silício!”
Chicago, IL 18 1200 “Ótimos produtos a preços imbatíveis”

O texto não processado no arquivo CSV teria esta aparência:

Office Division, Employees, Unit Sales, Office Motto
"New York, NY",73,8300,"""We sell great products"""
"Richmond, VA",42,3000,"""Try it and you'll want to buy it"""
"San Jose, CA",35,4250,"""Powering Silicon Valley!"""
"Chicago, IL",18,1200,"""Great products at great value"""

Um abre aspas (“) recebe um escape ficando com três aspas (“””), dando um toque interessante ao algoritmo. A primeira pergunta razoável a se fazer é esta: Porque um abre aspas se transformou em três aspas? Tal como no campo de departamento de escritório, o conteúdo do campo é colocado entre aspas. Para poder fazer escape dos caracteres de aspas que fazem parte do conteúdo, eles são duplicados. Portanto, “ se torna “”.

Outro exemplo (Figura 3) deve demonstrar o processo de forma mais clara.

Figura 3 Dados entre aspas

Citação
“A única coisa que devemos temer é o próprio medo.”- Presidente Roosevelt
“A lógica leva você de A para B. A imaginação o leva a qualquer lugar.” - Albert Einstein

Os dados na Figura 3 seriam representados no CSV desta forma:

Citação

"""The only thing we have to fear is fear itself."" -President Roosevelt"
"""Logic will get you from A to B. Imagination will take you everywhere."" -Albert Einstein"

Agora pode estar mais claro que o campo está entre aspas e que o abre aspas e o fecha aspas no conteúdo do campo estão duplicadas.

Casos extremos

Como mencionado na seção de abertura, nem todos os arquivos irão aderir à implementação do Excel do CSV. A falta de uma especificação verdadeira para o CSV dificulta a criação de um analisador para lidar com todos os arquivos CSV existentes. Certamente existirão as ocorrências extremas, o que significa que o código deve deixar uma porta aberta para a interpretação e a personalização.

A inversão de controle salvadora

Dadas as características vagas do padrão do formato CSV, não é prático escrever um analisador abrangente para todos os casos imagináveis. Pode ser mais adequado escrever um analisador para se adaptar a uma necessidade específica de um aplicativo. O uso da inversão de controle permite que você personalize um mecanismo de análise para uma necessidade específica.

Para executar esta ação, criarei uma interface para descrever as duas funções principais de análise: a extração de registros e a extração de campos. Decidi tornar a interface IParserEngine assíncrona. Isso garante que qualquer aplicativo que utilize este componente se mantenha dinâmico, independentemente do tamanho do arquivo CSV:

public interface IParserEngine
{
  IAsyncOperation<IList<string>> ExtractRecords(char lineDelimiter, string csvText);
  IAsyncOperation<IList<string>> ExtractFields(char delimiter, char quote,
    string csvLine);
}

Em seguida, adicionarei a seguinte propriedade à classe CSVParser:

public IParserEngine ParserEngine { get; private set; }

Depois ofereço aos desenvolvedores uma opção: usar o analisador padrão ou utilizar os seus próprios. Para simplificar, vou sobrecarregar o construtor:

public CsvParser()
{
  InitializeFields();
  this.ParserEngine = new ParserEngines.DefaultParserEngine();
}
public CsvParser(IParserEngine parserEngine)        
{
  InitializeFields();
  this.ParserEngine = parserEngine;
}

A classe CSVParser agora oferece a infraestrutura básica, mas a lógica de análise real está contida na interface IParserEngine. Para a conveniência dos desenvolvedores, criei o DefaultParserEngine, que pode processar a maioria dos arquivos CSV. Considerei os cenários mais comuns que os desenvolvedores irão encontrar.

Desafio do leitor

Considerei a maioria dos cenários que os desenvolvedores encontrarão com os arquivos CSV. No entanto, a natureza indefinida do formato CSV torna impraticável a criação de um analisador universal para todos os casos. Fatorar todas as variações e os casos extremos adicionaria um custo e uma complexidade significativos junto com o impacto no desempenho.

Estou certo de que há arquivos CSV “por aí” com que o DefaultParserEngine não conseguirá lidar. É isto que torna o padrão da injeção de dependência uma ótima opção. Se os desenvolvedores precisarem que um analisador lide com um caso extremo ou que escreva algo com mais desempenho, certamente eu os incentivo a fazê-lo. Os mecanismos de análise poderiam ser trocados sem alterações no código de consumo.

O código deste projeto está disponível em bit.ly/1To1IVI.

Conclusão

Os arquivos CSV são muito antigos e, mesmo com todos os esforços de XML e JSON, ainda são um formato de troca de dados bastante utilizado. Os arquivos CSV não possuem especificação comum ou padrão e, embora tenham traços comuns, não é certo de que funcionarão para qualquer arquivo fornecido. Isso faz da análise de um arquivo CSV um exercício não trivial.

Se pudessem escolher, a maioria dos desenvolvedores provavelmente excluiria os arquivos CSV de suas soluções. No entanto, a ampla presença em conjuntos de dados herdados do governo e de empresas pode excluir essa opção de vários cenários.

De forma simples, há uma necessidade de um analisador de CSV para aplicativos da UWP (Plataforma Universal do Windows) e um analisador CSV real deve ser flexível e robusto. Ao longo do percurso, demonstrei aqui o uso prático da injeção de dependência para fornecer essa flexibilidade. Embora esta coluna e seu código associado estejam direcionados a aplicativos da UWP, o conceito e o código se aplicam a outras plataformas capazes de executar C#, como o Microsoft Azure ou o desenvolvimento para a área de trabalho do Windows.


Frank La Vigne* é evangelista de tecnologia que faz parte da equipe de Tecnologia e Compromisso Cívico da Microsoft, onde ajuda os usuários a aproveitar a tecnologia para criar uma comunidade melhor. Ele tem um blog em FranksWorld.com e um canal do YouTube chamado “Frank’s World TV” (youtube.com/FranksWorldTV).*

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Rachel Appel