Índice
- 1. Lendo CSV com a biblioteca padrão
- 1.1. Struct Reader
- 1.1.1. Atributo Comma rune
- 1.1.2. Atributo Comment rune
- 1.1.3. Atributo FieldsPerRecord int
- 1.1.4. Atributo LazyQuotes bool
- 1.1.5. Atributo TrimLeadingSpace bool
- 1.1.6. Atributo ReuseRecord bool
- 1.1.6.1. Exemplo: reader_atributos.go – Testa os diferentes atributos do Reader
- 1.1.7. csv.NewReader
- 1.1.8. Método Read, ReadAll e FieldPos
- 1.2. Struct Write
- 1.2.1. Atributo Comma
- 1.2.2. Atributo UseCRLF
- 1.2.3. Função NewWriter
- 1.2.4. Função Flush
- 1.2.5. Função Write
- 1.2.6. Função WriteAll
- 1.2.7. Função Error
- 2. Pacote csvutil
- 2.1. Instalação do csvutil
- 2.2. Abrindo Arquivo CSV
- 2.2.1. O Tipo Table
- 2.2.2. Tipo Rows
- 2.2.3. Lendo um Arquivo CSV com csvutil
- 2.2.4. Gravando um CSV com csvutil
- 3. CVS com Gota DataFrame
- 3.1. Instalação do Gota
- 3.2. Documentação do Gota/DataFrame
- 3.3. Filtrando Elementos do DataFrame
- 3.3.1. FilterAggregation
- 4. Considerações Finais
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 Federativa | População (2010) | População (2021) |
---|---|---|
São Paulo | 41 262 199 | 46 649 132 |
Minas Gerais | 19 597 330 | 21 411 923 |
Rio de Janeiro | 15 989 929 | 17 463 349 |
Bahia | 14 016 906 | 14 985 284 |
Paraná | 10 444 526 | 11 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 umlog.Println
na linha 46, e linha 47 a adição docontinue
; - adição da matriz de strings
records
, operado nas linhas 39 e 49 e 52, para poder explorar melhor o atributoReuseRecord
, 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 CSV “populacao.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:
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.
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:
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 CSV “populacao.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:
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:
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:
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:
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:
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
:
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:
- “
==
” ouseries.Eq
– igual - “
!=
” ouseries.Neg
– diferente - “
>
” ouseries.Greater
– maior que - “
>=
” ouseries.GreaterEq
– maior ou igual - “
<
” ouseries.Less
– menor que - “
<=
” ouseries.LessEq
– menor ou igual - “
in
” ouseriesIn
– 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 CSV “populacao.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:
df_filter
alves@suzail:$ 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.