Arquivos CSV no Go

Arquivos CSV é a mais básica das formas de transferência de informações entre aplicativos. Como o próprio nome diz (CSV = Comma-Separated-Values) os valores, comumente chamados de campos, são separados por vírgila. Cada linha destes campos são chamados de registros e empacotedos em um arquivo texto com extensão csv. Essencialmente uma tabela de dados.

Por exemplo, a tabela a seguir apresenta as populações dos estados brasileiros nos anos de 2010 e 2021, extraídas da Wikipédia: Lista de unidades federativas do Brasil por população.

Unidade FederativaPopulação (2010)População (2021)
São Paulo41 262 19946 649 132
Minas Gerais19 597 33021 411 923
Rio de Janeiro15 989 92917 463 349
Bahia14 016 90614 985 284
Paraná10 444 52611 597 484
.........

Estes dados podem ser gravados em um arquivo CSV com a forma:

Unidade Federativa,População (2010),População (2021)
São Paulo,41262199,46649132
Minas Gerais,19597330,21411923
Rio de Janeiro,15989929,17463349
Bahia,14016906,14985284
Paraná,10444526,11597484
Rio Grande do Sul,10693929,11466630
Pernambuco,8796448,9674793
Ceará,8452381,9240580
Pará,7581051,8777124
Santa Catarina,6248436,7338473
Goiás,6003788,7206589
Maranhão,6574789,7153262
Amazonas,3483985,4269995
Espírito Santo,3514952,4108508
Paraíba,3766528,4059905
Mato Grosso,3035122,3567234
Rio Grande do Norte,3168027,3560903
Alagoas,3120494,3365351
Piauí,3118360,3289290
Distrito Federal,2570160,3094325
Mato Grosso do Sul,2449024,2839188
Sergipe,2068017,2338474
Rondônia,1562409,1815278
Tocantins,1383445,1607363
Acre,733559,906876
Amapá,669526,877613
Roraima,450479,652713

Já copie o conteúdo deste arquivo em um populacao.cvs e o guarde em uma pasta para o desenvolvimento dos códigos a seguir.

Neste artigo vou tratar de algumas formas para ler e escrever dados em arquivos CSV empregando a biblioteca padrão encoding/csv, bem como os pacotes github.com/go-gota/gota/dataframe e a go-hep.org/x/hep/csvutil, que permitem uma melhor manipulação e transformação nas informações armazenadas.

Lendo CSV com a biblioteca padrão

A biblioteca padrão do Go trabalha com arquivos CSV que respeitam o padrão RFC 4180. Tanto a referência da RFC4180 como a referência do manual da biblioteca padrão trazem uma boa descrição do padrão de CVS e não vou me estender além do já apresentado aqui.

A biblioteca padrão possui duas structs fundamentais, que com seus métodos implementam a leitura e escrita em arquivos CSV. Estes são tratados a seguir:

Struct Reader

A struct Reader possui atributos e métodos para suporte e leitura de arquivos CSV. Inicialmente vamos ver seus atributos:

type Reader struct {
	Comma rune
	Comment rune
	FieldsPerRecord int
	LazyQuotes bool
	TrimLeadingSpace bool
	ReuseRecord bool
	TrailingComma bool
}

Atributo Comma rune

Este campo armazena o delimitador de campos em uma linha CSV. Por padrão ela vem setada para o caractere vírgula (“,“). Caracteres como tab (“\t“), pipe (“|“) como tantos outros podem ser setados como delimitador, no entanto caracteres com CR (“\r“) e LF (“\n“) não.

Atributo Comment rune

Este atributo carrega o caractere para linhas de comentários. Uma boa escolha é o caractere tralha “#“. Como o Reader.Comma os caracteres CR e LF não são aceitos.

Atributo FieldsPerRecord int

Este atributo é usado para especificar o número de campos por linha. Se Reader.FieldsPerRecord for positivo o Reader vai tentar ler este número de campos por linha. Se for um 0, o número de campos da primeira linha será usado como padrão para as demais linhas. Se for um número negativo o número de campos por linha será variável.

Atributo LazyQuotes bool

Segundo a documentação se o LazyQuotes for true aspas duplas pode aparecer em meio a um campo sem que este esteja protegido por aspas duplas, ou uma aspas simples podem aparecer no meio de um campo protegido por aspas duplas.

Na prática, o que observei após alguns testes, é que com o LazyQuotes true aspas duplas podem ser inseridas em meio ao texto de um campo como passando a frase como um campo: materializando a "exclusão includente e inclusão excludente" nas relações.

Sobre as aspas simples elas podem ser inseridas em meio a qualquer campo com o LazyQuotes true ou false, e não necessita ser protegido por aspas duplas.

Atributo TrimLeadingSpace bool

Se o TrimLeadingSpace for true é feito um trim nos campos, removendo espaços do início dos campos. Isto faz com que campos como:

   Campo 1 ,   Campo 2   ,    Campo 3

sejam interpretados como

"Campo 1 ","Campo 2   ","Campo 3"

Atributo ReuseRecord bool

Com o ReuseRecord true cada chamada do Read retorna a mesma slice da primeira chamada, sem a necessidade de realocar uma nova na memória a cada chamada. Essencialmente é uma opção de performance útil para leitura de grandes conjuntos de dados, uma vez que não é necessário realocar um novo slice a cada operação de Read. No entanto se deve tomar alguns cuidados com o seu emprego para não ser surpreendido com resultados indesejados, como será mostrado em exemplo a seguir.

Exemplo: reader_atributos.go – Testa os diferentes atributos do Reader

O último atributo, TrailingComma, foi descontinuado e por isto não é apresentado. O código a seguir é uma alteração do código exemplo da documentação do Reader para facilitar o testes dos diferentes atributos.

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"strings"
)

func strSlice(list []string) string {
	str := "["
	for _, s := range list {
		str += fmt.Sprintf("%q ", s)
	}
	str = str[:len(str)-1] + "]"
	return str
}

const input = `first_name,last_name,username
"Rob","Pike",'rob'
Ken,Thom"pson,ken
"Robert","Griesemer","gri"
# lines beginning with a # character are ignored if r.Comment = #
   Campo 1 ,   Campo 2   ,    Campo 3
Campo 1; Campo 2; Campo 3
I Don't Know, I Don't Know, I Don't Know
`

func main() {
	r := csv.NewReader(strings.NewReader(input))
	r.Comma = ','
	// r.Comment = '#'
	r.FieldsPerRecord = 3
	r.LazyQuotes = false
	r.TrimLeadingSpace = true
	r.ReuseRecord = false

	var records [][]string
	for {
		record, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Println(err)
			continue
		}
		records = append(records, record)
	}

	for i, line := range records {
		s := strSlice(line)
		fmt.Printf("%2d: %s\n", i, s)
	}
}

As mudanças fundamentais que fiz são:

  • a cada erro de entrara é gerado uma mensagem de erro e o código continua a sua execução, ao invés da interrupção do programa. Troca do log.Fatal por um log.Println na linha 46, e linha 47 a adição do continue;
  • adição da matriz de strings records, operado nas linhas 39 e 49 e 52, para poder explorar melhor o atributo ReuseRecord, alterável na linha 33;
  • adição da função strSlice, linhas 11 à 18, para adicionar aspas duplas ao redor dos campos da slice, o que permite um melhor reconhecimento de espaços ao final e início dos campos.

Além de mais alguns ajustes estéticos

Da forma como está a saída deste código será:

2022/07/24 09:45:51 parse error on line 3, column 9: bare " in non-quoted-field
2022/07/24 09:45:51 record on line 5: wrong number of fields
2022/07/24 09:45:51 record on line 7: wrong number of fields
 0: ["first_name" "last_name" "username"]
 1: ["Rob" "Pike" "'rob'"]
 2: ["Robert" "Griesemer" "gri"]
 3: ["Campo 1 " "Campo 2   " "Campo 3"]
 4: ["I Don't Know" "I Don't Know" "I Don't Know"]

O primeiro erro da linha 3 da definição da constante input, diz respeito ao campo Thom"pson que possui uma aspas duplas no meio do campo. Isto pode ser resolvido alterando o atributo LazyQuotes para true, na linha 35, que permitirá que o campo seja lido corretamente.

O segundo erro diz sobre a linha iniciada com o caractere tralha (“#“), linha 5 da constante input. Se removido o comentário da linha 33, toda linha iniciada com uma tralha será ignorada.

O terceiro erro diz respeito ao uso de ponto e vírgula (“;“) como delimitador de campo na sétima linha do input. Neste caso se pode mudar o delimitador por um ponto e vírgula na linha 32, no entanto todas as demais entradas da constante input irão gerar erro.

Tanto no segundo como no terceiro erro o número de campos por linha é 1, já que o delimitador de campo é a vírgula. Isto faz com que o erro gerado seja sobre um incorreto número de campos na linha.

Outro Efeito interessante é habilitar o ReuseRecord (fazê-lo igual a true na linha 37). Isto vai fazer com que todas as saídas contenham a última leitura do record (["I Don't Know" "I Don't Know" "I Don't Know"]), já que todas as linhas da matriz records apontam para um mesmo slice record criado na primeira execução do Read, na linha 41.

2022/07/24 10:10:05 record on line 7: wrong number of fields
 0: ["I Don't Know" "I Don't Know" "I Don't Know"]
 1: ["I Don't Know" "I Don't Know" "I Don't Know"]
 2: ["I Don't Know" "I Don't Know" "I Don't Know"]
 3: ["I Don't Know" "I Don't Know" "I Don't Know"]
 4: ["I Don't Know" "I Don't Know" "I Don't Know"]
 5: ["I Don't Know" "I Don't Know" "I Don't Know"]

Obviamente este uso não é o desejado, no entanto caso as leituras fossem tratadas individualmente a cada linha lida, ao invés de serem armazenadas em uma matriz [][]string (records), o resultado seria um código mais rápido, já que novas alocações de memória não seriam necessárias para armazenar novas leituras em record. Experimente remover a matriz records substituindo as linhas 39 à 55 pelo código a seguir, que o programa executará com o ReuseRecord habilitado.

...
	var i int = 0
	for {
		record, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Println(err)
			continue
		}
		s := strSlice(record)
		fmt.Printf("%2d: %s\n", i, s)
		i++
	}

Aconselho a adicionar e remover campos nas linhas do input, fazer alterações diversas nos atributos para melhor compreender como estes funcionam.

Mais um comentário, na linha 31 o csv.NewReader necessita de um io.Reader como entrada, e por este motivo foi aplicado um strings.NewReader() ao input para gerar o io.Reader. O método NewReader será apresentado a seguir.

csv.NewReader

func NewReader(r io.Reader) *Reader

A função NewReader recebe um Reader e retorna um ponteiro para um csv.Reader, um Reader específico para arquivos CVS, que podem ser lido pelos métodos: FieldPos, Read e ReadAll.

Qualquer struct que satisfaça a interface Reader pode ser usada como entrada na função NewReader, como é o caso da struct os.File, retornada por um os.Open, função usada para abrir um arquivo para leitura ou escrita.

Método Read, ReadAll e FieldPos

func (r *Reader) Read() (record []string, err error)

O método Read lê um registro do csv.Reader, retornando os seus campos em uma slice de string ([]string). Um erro é gerado se o número de campos inesperado for encontrado ou um io.EOF se o final do CSV for encontrado.

De forma semelhante ao feito no código reader_atributos.go o código a seguir emprega o Read para ler o registros do arquivo CSVpopulacao.csv“, apresentado no início do texto.

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
)

type State struct {
	name    string
	pop2010 int
	pop2021 int
}

func printReport(States []State) {
	total2010, total2021 := 0, 0
	for _, state := range States {
		total2010 += state.pop2010
		total2021 += state.pop2021
	}
	fmt.Printf("   %20s %9s  %9s  %5s  %5s\n", "Estado", "Pop 2010", "Pop 2021", "%2010", "%2021")
	for i, state := range States {
		pp2010 := 100. * float64(state.pop2010) / float64(total2010)
		pp2021 := 100. * float64(state.pop2021) / float64(total2021)
		fmt.Printf("%2d %20s %9d  %9d  %4.1f%%  %4.1f%%\n", i+1, state.name, state.pop2010, state.pop2021, pp2010, pp2021)
	}
}

func main() {
	f, err := os.Open("populacao.csv")
	if err != nil {
		log.Fatal(err)
	}
  	defer f.Close()

	var brasil []State

	records := csv.NewReader(f)
	records.FieldsPerRecord = 3

	for {
		record, err := records.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Print(err)
			continue
		}
		nome := record[0]
		if record[1] == "População (2010)" {
			continue
		}
		pop10, err := strconv.Atoi(record[1])
		if err != nil {
			log.Print(err)
			continue
		}
		pop21, err := strconv.Atoi(record[2])
		if err != nil {
			log.Print(err)
			continue
		}
		brasil = append(brasil, State{name: nome, pop2010: pop10, pop2021: pop21})
	}

	printReport(brasil)
}

Coloque o código acima e o arquivo “populacao.csv” no mesmo diretório e execute com o comando:

Bash

alves@suzail:csv_read$ go run pop_read.go
Estado Pop 2010 Pop 2021 %2010 %2021
1 São Paulo 41262199 46649132 21.6% 21.9%
2 Minas Gerais 19597330 21411923 10.3% 10.0%
3 Rio de Janeiro 15989929 17463349 8.4% 8.2%
4 Bahia 14016906 14985284 7.3% 7.0%
5 Paraná 10444526 11597484 5.5% 5.4%
6 Rio Grande do Sul 10693929 11466630 5.6% 5.4%

As diferenças essenciais do código anterior estão: na linha 33, onde f recebe um descritor do arquivo “populacao.csv“, aberto pelo os.Open e na linha 41 este descritor é passado como argumento para o csv.NewReader, a fim de ler os registros.

O demais do código são para a formatação da leitura e apresentação dos dados.

func (r *Reader) ReadAll() (records [][]string, err error)

O método ReadAll lê todos os registros remanescentes em r. Como um erro é gerado se o número de campos inesperado for encontrado ou um io.EOF se o final do CSV for encontrado sem que nenhum dado seja lido.

Para empregar o ReadAll no código anterior basta substituir as linhas 43 até 52 do código anterior pelas linhas 4 a 12 do código abaixo.

...
	records := csv.NewReader(f)
	records.FieldsPerRecord = 3
	allRecs, err := records.ReadAll()
	if err != nil {
		log.Fatal(err)
	}
	if err == io.EOF {
		log.Fatal(err)
	}

	for _, record := range allRecs {
		nome := record[0]
		if record[1] == "População (2010)" {
			continue
		}
      ...

Agora AllRecs recebe todos os registros em um slice de slices de strings ([][]string) que é lido na linha 4, acima. Um for-range, linha 12, recarregando os registros em record, como no código anterior.

A checagem do fim de arquivo, if err == io.EOF, neste código nem mesmo é necessário, o coloquei apenas para efeito de coerência no código, sendo necessário em outros empregos mais genéricos. Este código irá gerar a mesma saída do código anterior.

Struct Write

A struct Write possui atributos e métodos para a escrita de arquivos CSV. Esta struct possui apenas dois atributos sendo eles o Comma e o UseCRLF.

Atributo Comma

O atributo Comma recebe um rune para ser usado como delimitador entre campos. O padrão é a vírgula, mas pode ser personalizado para outro delimitador.

Atributo UseCRLF

O atributo UseCRLF é um booleano que quando true adiciona um CR+LF (“\r\n“) ao final de cada linha. Se false será adicionado apenas um LF (“\n“) ao final de cada linha. Em sistemas Unix é de praxe usar apenas um “\n” como fim de linha, mas o Windows tradicionalmente emprega um “\r\n“.

Função NewWriter

func NewWriter(w io.Writer) *Writer

Esta função retorna um Writer para o w.

Função Flush

func (w *Writer) Flush()

Flush escreve todos os registros em buffer para o io.Writer w. Para verificar se algum erro ocorreu invoque o método Error.

Função Write

func (w *Writer) Write(record []string) error

O método Write escreve um registro record em w com a quota se necessário. O record deve ser um slice de string ([]string), onde cada elemento da slice é um campo. Observe que o Writer possui buffer e por isto seu conteúdo será descarregado após ser chamado um Flush.

O código a seguir cria um arquivo de saída com o conteúdo CSV passado pela matriz records:

package main

import (
	"encoding/csv"
	"log"
	"os"
)

func main() {
	var records = [][]string{
		{"first_name", "last_name", "age"},
		{"Rob", "Pike", "31"},
		{"Ken", "Thompson", "46"},
		{"Robert", "Griesemer", "28"},
		{"Albert", "Eintein", "143"}}

	f, err := os.Create("test.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	w := csv.NewWriter(f)
	w.Comma = ';'
	w.UseCRLF = false
	defer w.Flush()

	for _, record := range records {
		if err := w.Write(record); err != nil {
			log.Print(err)
		}
	}
}

O arquivo de saída, “test.csv“, terá o conteúdo apresentado a seguir, com “\n” como fim de linha e um ponto e vírgula como delimitador de campo.

Bash

alves@suzail:csv_write$ go run csv_write.go
alves@suzail:csv_write$ cat test.csv
first_name;last_name;age
Rob;Pike;31
Ken;Thompson;46
Robert;Griesemer;28
Albert;Eintein;143

Função WriteAll

func (w *Writer) WriteAll(records [][]string) error

O WriteAll escreve múltiplos registros CSV em w.

O código a seguir reimplementa o csv_write.go com o WriteAll:

package main

import (
	"encoding/csv"
	"log"
	"os"
)

func main() {
	var records = [][]string{
		{"first_name", "last_name", "age"},
		{"Rob", "Pike", "31"},
		{"Ken", "Thompson", "46"},
		{"Robert", "Griesemer", "28"},
		{"Albert", "Eintein", "143"}}

	f, err := os.Create("test.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	w := csv.NewWriter(f)
	w.Comma = ';'
	w.UseCRLF = false

	w.WriteAll(records)
	defer w.Flush()
}

Função Error

func (w *Writer) Error() error

Error retorna qualquer erro que ocorra durante o último Flush ou Write realizado.

Para adicionar uma checagem de erro nos códigos anteriores comente a linha com defer w.Flush e adicione as linhas abaixo ao seu código:

...
	w.Flush()
	if err := w.Error(); err != nil {
		log.Fatal(err)
    }
}

Neste caso o w.Flush() não deve ser executado por um defer no mesmo bloco, pois este deve ser executado antes do w.Error().

Pacote csvutil

O pacote csvutil é uma implementação sobre a biblioteca padrão encoding/csv que traz funções e tipos para facilitar o manuseio de dados CSV.

Instalação do csvutil

Para a instalação do csvutil execute um go get no terminal como no comando abaixo:

Bash

alves@suzail:~$ go get go-hep.org/x/hep/csvutil

Sua documentação pode ser encontrada em godoc.org/go-hep.org/x/hep/csvutil.

Abrindo Arquivo CSV

Para abri um arquivo CSV este pacote possui três funções:

func Open(fname string) (*Table, error)
func Create(fname string) (*Table, error)
func Append(fname string) (*Table, error)

A função csvutil.Open abre um arquivo CSV retornando um tipo Table, descrito logo abaixo.

As funções csvutil.Create e csvutil.Append abrem um arquivo CSV retornando um tipo Table no modo escrita, sendo o Create para criar um novo arquivo CSV e Append para adicionar dados a um arquivo CSV já existente.

Além do ponteiro para um tipo Table estas funções também retornam um error para o caso de ocorrer algum erro na abertura do arquivo solicitado.

O Tipo Table

O tipo Table possui dois atributos:

Reader *csv.Reader
Writer *csv.Writer

Reader é um *csv.Reader e o Writer um *csv.Write. Ambos permitem acesso de escrita e leitura a um arquivo CSV, tal como apresentado anteriormente.

O tipo Table possui também quatro métodos:

func (tbl *Table) Close() error
func (tbl *Table) ReadRows(beg, end int64) (*Rows, error)
func (tbl *Table) WriteHeader(hdr string) error
func (tbl *Table) WriteRow(args ...interface{}) error

O primeiro é o Close(), necessário para fechar a tabela e pela cara do pacote acredito que deva fechar o arquivo.

O segundo, o método ReadRows, recebe como entrada dois inteiros com o índice do primeiro e do último registro (linha no arquivo CSV) a serem lidos. Se o end for passado como -1 a leitura será feita até atingir o final do arquivo CSV. Este método retorna um ponteiro para uma Rows (apresentado logo a seguir) e um código de erro se houver algum.

O terceiro método, WriteHeader, grava o cabeçalho passado como uma string em hdr no arquivo CSV.

Por fim o método WriteRow grava uma linha no arquivo CSV de saída.

Tipo Rows

O Rows é um iterador sobre as linhas do arquivo CSV, acessando um novo registro a cada .Next(). Ele possui 6 métodos para manuseá-lo:

func (rows *Rows) Close() error
func (rows *Rows) Err() error
func (rows *Rows) Fields() []string
func (rows *Rows) Next() bool
func (rows *Rows) NumFields() int
func (rows *Rows) Scan(dest ...interface{}) error

O método Close() permite fechar o acesso a Rows aberta. É conveniente usar um defer rows.Close() para fecha a Rows no final de um bloco.

O método Err() permite acessar qualquer erro que tenha ocorrido durante a interação.

O método Fields() retorna um slice de string com os elementos da linha. Ele retorna o mesmo que o método Read de um csv.Reader apresentado na seção anterior.

O método Next() avança para o próximo registro do arquivo CSV. Este método retorna um booleano true se o próximo registro for alcançado, mas se alcançar o final do arquivo ele retorna um booleano false. Este retorno é bem útil para empregar este método em um laço for, como será visto no exemplo a seguir.

O método NumFields() retorna um inteiro com o número de campos no registro atual.

Por fim, o método Scan(dest ...interface{}) vai escanear os dados do registro atual e tentar emcaixar ele nos dados passados como dest. A parte interessante aqui é que este dest pode ser uma struct com os atributos que casem com os dados do registro atual.

Lendo um Arquivo CSV com csvutil

O código read_struct.go, a seguir, usa uma struct State, definida nas linhas 10 à 14, para receber os dados lidos no método Scan:

package main

import (
	"fmt"
	"log"

	"go-hep.org/x/hep/csvutil"
)

type State struct {
	UF      string
	Pop2010 int
	Pop2021 int
}

func main() {
	table, err := csvutil.Open("populacao.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer table.Close()

	table.Reader.Comma = ','
	table.Reader.Comment = '#'

	rows, err := table.ReadRows(1, -1)
	if err != nil {
		log.Println(err)
	}
	defer rows.Close()

	records := []State{}

	for rows.Next() {
		data := State{}

		err = rows.Scan(&data)
		if err != nil {
			log.Println(err)
			continue
		}
		records = append(records, data)
	}
	err = rows.Err()
	if err != nil {
		log.Println(err)
	}

	fmt.Println(records)
}

A struct State possuir a mesma estrutura de dados dos registros no arquivo CSVpopulacao.csv

Na linha 17 a tabela table é aberta com a função csvutil.Open. Como de praxe é feito uma checagem de erro, linhas 18 à 20, e em seguir um defer para fechar a tabela ao final do código, linha 21. As linhas 23 e 24 são apenas para salientar que a tabela table contém um csv.Reader, com os mesmos atributos.

Na linha 26 é aberto uma rows com o método table.ReadRows. A primeira linha será a de índice 1, a segunda linha do arquivo CSV, para pular a linha de cabeçalho e evitar gerar um erro de Scan adiante. Como índice para a última linha foi passado um -1, indicando que será lido até o último registro.

Após uma checagem de erro, linhas 27 à 29, um defer é aplicado para fechar a linha ao final de cada laço for, linha 30.

A variável records será um slice de State‘s, para armazenar as informações, linha 32.

Na linha 34 é feito o laço for sobre cada registro (row) em table. Este laço se repete até que rows.Next() alcance o final da tabela.

A linha 35 apenas cria um data para receber os dados do registro atual. A mágica vem na linha 37 onde data é passado como ponteiro para ter seus atributos preenchidos com os valores do registro atual pelo rows.Scan(&data).

Na sequência é verificado se ocorreu algum erro, linhas 38 à 41, e os novos dados são armazenados em records, linha 42.

Por fim é feita mais uma checagem de erro no fechamento da última linha e os resultados são impressos, linhas 44 à 50.

Executando este código irá gerar a saída a seguir:

Bash

alves@suzail:csvutil_read$ go run read_struct.go
2022/08/01 16:28:07 EOF
[{São Paulo 41262199 46649132} {Minas Gerais 19597330 21411923} {Rio de Janeiro 15989929 17463349}
{Bahia 14016906 14985284} {Paraná 10444526 11597484} {Rio Grande do Sul 10693929 11466630} {Pernambuco
8796448 9674793} {Ceará 8452381 9240580} {Pará 7581051 8777124} {Santa Catarina 6248436 7338473} {Goiás
6003788 7206589} {Maranhão 6574789 7153262} {Amazonas 3483985 4269995} {Espírito Santo 3514952 4108508}
{Paraíba 3766528 4059905} {Mato Grosso 3035122 3567234} {Rio Grande do Norte 3168027 3560903} {Alagoas
3120494 3365351} {Piauí 3118360 3289290} {Distrito Federal 2570160 3094325} {Mato Grosso do Sul 2449024
2839188} {Sergipe 2068017 2338474} {Rondônia 1562409 1815278} {Tocantins 1383445 1607363} {Acre 733559
906876} {Amapá 669526 877613} {Roraima 450479 652713}]

Gravando um CSV com csvutil

O código a seguir implementa a escrita de um arquivo CSV passando os dados por meio de uma struct:

package main

import (
	"log"

	"go-hep.org/x/hep/csvutil"
)

type State struct {
	UF      string
	Pop2010 int
	Pop2021 int
}

func main() {
	table, err := csvutil.Open("populacao.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer table.Close()

	table1, err := csvutil.Create("pop.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer table1.Close()

	table1.Writer.Comma = ';'
	table1.Writer.UseCRLF = false

	rows, err := table.ReadRows(1, -1)
	if err != nil {
		log.Println(err)
	}
	defer rows.Close()

	for rows.Next() {
		data := State{}

		err = rows.Scan(&data)
		if err != nil {
			log.Println(err)
			continue
		}
		err = table1.WriteRow(data)
		if err != nil {
			log.Fatal(err)
		}
	}
	err = rows.Err()
	if err != nil {
		log.Println(err)
	}
	table1.WriteHeader("UF;Pop 2010;Pop 2021")
}

A ideia aqui foi transcrever o “populacao.csv” em uma versão um pouco diferente: com ponto e vírgula como separador de campo, “\n” como fim se linha e um novo cabeçalho.

Basicamente abri uma nova tabela, table1, com a função csvutil.Create("pop.csv"), linhas 22 à 26. Configurei um novo separador e final de linha, linhas 28 e 29. E depois, na linha 45, gravei os dados com o método table1.WriteRow(data). O método retorna um código de erro se houver algum, linhas 46 à 48.

Para terminar adicionei o cabeçalho da tabela, linha 54. Ao executar o table1.Close() os dados são gravados no arquivo “pop.csv“.

Executando o código o novo arquivo CSV é criado:

Bash

alves@suzail:csvutil_write$ go run write_struct.go
2022/08/01 18:13:04 EOF
alves@suzail:csvutil_write$ cat pop.csv
UF;Pop 2010;Pop 2021
São Paulo;41262199;46649132
Minas Gerais;19597330;21411923
Rio de Janeiro;15989929;17463349
Bahia;14016906;14985284
Paraná;10444526;11597484
...

O pacote csvutil é bem simples, mas me parece que limita o controde dos possíveis erros de leitura com o pacote.

CVS com Gota DataFrame

Trabalhar os dados de uma tabela manualmente, com muito diferentes campos pode ser trabalhoso e complexo, gerando muito código para garantir que os dados manuseados correspondam ao esperado. No entanto manusear estes Data Frames se mostram procedimentos muito padrões de forma que ferramentas mais poderosas tendem a ser mais largamente disponibilizadas com o tempo. A API Gota: DataFrames, Series and Data Wrangling provem um maravilhoso conjunto de ferramentas para tornar estes processos mais simples e automatizados. Embora o projeto ainda esteja em desenvolvimento já apresenta muitas funcionalidades úteis, algumas das quais vou apresentar a seguir.

Gota é uma implementação dos métodos de DataFrames, Series e Data Wranging para a linguagem de programação Go. Neste texto vou focar apenas em alguns aspectos mais básicos de módulo DataFrames.

Instalação do Gota

Para a instalação do DataFrame execute um go install do terminal como o comando abaixo:

Bash

alves@suzail:~$ go install github.com/kniren/gota/dataframe

Isto irá baixar o pacote DataFrame e instalar em go/src/github.com/kniren/gota/dataframe.

Para testar a instalação do DataFrame copie o arquivo “populacao.csv” para uma pasta e adicione o código abaixo:

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/kniren/gota/dataframe"
)

func main() {
	f, err := os.Open("populacao.csv")
	if err != nil {
		log.Fatal(err)
	}
  	defer f.Close()

	df := dataframe.ReadCSV(f)
	if err := df.Error(); err != nil {
		fmt.Println(err)
	}
  
	fmt.Println(df)
}

Executando este código o arquivo CSV será carregado pelo comando dataframe.ReadCSV e disponibilizado na variável df, linha 18. A impressão da linha 20 vai gerar uma saída como a abaixo:

Bash

alves@suzail:df_read$ go run df_read.go
[27×3] DataFrame
Unidade Federativa População (2010) População (2021)
0: São Paulo 41262199 46649132
1: Minas Gerais 19597330 21411923
2: Rio de Janeiro 15989929 17463349
3: Bahia 14016906 14985284
4: Paraná 10444526 11597484
5: Rio Grande do Sul 10693929 11466630
6: Pernambuco 8796448 9674793
7: Ceará 8452381 9240580
8: Pará 7581051 8777124
9: Santa Catarina 6248436 7338473
... ... ...
<string> <int> <int>

Observe que o DataFrame reconheceu que a primeira linha é o cabeçalho da tabela e que os campos são dos tipos string, inteiro e inteiro.

A checagem de erro é feita através da função df.Error(), linhas 19 à 21.

Documentação do Gota/DataFrame

Não encontrei um site com a documentação completa do projeto, mas você pode gerar a documentação do pacote a partir do godoc. Caso não o tenha instalado execute o comando a seguir, de um terminal:

Bash

alves@suzail:~$ go get golang.org/x/tools/cmd/godoc

Em seguida execute o comando a seguir para gerar a documentação de todos os pacotes instalado em seu $GOPATH:

Bash

alves@suzail:~$ godoc -http=localhost:6060

Isto vai instanciar um pequeno servidor html em sua máquina com toda a documentação. Em seguida abra o navegador na página http://localhost:6060 e procure por dataframe:

Como pode observar o pacote é bem extenso e não pretendo adentrar todos os aspectos do projeto.

Filtrando Elementos do DataFrame

Um filtro para selecionar elementos específicos da sua base de dados pode ser feito com a struct dataframe.F, cujos atributos são apresentados abaixo:

type F struct {
    Colidx     int
    Colname    string
    Comparator series.Comparator
    Comparando interface{}
}

Coldidx é um inteiro para indica a coluna a ser pesquisada e Colname o nome da coluna, bem útil se a primeira linha dos seus dados CSV contiver os nomes das colunas.

Comparator é o series.Comparator, uma string representante do operador ou a constante definida no pacote series, que será empregado para a comparação:

  • ==” ou series.Eq – igual
  • !=” ou series.Neg – diferente
  • >” ou series.Greater – maior que
  • >=” ou series.GreaterEq – maior ou igual
  • <” ou series.Less – menor que
  • <=” ou series.LessEq – menor ou igual
  • in” ou seriesIn – dentro
  • func” ou series.CompFunc – função de comparação passada

Comparando é o elemento de comparação que em geral é uma string, inteiro, float ou função de comparação.

Para o caso de uma função a ser passada para a filtragem, esta deve retornar uma função com a assinatura:

func(series.Element) bool 

Uma vez o filtro montado basta passar para o dataframe.Filter que retorna um dataframe com os dados filtrados.

O código a seguir implementa três filtragens ao dataframe carregado do arquivo CSVpopulacao.csv“:

package main

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/go-gota/gota/series"
	"github.com/kniren/gota/dataframe"
)

func main() {
	f, err := os.Open("populacao.csv")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	df := dataframe.ReadCSV(f)
	if err := df.Error(); err != nil {
		fmt.Println(err)
	}

	// ------ Estados com menos de 1M de habitantes -------
	fmt.Println("Estados com menos de 1 milhão de habitantes:")
	f1 := dataframe.F{
		Colname:    "População (2021)",
		Comparator: "<",
		Comparando: 1000000,
	}

	df1 := df.Filter(f1)
	if df1.Err != nil {
		fmt.Println(err)
	}
	fmt.Println(df1)

	// ------ Estados iniciados com R -------
	fmt.Println("Estados iniciados com a letra \"R\":")
	fsearch := func(prefix string) func(series.Element) bool {
		return func(e series.Element) bool {
			if e.Type() == "string" {
				if str, ok := e.Val().(string); ok {
					return strings.HasPrefix(str, prefix)
				}
			}
			return false
		}
	}
	f2 := dataframe.F{
		Colname:    "Unidade Federativa",
		Comparator: series.CompFunc,
		Comparando: fsearch("R"),
	}

	df2 := df.Filter(f2)
	if df2.Err != nil {
		fmt.Println(err)
	}
	fmt.Println(df2)

	// ------ Estados com menos de 1M de habitantes e iniciados por R -------
	fmt.Println("Estados iniciados com a letra \"R\" e com menos de 1 milhão de habitantes:")
	df3 := df1.Filter(f2)
	if df3.Err != nil {
		fmt.Println(err)
	}
	fmt.Println(df3)
}

Nas linhas 27 a 31 é definido o filtro f1 para os estados com população com menos de 1M de habitantes em 2021. O filtro é aplicado na linha 33, que gera um novo dataframe df1 com os registros filtrados por f1.

As linhas 41 à 50 definem uma função que retorna uma função de assinatura func(series.Element) bool para ser usada no filtro f2, que deve filtrar os estados iniciados pela letra “R“. Nas linhas 51 à 55 o filtro f2 é montado, e aplicado na linha 57 gerando o dataframe df2 com os estados filtrados pelo nome.

O último filtragem dos dados é a associação f1 AND f2: Ela é feita aplicando o filtro de nomes, f2, ao resultado da filtragem do filtro f1, o dataframe df1.

A saída deste código é apresentada a seguir:

Bash

alves@suzail:
df_filter$ go run df_filter.go
Estados com menos de 1 milhão de habitantes:
[3x3] DataFrame


Unidade Federativa População (2010) População (2021)
0: Acre 733559 906876
1: Amapá 669526 877613
2: Roraima 450479 652713

Estados iniciados com a letra "R":
[5x3] DataFrame

Unidade Federativa População (2010) População (2021)
0: Rio de Janeiro 15989929 17463349
1: Rio Grande do Sul 10693929 11466630
2: Rio Grande do Norte 3168027 3560903
3: Rondônia 1562409 1815278
4: Roraima 450479 652713

Estados iniciados com a letra "R" e com menos de 1 milhões de habitantes:
[1x3] DataFrame

Unidade Federativa População (2010) População (2021)
0: Roraima 450479 652713


FilterAggregation

 func (df DataFrame) FilterAggregation(agg Aggregation, filters ...F) DataFrame

Outra forma de associar filtros é empregando o FilterAggregation que recebe inicialmente um agregador agg, que pode ser um dataframe.And ou dataframe.Or, para a operação de agregação, seguido dos filtros a serem passados.

Por exemplo, a última filtragem no código anterior poderia ser feita com as linhas a seguir:

...
	df4 := df.FilterAggregation(dataframe.And, f1, f2)
	fmt.Println(df4)
...

Gota DataFrame ainda consegue trabalhar com JSON e possui muitas outras funções interessantes como Mutate, Arrange, Select, Subset entre outras. Estas funções permitem manipular linhas e colunas das tabelas, reorganizar os dados, ordenar entre outros. Aconselho ver a documentação para conhecer a API em detalhes.

Considerações Finais

Trabalhar arquivos CSV via biblioteca padrão gera mais código mas, em contrapartida, permite um melhor controle e tratamento dos eventuais erros nos dados trabalhados. O pacote csvutil traz ferramentas muito práticas permitindo carregar structs de forma praticamente direta. No entanto o controle e tratamento de erros nos arquivos CSV se torna mais limitado.

Já APIs mais completas com o Data Frame Gota trazem um conjunto de ferramentas mais completas e poderosas que lhe permite manipular os dados nos arquivos CSV com muito menos código e muitas possibilidades. A depender da necessidade em manipulação do conteúdo do CSV pode ser uma boa opção.

A meu gosto a biblioteca padrão me parece uma ótima opção, no entanto é bom conhecer outras possibilidades que podem ser mais convenientes em certas situações.

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.