Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


CSV para JSON com linguagem GO

Nesse texto será apresentado um exemplo de como como converter um arquivo CSV para JSON em linguagem GO.

Ao trabalho

Ao Trabalho! link original da imagem

Esse texto foi primeiro publicado na Revista /dev/all.

Dados reais para testes

Para projetos que estão no meu backlog pessoal (e que ainda não vou contar), preciso de uma certa quantidade de dados para fazer uns testes. Dessa forma, uma boa ideia é trabalhar com uma amostra dos arquivos do IMDb, conhecida como https://www.kaggle.com/carolzhangdc/imdb-5000. Ela tem uma quantidade razoável de registros, com dados que possibilitam trabalhar bem tanto em bancos de dados relacionais como NoSQL (meu foco agora são os NoSQL).

Assim, quando se precisa trabalhar com uma maior diversidade e quantidade de dados, um local fácil de achar coleções é o https://www.kaggle.com/, plataforma dedicada a desafios e competições no campo da ciência de dados, na qual estão disponíveis, além dessa amostra, muitos outros conjuntos.

Lendo o arquivo

Antes de mais nada, o primeiro desafio nessa jornada maior é pegar esses dados, que estão em arquivos CSV, e transformá-los em objetos JSON.

Antes de mais nada, deve-se ler o arquivo CSV:


package main

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

func main() {
    f, _ := os.Open("fixture/movie_metadata.csv")
    r := csv.NewReader(f)

    for {
        record, err := r.Read()
        if err == io.EOF {
            break
        }

        fmt.Println(record[11])
    }
}

Primeiramente, o arquivo é aberto e o leitor de CSV é inicializado, então o arquivo é lido até o fim, imprimindo o campo correspondente ao título.

Módulos padrão da GOLANG

De acordo com o KISS, foram usados apenas módulos padrão:

Inicialmente, Nessa primeira versão fiz o mínimo possível, deixando de lado coisas como passagem do nome do arquivo, verificar se ele existe e verificar se a entrada lida é válida.

A execução desse código gera uma saída que começa com:


go run loadscv.go | head
movie_title
Avatar 
Pirates of the Caribbean: At World's End 
Spectre 
The Dark Knight Rises 
Star Wars: Episode VII - The Force Awakens             
John Carter 
Spider-Man 3 
Tangled 
Avengers: Age of Ultron 
Harry Potter and the Half-Blood Prince 
Batman v Superman: Dawn of Justice 
Superman Returns 
signal: broken pipe

Perceberam o “broken pipe” na última linha? Falta um tratamento para erro na saída, mas isso é algo que irei trabalhar posteriormente.

Os dados

E o arquivo tem essa “cara”:


➤ head -2 fixture/movie_metadata.csv 
color,director_name,num_critic_for_reviews,duration,director_facebook_likes,actor_3_facebook_likes,actor_2_name,actor_1_facebook_likes,gross,genres,actor_1_name,movie_title,num_voted_users,cast_total_facebook_likes,actor_3_name,facenumber_in_poster,plot_keywords,movie_imdb_link,num_user_for_reviews,language,country,content_rating,budget,title_year,actor_2_facebook_likes,imdb_score,aspect_ratio,movie_facebook_likes
Color,James Cameron,723,178,0,855,Joel David Moore,1000,760505847,Action|Adventure|Fantasy|Sci-Fi,CCH Pounder,Avatar ,886204,4834,Wes Studi,0,avatar|future|marine|native|paraplegic,http://www.imdb.com/title/tt0499549/?ref_=fn_tt_tt_1,3054,English,USA,PG-13,237000000,2009,936,7.9,1.78,33000

Os campos são:


color
director_name
num_critic_for_reviews
duration
director_facebook_likes
actor_3_facebook_likes
actor_2_name,
actor_1_facebook_likes
gross
genres
actor_1_name
movie_title
num_voted_users
cast_total_facebook_likes
actor_3_name
facenumber_in_poster
plot_keywords
movie_imdb_link
num_user_for_reviews
language
country
content_rating
budget
title_year
actor_2_facebook_likes
imdb_score
aspect_ratio
movie_facebook_likes

CSV para JSON

Agora é pegar esses dados e meter em um JSON bonitão. O módulo, padrão para isso é o encoding/json. Há até, no blog da linguagem GO, um texto de introdução ao uso de JSON.

Fazendo algo bem básico, só pra testar o novo módulo:


package main

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

type Movie struct {
    Title string
    Year int
}

func main() {
    f, _ := os.Open("fixture/movie_metadata.csv")
    r := csv.NewReader(f)

    for {
        record, err := r.Read()
        if err == io.EOF {
            break
        }

        year, _ := strconv.Atoi(record[23])
        movieData := Movie {record[11], year}
        movieJson, _ := json.Marshal(movieData)
        fmt.Println(string(movieJson))
    }
}

Executando:


➤ go run loadscv.go|head
{"Title":"movie_title","Year":0}
{"Title":"Avatar ","Year":2009}
{"Title":"Pirates of the Caribbean: At World's End ","Year":2007}
{"Title":"Spectre ","Year":2015}
{"Title":"The Dark Knight Rises ","Year":2012}
{"Title":"Star Wars: Episode VII - The Force Awakens             ","Year":0}
{"Title":"John Carter ","Year":2012}
{"Title":"Spider-Man 3 ","Year":2007}
{"Title":"Tangled ","Year":2010}
{"Title":"Avengers: Age of Ultron ","Year":2015}
signal: broken pipe

Ao trocar o json.Marshal por json.MarshalIndent, tem-se uma saída mais amigável. Alterei, no código anteirior, o Marshal para movieJson, _ := json.MarshalIndent(movieData, “”, ” “), e obtive essa saída (a primeira linha do CSV é formado pelos nomes dos campos):


➤ go run loadscv.go |head -20
{
   "Title": "movie_title",
   "Year": 0,
   "Actors": [
      {
         "Name": "actor_1_name",
         "FacebookLikes": 0
      },
      {
         "Name": "actor_2_name",
         "FacebookLikes": 0
      },
      {
         "Name": "actor_3_name",
         "FacebookLikes": 0
      }
   ]
}
{
   "Title": "Avatar ",
signal: broken pipe

Alterando o comportamento

É possível configurar outros aspectos sa saída JSON, com metadados na definição do objeto. Por exemplo, se quisermos alterar o nome das chaves (os nomes são obtidos por reflexão, a partir do nome definido na estrutura), dado que os nomes públicos devem começar com letra maiúscula, o que não fica legal em uma saída JSON, basta informar esse nome na própria estrutura:


type Person struct {
    Name          string `json:"name"`
    FacebookLikes int    `json:"facebook_likes"`
}

type Movie struct {
    Title  string   `json:"title"`
    Year   int      `json:"year"`
    Actors []Person `json:"actors"`
}

Obtendo, por exemplo, essa saída (peguei apenas a segunda entrada):


{
   "title": "Avatar ",
   "year": 2009,
   "actors": [
      {
         "name": "CCH Pounder",
         "facebook_likes": 1000
      },
      {
         "name": "Joel David Moore",
         "facebook_likes": 936
      },
      {
         "name": "Wes Studi",
         "facebook_likes": 855
      }
   ]
}

Um outro modificador interessante é o omitempty, que não gera saída para campos considerados nulos (zero, falso, nil, entre outros). Usando ele para ano, deixaria a declaração para Year assim:


Year   int      `json:"year,omitempty"`

Aprimorando

As tarefas restantes para resolver esse problema, ainda temos, em cada linha do CSV, gêneros, com uma lista separada por | contendo palavras que tipificam o filme. Esses campos podem ter tratamentos diferentes em relações à sua normalização, e são especialmente interessantes por isso.

Como o que precisa ser feito ainda tem mais a ver com outros aspectos do GO, que não especificamente JSON, podem ser tratados normalmente, e para você, caro leitor, ficam como um desafio, especialmente para os que estamos iniciando nessa linguagem.

Outros materiais

Esses textos me ajudaram muito nessa empreitada: