Limpiando Sus Datos Con Go: Parte 1
Spanish (Español) translation by Valeria Angulo (you can also view the original English article)
Panorama
Uno de los aspectos más importantes de cualquier aplicación es validar su entrada. El enfoque más básico simplemente está fallando si la entrada no cumple con los requisitos. Sin embargo, en muchos casos esto no es suficiente. En muchos sistemas, la recopilación de datos está separada del análisis de datos. Podría ser una encuesta o un conjunto de datos antiguo.
En estos casos, es necesario revisar todo el conjunto de datos antes del análisis, detectar datos no válidos o faltantes, corregir lo que se puede arreglar y marcar o eliminar datos que no se pueden salvar. También es útil proporcionar estadísticas sobre la calidad de los datos y qué tipo de errores se encontraron.
En esta serie de dos partes, aprenderá cómo usar las funciones de texto de Go, cortar en partes más pequeñas cubos de archivos CSV y garantizar que sus datos estén impecablemente limpios. En la primera parte, nos enfocaremos en la base del procesamiento de texto en Go-bytes, runas y cadenas, y también trabajaremos con archivos CSV.
Texto en Go
Antes de sumergirnos en la limpieza de datos, comencemos con la base del texto en Go. Los bloques de construcción son bytes, runes y cadenas. Veamos qué representa cada uno y cuáles son las relaciones entre ellos.
Bytes
Los bytes son números de 8 bits. Cada byte puede representar uno de los posibles 256 valores (2 a la potencia de 8). Cada carácter en el conjunto de caracteres ASCII se puede representar con un solo byte. Pero los bytes no son caracteres. La razón es que Go es un lenguaje moderno que admite Unicode, donde hay mucho más de 256 caracteres separados. Ingresa runes.
Runes
Un rune en Go es otro nombre para el tipo int32. Esto significa que cada rune puede representar más de cuatro mil millones de valores separados (2 a la potencia de 32), lo cual es suficiente para cubrir todo el conjunto de caracteres Unicode.
En el siguiente código, puede ver que el rune 'Δ' (alt-J en Mac) es solo un int32. Para imprimir el caracter que representa en la pantalla, tengo que convertirlo en una cadena.
1 |
package main |
2 |
|
3 |
import ( |
4 |
"fmt"
|
5 |
)
|
6 |
|
7 |
|
8 |
func main() { |
9 |
r := '∆' |
10 |
fmt.Println(r) |
11 |
fmt.Println(int32(r)) |
12 |
fmt.Println(string(r)) |
13 |
}
|
14 |
|
15 |
Output: |
16 |
|
17 |
8710
|
18 |
8710
|
19 |
∆
|
Unicode es complicado. Un rune representa oficialmente un punto de código Unicode. Los caracteres Unicode generalmente están representados por un único punto de código Unicode, pero a veces más de uno.
Cadenas
Las cadenas son oficialmente segmentos de bytes de solo lectura. Si indexas una cadena, obtienes un byte de vuelta:
1 |
func main() { |
2 |
s := "abc" |
3 |
for i := 0; i < len(s); i++ { |
4 |
fmt.Println(s[i]) |
5 |
}
|
6 |
}
|
7 |
|
8 |
Output: |
9 |
|
10 |
97
|
11 |
98
|
12 |
99
|
Los literales de cadena son una secuencia de caracteres UTF-8 encerrados entre comillas dobles. Pueden contener secuencias de escape, que son una barra invertida seguida de un carácter ASCII como\n (nueva línea) o\t (tabulador). Ellos tienen significados especiales. Aquí está la lista completa:
1 |
\a U+0007 alert or bell |
2 |
\b U+0008 backspace |
3 |
\f U+000C form feed |
4 |
\n U+000A line feed or newline |
5 |
\r U+000D carriage return |
6 |
\t U+0009 horizontal tab |
7 |
\v U+000b vertical tab |
8 |
\\ U+005c backslash |
9 |
\' U+0027 single quote (valid only within rune literals) |
10 |
\" U+0022 double quote (valid only within string literals) |
A veces es posible que desee almacenar bytes literales directamente en una cadena, independientemente de las secuencias de escape. Podrías escapar cada barra invertida, pero eso es tedioso. Un enfoque mucho mejor es usar cadenas sin formato que están encerradas en tildes de retroceso.
Aquí hay un ejemplo de una cadena con una secuencia de escape\t (tab), que se representa una vez como está, luego con la barra invertida de escape, y luego como una cadena sin formato:
1 |
func main() { |
2 |
s1 := "1\t2" |
3 |
s2 := "1\\t2" |
4 |
s3 := `1\t2` |
5 |
|
6 |
fmt.Println(s1) |
7 |
fmt.Println(s2) |
8 |
fmt.Println(s3) |
9 |
}
|
10 |
|
11 |
Output: |
12 |
|
13 |
1 2 |
14 |
1\t2 |
15 |
1\t2 |
Si bien las cadenas son porciones de bytes, cuando iteras sobre una cadena con una instrucción for-range, obtienes un rune en cada iteración. Esto significa que puede obtener uno o más bytes. Esto es fácil de ver con el índice de rango. Aquí hay un ejemplo loco. La palabra hebrea "שלום" significa "Hola" (y paz). El hebreo también está escrito de derecha a izquierda. Construiré una cadena que mezcle la palabra hebrea con su traducción al inglés.
Luego, imprimiré rune por rune, incluido el índice de bytes de cada rune dentro de la cadena. Como verá, cada rune Hebrea toma dos bytes, mientras que los caracteres Ingleses toman un byte, por lo que la longitud total de esta cadena es de 16 bytes, aunque tiene cuatro caracteres hebreos, tres símbolos y cinco caracteres Ingleses (12 caracteres). Además, los caracteres hebreos se mostrarán de derecha a izquierda:
1 |
func main() { |
2 |
hello := "שלום = hello" |
3 |
fmt.Println("length:", len(hello)) |
4 |
for i, r := range(hello) { |
5 |
fmt.Println(i, string(r)) |
6 |
}
|
7 |
}
|
8 |
|
9 |
Output: |
10 |
|
11 |
length: 16 |
12 |
0 ש |
13 |
2 ל |
14 |
4 ו |
15 |
6 ם |
16 |
8
|
17 |
9 = |
18 |
10
|
19 |
11 h |
20 |
12 e |
21 |
13 l |
22 |
14 l |
23 |
15 o |
Todos estos matices pueden ser extremadamente importantes cuando tienes un conjunto de datos para limpiar con comillas extrañas y una combinación de caracteres y símbolos Unicode.
Al imprimir cadenas y segmentos de bytes, existen varios especificadores de formato que funcionan igual en ambos. El formato %s imprime los bytes como está, %x imprime dos caracteres hexadecimales en minúsculas por byte, %X imprime dos caracteres hexadecimales en mayúsculas por byte, y %q imprime una cadena de comillas doble escapada con sintaxis go.
Para escapar del signo % dentro de un especificador de cadena de formato, simplemente duplíquelo. Para separar los bytes al usar %x o %X, puede agregar un espacio, como en "% x" y "% X". Aquí está la demostración:
1 |
func main() { |
2 |
s := "שלום" |
3 |
|
4 |
fmt.Printf("%%s format: %s\n", s) |
5 |
fmt.Printf("%%x format: %x\n", s) |
6 |
fmt.Printf("%%X format: %X\n", s) |
7 |
fmt.Printf("%% x format: % x\n", s) |
8 |
fmt.Printf("%% X format: % X\n", s) |
9 |
fmt.Printf("%%q format: %q\n", s) |
10 |
}
|
11 |
|
12 |
Output: |
13 |
|
14 |
%s format: שלום |
15 |
%x format: d7a9d79cd795d79d |
16 |
%X format: D7A9D79CD795D79D |
17 |
% x format: d7 a9 d7 9c d7 95 d7 9d |
18 |
% X format: D7 A9 D7 9C D7 95 D7 9D |
19 |
%q format: "שלום" |
Leyendo y Escribiendo Archivos CSV
Los datos pueden llegar de muchas formas y formatos. Uno de los formatos más comunes es CSV (valores separados por comas). Los datos CSV son muy eficientes. Los archivos suelen tener una línea de encabezado con el nombre de los campos o columnas y filas de datos donde cada fila contiene un valor por campo, separados por comas.
Aquí hay un pequeño fragmento de un conjunto de datos de avistamientos de OVNI (en realidad). La primera fila (encabezado) contiene los nombres de las columnas y las otras líneas contienen los datos. Puede ver que a menudo la columna "Colores Informados" está vacía:
1 |
City,Colors Reported,Shape Reported,State,Time |
2 |
Ithaca,,TRIANGLE,NY,6/1/1930 22:00 |
3 |
Willingboro,,OTHER,NJ,6/30/1930 20:00 |
4 |
Holyoke,,OVAL,CO,2/15/1931 14:00 |
5 |
Abilene,,DISK,KS,6/1/1931 13:00 |
6 |
New York Worlds Fair,,LIGHT,NY,4/18/1933 19:00 |
7 |
Valley City,,DISK,ND,9/15/1934 15:30 |
8 |
Crater Lake,,CIRCLE,CA,6/15/1935 0:00 |
9 |
Alma,,DISK,MI,7/15/1936 0:00 |
10 |
Eklutna,,CIGAR,AK,10/15/1936 17:00 |
11 |
Hubbard,,CYLINDER,OR,6/15/1937 0:00 |
12 |
Fontana,,LIGHT,CA,8/15/1937 21:00 |
13 |
Waterloo,,FIREBALL,AL,6/1/1939 20:00 |
14 |
Belton,RED,SPHERE,SC,6/30/1939 20:00 |
Escribir este fragmento de datos CSV en un archivo implica algunas operaciones de cadena, así como trabajar con archivos. Antes de sumergirnos en la lógica principal, aquí están las partes obligatorias: la definición del paquete, las importaciones y la cadena de datos (tenga en cuenta el uso de const).
1 |
package main |
2 |
|
3 |
import ( |
4 |
"os"
|
5 |
"strings"
|
6 |
"bufio"
|
7 |
)
|
8 |
|
9 |
data := ` |
10 |
City,Colors Reported,Shape Reported,State,Time |
11 |
Ithaca,,TRIANGLE,NY,6/1/1930 22:00 |
12 |
Willingboro,,OTHER,NJ,6/30/1930 20:00 |
13 |
Holyoke,,OVAL,CO,2/15/1931 14:00 |
14 |
Abilene,,DISK,KS,6/1/1931 13:00 |
15 |
New York Worlds Fair,,LIGHT,NY,4/18/1933 19:00 |
16 |
Valley City,,DISK,ND,9/15/1934 15:30 |
17 |
Crater Lake,,CIRCLE,CA,6/15/1935 0:00 |
18 |
Alma,,DISK,MI,7/15/1936 0:00 |
19 |
Eklutna,,CIGAR,AK,10/15/1936 17:00 |
20 |
Hubbard,,CYLINDER,OR,6/15/1937 0:00 |
21 |
Fontana,,LIGHT,CA,8/15/1937 21:00 |
22 |
Waterloo,,FIREBALL,AL,6/1/1939 20:00 |
23 |
Belton,RED,SPHERE,SC,6/30/1939 20:00 |
24 |
`
|
La función main() crea un archivo llamado "ufo-sightings.csv", comprueba que no hay ningún error y luego crea un escritor en búfer w. La llamada defer en la siguiente línea, que vacía el contenido del búfer al archivo, se ejecuta al final de la función. Ese es el significado de defer. Luego, usa la función Split()del paquete de cadenas para dividir las cadenas de datos en líneas individuales.
Luego, dentro del for-loop, el espacio en blanco inicial y final se recorta en cada línea. Las líneas vacías se omiten y las líneas no vacías se escriben en el búfer, seguidas de un carácter de nueva línea. Eso es. El búfer se descargará al archivo al final.
1 |
func main() { |
2 |
f, err := os.Create("ufo-sightings.csv") |
3 |
if err != nil { |
4 |
panic(e) |
5 |
}
|
6 |
|
7 |
w := bufio.NewWriter(f) |
8 |
defer w.Flush() |
9 |
lines := strings.Split(data, "\n") |
10 |
for _, line := range lines { |
11 |
line := strings.Trim(line, " ") |
12 |
if line == "" { |
13 |
continue
|
14 |
}
|
15 |
w.WriteString(line) |
16 |
w.WriteString("\n") |
17 |
}
|
18 |
}
|
Leer del archivo es bastante simple:
1 |
package main |
2 |
|
3 |
import ( |
4 |
"fmt"
|
5 |
"io/ioutil"
|
6 |
)
|
7 |
|
8 |
|
9 |
func main() { |
10 |
data, err := ioutil.ReadFile("ufo-sightings.csv") |
11 |
if err != nil { |
12 |
panic(err) |
13 |
}
|
14 |
|
15 |
fmt.Println(string(data)) |
16 |
}
|
Conclusión
Go tiene facilidades sólidas para tratar el texto de todas las formas y codificaciones. En esta parte de la serie, examinamos los conceptos básicos de la representación de texto en Go, el procesamiento de texto utilizando el paquete de cadenas y el manejo de archivos CSV.
En la segunda parte, pondremos en práctica lo que aprendimos para limpiar los datos confusos en preparación para el análisis.



