Es momento de mostrar el resultado al primer desafío del “progamador oxidado”.
Recordemos el desafío:
El desafío de las ciudades
El sitio “world cities database” tiene una lista de las ciudades del mundo, que puedes descargar (elige la versión basic gratuita para este desafío).
Las primeras líneas de esta base de datos se ven así:
"city","city_ascii","lat","lng","country","iso2","iso3","admin_name","capital","population","id"
"Tokyo","Tokyo","35.6897","139.6922","Japan","JP","JPN","Tōkyō","primary","37732000","1392685764"
"Jakarta","Jakarta","-6.1750","106.8275","Indonesia","ID","IDN","Jakarta","primary","33756000","1360771077"
"Delhi","Delhi","28.6100","77.2300","India","IN","IND","Delhi","admin","32226000","1356872604"
"Guangzhou","Guangzhou","23.1300","113.2600","China","CN","CHN","Guangdong","admin","26940000","1156237133"
"Mumbai","Mumbai","19.0761","72.8775","India","IN","IND","Mahārāshtra","admin","24973000","1356226629"
"Manila","Manila","14.5958","120.9772","Philippines","PH","PHL","Manila","primary","24922000","1608618140"
"Shanghai","Shanghai","31.1667","121.4667","China","CN","CHN","Shanghai","admin","24073000","1156073548"
"São Paulo","Sao Paulo","-23.5500","-46.6333","Brazil","BR","BRA","São Paulo","admin","23086000","1076532519"
"Seoul","Seoul","37.5600","126.9900","South Korea","KR","KOR","Seoul","primary","23016000","1410836482"
El desafío es el siguiente:
Transforma este archivo a formato JSON.
Suma el valor del atributo
population
Lista los nombres de los países y su código de 2 letras (iso2) sin repetición
Todo esto debe hacerse usando JQ.
Solución
Primero analicemos el archivo. Como no está en formato JSON lo mejor es leerlo en modo “raw", con la opción -R, así:
head -2 worldcities.csv | jq -R ‘.’
Esto nos arroja lo siguiente:
"\"city\",\"city_ascii\",\"lat\",\"lng\",\"country\",\"iso2\",\"iso3\",\"admin_name\",\"capital\",\"population\",\"id\"\r"
"\"Tokyo\",\"Tokyo\",\"35.6897\",\"139.6922\",\"Japan\",\"JP\",\"JPN\",\"Tōkyō\",\"primary\",\"37732000\",\"1392685764\"\r"
La opción -s, o —slurp combinada con raw nos permite convertir toda la entrada en un sólo string:
head -2 worldcities.csv | jq -R -s ‘.’
Obteniendo esto:
head -2 worldcities.csv| jq -R -s '.'
"\"city\",\"city_ascii\",\"lat\",\"lng\",\"country\",\"iso2\",\"iso3\",\"admin_name\",\"capital\",\"population\",\"id\"\r\n\"Tokyo\",\"Tokyo\",\"35.6897\",\"139.6922\",\"Japan\",\"JP\",\"JPN\",\"Tōkyō\",\"primary\",\"37732000\",\"1392685764\"\r\n"
Noten que ahora cada linea se separa con “\r\n”
, así que podemos usar este hecho para convertir todo en un arreglo, usando la función split()
head -2 worldcities.csv| jq -R -s 'split("\r\n")'
[ "\"city\",\"city_ascii\",\"lat\",\"lng\",\"country\",\"iso2\",\"iso3\",\"admin_name\",\"capital\",\"population\",\"id\"",
"\"Tokyo\",\"Tokyo\",\"35.6897\",\"139.6922\",\"Japan\",\"JP\",\"JPN\",\"Tōkyō\",\"primary\",\"37732000\",\"1392685764\"",
""
]
Pero tenemos esas comillas demás, así que las vamos a eliminar usando sub de este modo:
head -2 worldcities.csv|jq -R -s 'split("\r\n")|map(sub("\\\"";"";"g"))'
[ "city,city_ascii,lat,lng,country,iso2,iso3,admin_name,capital,population,id", "Tokyo,Tokyo,35.6897,139.6922,Japan,JP,JPN,Tōkyō,primary,37732000,1392685764",
""
]
Fíjense que he mapeado cada string del arreglo a sub, esto hace que en cada string se eliminen las comillas escapadas (\”).
Ahora cada string lo podemos volver a separar por comas usando split
:
head -2 worldcities.csv
| jq -R -s 'split("\r\n") | map(sub("\\\"";"";"g")| split(","))'
Obteniendo lo que queríamos:
[
[
"city",
"city_ascii",
"lat",
"lng",
"country",
"iso2",
"iso3",
"admin_name",
"capital",
"population",
"id"
],
[
"Tokyo",
"Tokyo",
"35.6897",
"139.6922",
"Japan",
"JP",
"JPN",
"Tōkyō",
"primary",
"37732000",
"1392685764"
],
[]
]
Ahora tenemos un arreglo con cada uno de los valores de los atributos, y el primer elemento de este arreglo corresponde a los nombres de los atributos. Noten que tenemos un arreglo vacío al final.
El elemento 0 de este arreglo de resultado es nuestro encabezado que almacenaremos en una variable que llamaremos $head,
esto se logra así:
head -2 worldcities.csv| jq -R -s
'split("\r\n") | map(sub("\\\"";"";"g")|split(","))
| .[0] as $head | $head'
[
"city",
"city_ascii",
"lat",
"lng",
"country",
"iso2",
"iso3",
"admin_name",
"capital",
"population",
"id"
]
El resto del arreglo lo guardaremos en una variable que llamaremos $body
, de este modo:
head -2 worldcities.csv| jq -R -s 'split("\r\n") | map(sub("\\\"";"";"g")|split(",")) | .[1:] as $body | $body'
[
[
"Tokyo",
"Tokyo",
"35.6897",
"139.6922",
"Japan",
"JP",
"JPN",
"Tōkyō",
"primary",
"37732000",
"1392685764"
],
[]
]
Pero vamos a eliminar ese molesto arreglo vacío, para eso usamos el comando select, que nos permite aplicar un filtro, de este modo:
head -2 worldcities.csv| jq -R -s 'split("\r\n") | map(sub("\\\"";"";"g")|split(",")) | .[1:][] | select(length > 0) as $body | $body'
[
"Tokyo",
"Tokyo",
"35.6897",
"139.6922",
"Japan",
"JP",
"JPN",
"Tōkyō",
"primary",
"37732000",
"1392685764"
]
Bien, ahora que sabemos separar nuestro arreglo en un encabezado y un cuerpo, podemos intentar crear un objeto.
La función reduce
nos va a ayudar. Esta función, que es típica de los lenguajes funcionales, recorre una lista aplicando una función a cada elemento y lo “acumula” en un solo valor.
Hagamos un experimento:
head -2 worldcities.csv
| jq -R -s 'split("\r\n")
| map(sub("\\\"";"";"g")|split(","))
| .[0] as $head
| reduce $head[] as $item (""; . + "|" + $item)'
"|city|city_ascii|lat|lng|country|iso2|iso3|admin_name|capital|population|id"
Lo que hicimos fue concatenar los nombres de los atributos, colocando una barra entre cada uno.
Vamos a crear un objeto con cada atributo con valor vacío (““
):
head -2 worldcities.csv
| jq -R -s 'split("\r\n")
| map(sub("\\\"";"";"g")|split(","))
| .[0] as $head
| reduce range(0;$head|length) as $i ({}; .[$head[$i]] = "")'
{
"city": "",
"city_ascii": "",
"lat": "",
"lng": "",
"country": "",
"iso2": "",
"iso3": "",
"admin_name": "",
"capital": "",
"population": "",
"id": ""
}
La función range(ini;end)
genera una lista de números desde ini
hasta el valor previo a end
, en nuestro caso, desde 0 hasta 1 menos que el largo del arreglo $head.
Notar que estamos reduciendo sobre un objeto {},
en JavaScript y en JQ el atributo attr
del objeto obj
se puede acceder así: obj.attr
o así: obj[“attr”].
Entonces, en cada iteración asignamos al objeto al valor de cada atributo en $head
el valor ““.
Recordemos que sabemos como guardar el resto de los elementos del arreglo en la variable $body, así que la podemos agregar a nuesta función reduce para crear un objeto por cada elemento de $body, de este modo:
head -3 worldcities.csv
| jq -R -s 'split("\r\n")
| map(sub("\\\"";"";"g")|split(","))
| .[0] as $head
| reduce(.[1:][] | select(length>0)) as $body
([]
; . + [$body
| reduce range(0;$head|length) as $i
({}; .[$head[$i]] = $body[$i])
]
)'
[
{
"city": "Tokyo",
"city_ascii": "Tokyo",
"lat": "35.6897",
"lng": "139.6922",
"country": "Japan",
"iso2": "JP",
"iso3": "JPN",
"admin_name": "Tōkyō",
"capital": "primary",
"population": "37732000",
"id": "1392685764"
},
{
"city": "Jakarta",
"city_ascii": "Jakarta",
"lat": "-6.1750",
"lng": "106.8275",
"country": "Indonesia",
"iso2": "ID",
"iso3": "IDN",
"admin_name": "Jakarta",
"capital": "primary",
"population": "33756000",
"id": "1360771077"
}
]
Para calcular la suma del atributo “population”
, simplemente creamos un arreglo con los valores de este atributo y usamos la función add:
cat worldcities.csv
| jq -R -s 'split("\r\n")
| map(sub("\\\"";"";"g")|split(","))
| .[0] as $head
| reduce(.[1:][] | select(length>0)) as $body
([]; . + [$body
| reduce range(0;$head|length) as $i
({}; .[$head[$i]] = $body[$i] )
])
| [.[].population | tonumber?]
| add'
5038931800
Nota que usé la función tonumber
con el operador ?
, que permite filtrar todos los valores numéricos.
Y para obtener los paises y sus códigos usamos la función unique_by:
cat worldcities.csv
| jq -R -s 'split("\r\n")
| map(sub("\\\"";"";"g")|split(","))
| .[0] as $head
| reduce(.[1:][] | select(length>0)) as $body
([]; . + [$body
| reduce range(0;$head|length) as $i
({}; .[$head[$i]] = $body[$i] )
])
| unique_by(.country)
| map({country, iso2})'
[
{
"country": "Afghanistan",
"iso2": "AF"
},
{
"country": "Albania",
"iso2": "AL"
},
{
"country": "Algeria",
"iso2": "DZ"
},
{
"country": "American Samoa",
"iso2": "AS"
},
...
Conclusión
Como pueden ver, JQ es muy poderoso como lenguaje. De hecho, es un lenguaje funcional, y Turing completo, así que podemos hacer cualquier tipo de programa. Por supuesto, no necesariamente será la forma más eficiente de resolver algunos problemas, pero como hemos visto con este ejercicio, se pueden procesar complejas estructuras de datos, así que esta es una herramienta que considero fundamental dominar, sobre todo si te toca hacer análisis de datos semiestructurados, como en JSON.
Si encuentras esto interesante, y crees que puede servirle a algún colega, te invito a compartir este artículo presionando el siguiente botón.
Y si quieres seguir aprendiendo puedes suscribirte a este newsletter:
Nos vemos la semana que viene.