Capítulo 4. Trabajar con datos de archivos y fuentes en Python
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
En el Capítulo 3, nos centramos en las muchas características que contribuyen a la calidad de los datos: desde la exhaustividad, coherencia y claridad de la integridad de los datos hasta la fiabilidad, validez y representatividad del ajuste de los datos. Hablamos de la necesidad de "limpiar" y normalizar los datos, así como de la necesidad de aumentarlos combinándolos con otros conjuntos de datos. Pero, ¿cómo logramos realmente estas cosas en la práctica?
Obviamente, es imposible empezar a evaluar la calidad de un conjunto de datos sin revisar primero su contenido, pero a veces es más fácil decirlo que hacerlo. Durante décadas, la gestión de datos fue una actividad muy especializada, que llevó a empresas y organizaciones a crear toda una serie de formatos de datos digitales distintos (y a veces patentados) diseñados para satisfacer sus necesidades particulares. A menudo, estos formatos venían con sus propias extensiones de archivo, algunas de las cuales puede que hayas visto: xls, csv, dbf y spss son formatos de archivo típicamente asociados con archivos de "datos".1 Aunque sus estructuras y detalles específicos varían, todos estos formatos son lo que yo describiría como basados en archivos,es decir, contienen (más o menos) datos históricos en archivos estáticos que pueden descargarse de una base de datos, ser enviados por correo electrónico por un colega o a los que se puede acceder a través de sitios de intercambio de archivos. Lo más significativo es que un conjunto de datos basado en archivos contendrá, en su mayor parte, la misma información tanto si lo abres hoy como dentro de una semana, un mes o un año.
En la actualidad, estos formatos basados en archivos contrastan con los formatos de datos y las interfaces que han surgido junto a los servicios web en tiempo real en los últimos 20 años. Hoy en día, los datos basados en la web están disponibles para todo, desde las noticias hasta el monitoreo del tiempo o los sitios de medios sociales, y estas fuentes de datos de estilo feed tienen sus propios formatos y estructuras únicos. Extensiones como xml, json y rss indican este tipo de datos en tiempo real, a los que a menudo hay que acceder mediante interfaces de programación de aplicaciones especializadas, o API. A diferencia de los formatos basados en archivos, acceder a la misma ubicación de datos basada en la web o "punto final" a través de una API siempre te mostrará los datos más recientes disponibles, y esos datos pueden cambiar en días, horas o incluso segundos.
No son distinciones perfectas, por supuesto. Hay muchas organizaciones (especialmente departamentos gubernamentales) que proporcionan datos basados en archivos para su descarga, pero luego sobrescriben esos archivos con otros nuevos que tienen el mismo nombre cada vez que se actualizan los datos de origen. Al mismo tiempo, los formatos de datos tipo feed pueden descargarse y guardarse para futuras consultas, pero su ubicación de origen en Internet no suele permitir el acceso a versiones anteriores. Sin embargo, a pesar de estos usos a veces poco convencionales de cada clase de formato de datos, en la mayoría de los casos puedes utilizar las diferencias de alto nivel entre los formatos de datos basados en archivos y los basados en fuentes para ayudarte a elegir las fuentes más apropiadas para un determinado proyecto de manipulación de datos.
¿Cómo sabes si quieres datos basados en archivos o en fuentes? En muchos casos, no tienes elección. Las empresas de medios de comunicación social, por ejemplo, facilitan el acceso a sus fuentes de datos a través de sus API, pero no suelen proporcionar datos retrospectivos. Otros tipos de datos -especialmente los que se sintetizan a partir de otras fuentes o se revisan exhaustivamente antes de su publicación- es mucho más probable que estén disponibles en formatos basados en archivos. Si puedes elegir entre los formatos basados en archivos y los basados en fuentes, la elección dependerá realmente de la naturaleza de tu pregunta sobre la gestión de datos: si se trata de disponer de los datos más recientes, probablemente sea preferible un formato basado en fuentes. Pero si te preocupan las tendencias, los datos basados en archivos, que es más probable que contengan información recopilada a lo largo del tiempo, serán probablemente tu mejor opción. Dicho esto, incluso cuando ambos formatos están disponibles, no hay garantía de que contengan los mismos campos, lo que, una vez más, puede hacer que te decidas por .
A lo largo de este capítulo, trabajaremos con ejemplos prácticos de manipulación de datos de varios de los formatos de datos basados en archivos y fuentes más comunes, con el objetivo de facilitar su revisión, limpieza, aumento y análisis. También echaremos un vistazo a algunos de los formatos de datos más difíciles de manejar, con los que podrías tener que trabajar estrictamente por necesidad. A lo largo de estos procesos, nos apoyaremos en gran medida en la excelente variedad de bibliotecas que la comunidad Python ha desarrollado para estos fines, incluidas bibliotecas y programas especializados para procesar desde hojas de cálculo hasta imágenes. Cuando terminemos, tendrás las habilidades y los scripts de ejemplo que necesitas para enfrentarte a una gran variedad de proyectos de manipulación de datos, ¡preparando el camino para tu próximo proyecto de manipulación de datos!
Datos estructurados frente a datos no estructurados
Antes de que nos sumerja en la escritura de código y la gestión de datos, quiero hablar brevemente de otro atributo clave de las fuentes de datos que puede influir en la dirección (y la velocidad) de tus proyectos de gestión de datos: trabajar con datos estructurados frente a datos no estructurados.
El objetivo de la mayoría de los proyectos de manipulación de datos es generar conocimientos y, a menudo, utilizar los datos para tomar mejores decisiones. Pero las decisiones son sensibles al tiempo, por lo que nuestro trabajo con los datos también requiere equilibrar compensaciones: en lugar de esperar al conjunto de datos "perfecto", podemos combinar dos o tres no tan perfectos para construir una aproximación válida del fenómeno que estamos investigando, o podemos buscar conjuntos de datos que compartan identificadores comunes (por ejemplo, códigos postales), aunque eso signifique que tengamos que derivar más tarde la estructura dimensional concreta (como el vecindario) que realmente nos interesa. Siempre que podamos obtener estas eficiencias sin sacrificar demasiado la calidad de los datos, mejorar la puntualidad de nuestro trabajo con los datos también puede aumentar su impacto.
Una de las formas más sencillas de hacer que nuestro manejo de datos sea más eficaz es buscar formatos de datos a los que Python y otras herramientas informáticas puedan acceder y comprender fácilmente. Aunque los avances en visión por ordenador, procesamiento del lenguaje natural y aprendizaje automático han facilitado a los ordenadores el análisis de datos independientemente de su estructura o formato subyacente, lo cierto es que los datos estructurados y legibles por máquina siguen siendo -sin que resulte sorprendente, quizás- el tipo de datos más sencillo con el que trabajar. En realidad, aunque cualquier cosa, desde entrevistas a imágenes o el texto de los libros, puede utilizarse como fuente de datos, cuando muchos de nosotros pensamos en "datos", solemos pensar en datos estructurados y numéricos más que en cualquier otra cosa.
Los datosestructurados son cualquier tipo de datos que se han organizado y clasificado de alguna manera, en alguna versión de registros y campos. En los formatos basados en archivos, suelen ser filas y columnas; en los formatos basados en fuentes, suelen ser (esencialmente) listas de objetos o diccionarios.
Los datosno estructurados, por el contrario, pueden consistir en una mezcla de distintos tipos de datos, combinando texto, números e incluso fotografías o ilustraciones. El contenido de una revista o una novela, o las formas de onda de una canción, por ejemplo, se considerarían normalmente datos no estructurados.
Si ahora mismo estás pensando: "¡Espera, las novelas tienen estructura! ¿Y los capítulos?", enhorabuena: ya estás pensando como un gestor de datos. Podemos crear datos sobre casi cualquier cosa recopilando información sobre el mundo y aplicándole la estructura .4 Y en realidad, así es como se crean todos los datos: los conjuntos de datos a los que accedemos a través de archivos y feeds son todos producto de las decisiones de alguien sobre cómo recopilar y organizar la información. En otras palabras, siempre hay más de una forma de organizar la información, pero la estructura elegida influye en cómo se puede analizar. Por eso es un poco ridículo sugerir que los datos pueden ser de algún modo "objetivos"; al fin y al cabo, son el producto de decisiones humanas (inherentemente subjetivas).
Por ejemplo, intenta llevar a cabo este miniexperimento: piensa en cómo organizas alguna colección tuya (puede ser una colección de música, libros, juegos o variedades de té, lo que quieras). Ahora pregunta a un amigo cómo organiza su propia colección de ese objeto. ¿Lo hacéis igual? ¿Cuál es "mejor"? Ahora pregunta a otra persona, e incluso a una tercera. Aunque puede que encuentres similitudes entre los sistemas que tú y tus amigos utilizáis para organizar vuestras colecciones de música, por ejemplo, me sorprendería mucho que descubrierais que dos de vosotros lo hacéis exactamente igual. De hecho, probablemente descubrirás que cada uno lo hace de una forma un poco diferente, pero también siente apasionadamente que su forma es "la mejor". ¡Y lo es! Para ellos.
Si esto te está recordando a nuestra discusión en "¿Cómo? y ¿para quién?", no es casualidad, porque el resultado de tu pregunta y tus esfuerzos en el manejo de los datos será finalmente -¡lo has adivinado!- otro conjunto de datos, que reflejará tus intereses y prioridades. También estará estructurado y organizado, lo que hará que trabajar con él de determinadas maneras sea más fácil que de otras. Pero lo importante no es que una forma determinada sea correcta o incorrecta, sino que toda elección implica concesiones mutuas. Identificar y reconocer esas compensaciones es una parte clave del uso honesto y responsable de los datos.
Por tanto, una contrapartida clave al utilizar datos estructurados es que requiere depender de los juicios y prioridades de otra persona para organizar la información subyacente. Obviamente, esto puede ser algo bueno -¡o incluso genial!- si esos datos se han estructurado según un proceso abierto y transparente en el que participan expertos bien cualificados. Unas estructuras de datos así, aplicadas con cuidado, pueden darnos una visión temprana de un tema del que, de otro modo, sabríamos poco o nada. Por otra parte, también existe la posibilidad de que heredemos las opciones sesgadas o mal diseñadas de otra persona.
Los datosno estructurados, por supuesto, nos dan total libertad para organizar la información en las estructuras de datos que mejor se adapten a nuestras necesidades. Como es lógico, esto nos obliga a responsabilizarnos de emprender un sólido proceso de calidad de los datos, que puede ser complejo y llevar mucho tiempo.
¿Cómo podemos saber de antemano si un conjunto de datos concreto está estructurado o no? En este caso, las extensiones de archivo pueden ayudarnos. Los formatos de datos basados en feeds siempre tienen al menos cierta estructura, aunque contengan trozos de "texto libre", como las publicaciones de las redes sociales. Así que si ves las extensiones de archivo .json, .xml, .rss o .atom, los datos tienen al menos algún tipo de estructura de registros y campos, como veremos en "Datos basados en feeds: actualizaciones en directo basadas en la Web". Los datos basados en archivos que terminan en .csv, .tsv, .txt, .xls(x) u .ods tienden a seguir una estructura de tipo tabla, filas y columnas, como veremos en la siguiente sección. Los datos verdaderamente no estructurados, mientras tanto, es más probable que nos lleguen como .doc(x) o .pdf.
Ahora que conocemos bien los distintos tipos de fuentes de datos que nos podemos encontrar, e incluso sabemos cómo localizarlas, ¡vamos a wrangling!
Trabajar con datos estructurados
Desde los primeros días de la informática digital, la tabla ha sido una de las formas más comunes de estructurar los datos. Incluso hoy en día, muchos de los formatos de datos más comunes y fáciles de manejar son poco más que tablas o colecciones de tablas. De hecho, ya trabajamos con un formato de datos tipo tabla muy común en el Capítulo 2: el formato .csv o de valores separados por comas.
Datos basados en archivos y tablas: llévalos a delimitar
En en general, todos los formatos de datos de tipo tabla que sueles encontrar son ejemplos de lo que se conoce como archivos delimitados: cada registro de datos está en su propia línea o fila, y los límites entre campos o columnas de valores de datos están indicados -o delimitados- porun carácter de texto específico. A menudo, la indicación del carácter de texto que se utiliza como delimitador en un archivo se incorpora a la extensión del archivo del conjunto de datos. Por ejemplo, la extensión de archivo .csv significa valor separado por comas, porque estos archivos utilizan un carácter coma (,
) como delimitador; la extensión de archivo .tsv significa valor separado por tabuladores, porque las columnas de datos están separadas por un tabulador. A continuación figura una lista de las extensiones de archivo que suelen asociarse a los datos delimitados:
- .csv
-
Archivos devalores separados por comas es una de las formas más comunes de archivos de datos estructurados de tipo tabla que encontrarás. Casi cualquier sistema de software que maneje datos tabulares (como los sistemas de datos gubernamentales o corporativos, los programas de hojas de cálculo e incluso los programas de datos comerciales especializados) puede dar salida a los datos como .csv y, como vimos en el Capítulo 2, existen prácticas bibliotecas para trabajar fácilmente con este tipo de datos en Python.
- .tsv
-
Los archivosde valores separados por tabulaciones existen desde hace mucho tiempo, pero la extensión descriptiva .tsv no se ha generalizado hasta hace relativamente poco. Aunque los proveedores de datos no suelen explicar por qué eligen un delimitador en lugar de otro, los archivos delimitados por tabulaciones pueden ser más habituales para conjuntos de datos cuyos valores necesitan incluir comas, como las direcciones postales.
- .txt
-
Los archivos de datos estructurados de con esta extensión suelen ser archivos .tsv disfrazados; los sistemas de datos más antiguos solían etiquetar los datos separados por tabuladores con la extensión .txt. Como verás en los ejemplos prácticos que siguen, es una buena idea abrir y revisar cualquier archivo de datos que quieras manipular con un programa de texto básico (o un editor de código como Atom) antes de escribir ningún código, ya que mirar el contenido del archivo es la única forma segura de saber con qué delimitadores estás trabajando.
- .xls(x)
-
Esto es la extensión de archivo de las hojas de cálculo producidas con Microsoft Excel. Como estos archivos pueden contener varias "hojas", además de fórmulas, formato y otras funciones que los archivos delimitados simples no pueden reproducir, necesitan más memoria para almacenar la misma cantidad de datos. También tienen otras limitaciones (como que sólo pueden manejar un determinado número de filas) que pueden tener implicaciones para la integridad de tu conjunto de datos.
- .ods
-
Los archivosde hoja de cálculo de documento abierto son la extensión por defecto de las hojas de cálculo producidas por varias suites de software de código abierto como LibreOffice y OpenOffice, y tienen limitaciones y características similares a las de los archivos .xls(x).
Antes de sumergirnos en cómo trabajar con cada uno de estos tipos de archivo en Python, merece la pena dedicar un poco de tiempo a pensar en cuándo podemos querer trabajar con datos de tipo tabla y dónde encontrarlos cuando lo hagamos.
Cuándo trabajar con datos de tipo tabla
La mayoría de las veces, no tenemos muchas opciones sobre el formato de nuestros datos de origen. De hecho, gran parte de la razón por la que necesitamos trabajar con datos es porque los datos que tenemos no satisfacen nuestras necesidades. Dicho esto, sigue siendo valioso saber con qué formato de datos preferirías poder trabajar, de modo que puedas utilizarlo para informar tu búsqueda inicial de datos.
En "Datos estructurados frente a datos no estructurados", hablamos de las ventajas y limitaciones de los datos estructurados, y ahora sabemos que los datos de tipo tabla son una de las formas más antiguas y comunes de datos legibles por máquina. Esta historia significa, en parte, que a lo largo de los años muchas formas de datos de origen se han hacinado en tablas, aunque no necesariamente se adapten bien a las representaciones tipo tabla. Aun así, este formato puede ser especialmente útil para responder a preguntas sobre tendencias y pautas a lo largo del tiempo. En nuestro ejercicio de Citi Bike del Capítulo 2, por ejemplo, examinamos cuántos "Clientes" frente a "Abonados" habían dado paseos en Citi Bike en el transcurso de un solo mes. Si quisiéramos, podríamos realizar el mismo cálculo para todos los meses disponibles de viajes en Citi Bike, a fin de comprender cualquier pauta en esta proporción a lo largo del tiempo.
Por supuesto, los datos de tipo tabla no suelen ser un gran formato para datos en tiempo real, o datos en los que no todas las observaciones contienen los mismos valores posibles. Este tipo de datos suelen ser más adecuados para los formatos de datos basados en feeds que tratamos en "Datos basados en feeds - Actualizaciones en directo impulsadas por la Web".
Dónde encontrar datos de tipo tabla
Dado que la inmensa mayoría de los datos legibles por máquina siguen estando en formatos de datos de tipo tabla, es uno de los formatos de datos más fáciles de localizar. Las hojas de cálculo son habituales en todas las disciplinas, y un gran número de sistemas de información gubernamentales y comerciales se basan en software que organiza los datos de este modo. Casi siempre que solicites datos a un experto o a una organización, lo más probable es que obtengas un formato tipo tabla. Lo mismo ocurre en casi todos los portales de datos abiertos y sitios de intercambio de datos que encontrarás en Internet. Como explicamos en "Búsqueda inteligente de tipos de datos específicos", incluso puedes encontrar datos de tipo tabla (y otros formatos de archivo específicos) a través de motores de búsqueda, si sabes cómo buscar.
Tratar datos de tipo tabla con Python
Para ayudar a a ilustrar lo sencillo que es trabajar con datos de tipo tabla en Python, veremos ejemplos de cómo leer datos de todos los tipos de archivos mencionados en esta sección, además de algunos otros, por si acaso. Aunque en capítulos posteriores veremos cómo hacer más cosas con la limpieza, la transformación y la evaluación de la calidad de los datos, de momento nos centraremos simplemente en acceder a los datos de cada tipo de archivo de datos e interactuar con ellos utilizando Python.
Leer datos de CSV
En caso de que no lo hayas seguido en el Capítulo 2, aquí tienes un repaso sobre cómo leer datos de un archivo .csv, utilizando una muestra del conjunto de datos de Citi Bike(Ejemplo 4-1). Como siempre, he incluido una descripción de lo que hace el programa -así como enlaces a los archivos fuente- en los comentarios de la parte superior de mi script. Como ya hemos trabajado antes con este formato de datos, por ahora sólo nos preocuparemos de imprimir las primeras filas de datos para ver qué aspecto tienen.
Ejemplo 4-1. csv_parsing.py
# a simple example of reading data from a .csv file with Python
# using the "csv" library.
# the source data was sampled from the Citi Bike system data:
# https://drive.google.com/file/d/17b461NhSjf_akFWvjgNXQfqgh9iFxCu_/
# which can be found here:
# https://s3.amazonaws.com/tripdata/index.html
# import the `csv` library
import
csv
# open the `202009CitibikeTripdataExample.csv` file in read ("r") mode
# this file should be in the same folder as our Python script or notebook
source_file
=
open
(
"
202009CitibikeTripdataExample.csv
"
,
"
r
"
)
# pass our `source_file` as an ingredient to the `csv` library's
# DictReader "recipe".
# store the result in a variable called `citibike_reader`
citibike_reader
=
csv
.
DictReader
(
source_file
)
# the DictReader method has added some useful information to our data,
# like a `fieldnames` property that lets us access all the values
# in the first or "header" row
(
citibike_reader
.
fieldnames
)
# let's just print out the first 5 rows
for
i
in
range
(
0
,
5
)
:
(
next
(
citibike_reader
)
)
Es nuestra biblioteca de trabajo cuando se trata de datos de tipo tabla.
open()
es una función incorporada que toma como parámetros un nombre de archivo y un "modo". En este ejemplo, el archivo de destino (202009CitibikeTripdataExample.csv
) debe estar en la misma carpeta que nuestro script o cuaderno de Python. Los valores para el "modo" pueden serr
para "lectura" ow
para "escritura".Al imprimir los valores de
citibike_reader.fieldnames
, podemos ver que la etiqueta exacta de la columna "Tipo de usuario" esusertype
.La función
range()
nos proporciona una forma de ejecutar un trozo de código un número determinado de veces, empezando por el valor del primer argumento y terminando justo antes del valor del segundo argumento. Por ejemplo, el código sangrado bajo esta línea se ejecutará cinco veces, pasando por los valoresi
de0
,1
,2
,3
y4
. Para más información sobre la funciónrange()
, consulta "Añadir iteradores: La función rango".
El resultado de la ejecución debería ser algo parecido a lo siguiente:
['tripduration', 'starttime', 'StartDate', 'stoptime', 'start station id', 'start station name', 'start station latitude', 'start station longitude', 'end station id', 'end station name', 'end station latitude', 'end station longitude', 'bikeid', 'usertype', 'birth year', 'gender'] {'tripduration': '4225', 'starttime': '2020-09-01 00:00:01.0430', 'StartDate': '2020-09-01', 'stoptime': '2020-09-01 01:10:26.6350', 'start station id': '3508', 'start station name': 'St Nicholas Ave & Manhattan Ave', 'start station latitude': '40.809725', 'start station longitude': '-73.953149', 'end station id': '116', 'end station name': 'W 17 St & 8 Ave', 'end station latitude': '40. 74177603', 'end station longitude': '-74.00149746', 'bikeid': '44317', 'usertype': 'Customer', 'birth year': '1979', 'gender': '1'} ... {'tripduration': '1193', 'starttime': '2020-09-01 00:00:12.2020', 'StartDate': '2020-09-01', 'stoptime': '2020-09-01 00:20:05.5470', 'start station id': '3081', 'start station name': 'Graham Ave & Grand St', 'start station latitude': '40.711863', 'start station longitude': '-73.944024', 'end station id': '3048', 'end station name': 'Putnam Ave & Nostrand Ave', 'end station latitude': '40.68402', 'end station longitude': '-73.94977', 'bikeid': '26396', 'usertype': 'Customer', 'birth year': '1969', 'gender': '0'}
Lectura de datos de archivos TSV y TXT
A pesar de su nombre , la biblioteca csv de Python es básicamente una ventanilla única para manejar datos de tipo tabla en Python, gracias a la opción delimiter
de la función DictReader
. A menos que le indiques lo contrario, DictReader
asume que el carácter coma (,
) es el separador que debe buscar. Sin embargo, anular esa suposición es fácil: simplemente puedes especificar un carácter diferente cuando llames a la función. En el Ejemplo 4-2, especificamos el carácter tabulador (\t
), pero podríamos sustituirlo fácilmente por cualquier delimitador que prefiramos (o que aparezca en un archivo fuente concreto).
Ejemplo 4-2. tsv_parsing.py
# a simple example of reading data from a .tsv file with Python, using
# the `csv` library. The source data was downloaded as a .tsv file
# from Jed Shugerman's Google Sheet on prosecutor politicians:
# https://docs.google.com/spreadsheets/d/1E6Z-jZWbrKmit_4lG36oyQ658Ta6Mh25HCOBaz7YVrA
# import the `csv` library
import
csv
# open the `ShugermanProsecutorPoliticians-SupremeCourtJustices.tsv` file
# in read ("r") mode.
# this file should be in the same folder as our Python script or notebook
tsv_source_file
=
open
(
"
ShugermanProsecutorPoliticians-SupremeCourtJustices.tsv
"
,
"
r
"
)
# pass our `tsv_source_file` as an ingredient to the csv library's
# DictReader "recipe."
# store the result in a variable called `politicians_reader`
politicians_reader
=
csv
.
DictReader
(
tsv_source_file
,
delimiter
=
'
\t
'
)
# the DictReader method has added some useful information to our data,
# like a `fieldnames` property that lets us access all the values
# in the first or "header" row
(
politicians_reader
.
fieldnames
)
# we'll use the `next()` function to print just the first row of data
(
next
(
politicians_reader
)
)
Este conjunto de datos apareció en el boletín "Data Is Plural" de Jeremy Singer-Vine (@jsvine) (https://data-is-plural.com).
El resultado debería ser algo parecido a lo siguiente
['', 'Justice', 'Term Start/End', 'Party', 'State', 'Pres Appt', 'Other Offices Held', 'Relevant Prosecutorial Background'] {'': '40', 'Justice': 'William Strong', 'Term Start/End': '1870-1880', 'Party': 'D/R', 'State': 'PA', 'Pres Appt': 'Grant', 'Other Offices Held': 'US House, Supr Court of PA, elect comm for elec of 1876', 'Relevant Prosecutorial Background': 'lawyer'}
Aunque la extensión de archivo .tsv se ha vuelto relativamente común hoy en día, muchos archivos generados por bases de datos antiguas que en realidad están separados por tabuladores pueden llegarte con una extensión de archivo .txt. Afortunadamente, como se describe en la barra lateral anterior, esto no cambia nada en la forma de manejar el archivo, siempre que especifiquemos el delimitador correcto, como puedes ver en el Ejemplo 4-3.
Ejemplo 4-3. txt_parsing.py
# a simple example of reading data from a .tsv file with Python, using
# the `csv` library. The source data was downloaded as a .tsv file
# from Jed Shugerman's Google Sheet on prosecutor politicians:
# https://docs.google.com/spreadsheets/d/1E6Z-jZWbrKmit_4lG36oyQ658Ta6Mh25HCOBaz7YVrA
# the original .tsv file was renamed with a file extension of .txt
# import the `csv` library
import
csv
# open the `ShugermanProsecutorPoliticians-SupremeCourtJustices.txt` file
# in read ("r") mode.
# this file should be in the same folder as our Python script or notebook
txt_source_file
=
open
(
"
ShugermanProsecutorPoliticians-SupremeCourtJustices.txt
"
,
"
r
"
)
# pass our txt_source_file as an ingredient to the csv library's DictReader
# "recipe" and store the result in a variable called `politicians_reader`
# add the "delimiter" parameter and specify the tab character, "\t"
politicians_reader
=
csv
.
DictReader
(
txt_source_file
,
delimiter
=
'
\t
'
)
# the DictReader function has added useful information to our data,
# like a label that shows us all the values in the first or "header" row
(
politicians_reader
.
fieldnames
)
# we'll use the `next()` function to print just the first row of data
(
next
(
politicians_reader
)
)
Como se explica en "¡No dejes espacios en blanco!", los caracteres de espacio en blanco deben escaparse cuando los utilizamos en código. Aquí, estamos utilizando el carácter escapado para un
tab
, que es\t
. Otro código común de carácter de espacio en blanco es\n
paranewline
(o\r
parareturn
, dependiendo de tu dispositivo).
Si todo ha ido bien, la salida de este script debería ser exactamente la misma que la del Ejemplo 4-2.
Una pregunta que puedes estar haciéndote en este momento es: "¿Cómo sé qué delimitador tiene mi archivo?". Aunque hay formas programáticas de ayudar a detectarlo, la respuesta sencilla es: ¡Mira! Cada vez que empieces a trabajar con (o pienses en trabajar con) un nuevo conjunto de datos, empieza por abrirlo en el programa de texto más básico que te ofrezca tu dispositivo (cualquier editor de código también será una opción fiable). Especialmente si el archivo es grande, utilizar el programa más sencillo posible permitirá que tu dispositivo dedique la máxima memoria y capacidad de procesamiento a leer realmente los datos, reduciendo la probabilidad de que el programa se cuelgue o de que tu dispositivo se bloquee (cerrar otros programas y el exceso de pestañas del navegador también ayudará).
Aunque hablaré de algunas formas de inspeccionar pequeñas partes de archivos realmente grandes más adelante en el libro, ahora es el momento de empezar a practicar las habilidades que son esenciales para evaluar la calidad de los datos, todas las cuales requieren revisar tus datos y emitir juicios sobre ellos. Así que, aunque hay formas de "automatizar" tareas como identificar el delimitador correcto para tus datos, a menudo no sólo será más rápido e intuitivo hacerlo a ojo en un editor de texto, sino que te ayudará a familiarizarte con otros aspectos importantes de los datos al mismo tiempo .
Manipulación de datos del mundo real: Comprender el desempleo
El conjunto de datos subyacente que utilizaremos para explorar algunos de nuestros formatos de datos de tipo tabla más complicados son los datos de desempleo sobre Estados Unidos. ¿Por qué? De un modo u otro, el desempleo nos afecta a la mayoría de nosotros, y en las últimas décadas EE.UU. ha experimentado algunas tasas de desempleo especialmente elevadas. Las cifras de desempleo de EEUU las publica mensualmente la Oficina de Estadísticas Laborales (BLS), y aunque a menudo se informa de ellas en fuentes de noticias de interés general, suelen tratarse como una especie de indicador abstracto de cómo va "la economía". Rara vez se discute en profundidad lo que las cifras representan realmente.
Cuando me incorporé al Wall Street Journal en 2007, mi primer gran proyecto fue crear un panel interactivo para explorar los datos de los indicadores económicos mensuales, incluido el desempleo. Una de las cosas más interesantes que aprendí en el proceso es que no hay "una" tasa de desempleo calculada cada mes, sino varias (seis, para ser exactos). La que suele aparecer en las noticias es la llamada tasa de desempleo "U3", que el BLS describe así:
Total de parados, en porcentaje de la población activa civil (tasa oficial de paro).
A primera vista, parece una definición sencilla del desempleo: de todas las personas que razonablemente podrían estar trabajando, ¿qué porcentaje no lo está?
Sin embargo, la verdadera historia es un poco más compleja. ¿Qué significa estar "empleado" o formar parte de la "población activa"? Un vistazo a diferentes cifras de desempleo aclara lo que la cifra "U3" no tiene en cuenta. La tasa de desempleo "U6" se define como:
Total de parados, más todas las personas marginalmente vinculadas a la población activa, más el total de empleados a tiempo parcial por motivos económicos, en porcentaje de la población activa civil más todas las personas marginalmente vinculadas a la población activa.
Cuando leemos la nota adjunta, esta definición más larga empieza a tomar forma:5
NOTA: Las personas marginalmente vinculadas a la población activa son las que actualmente no trabajan ni buscan trabajo, pero indican que quieren y están disponibles para un empleo y han buscado trabajo alguna vez en los últimos 12 meses. Los trabajadores desanimados, un subconjunto de los marginalmente vinculados, han dado una razón relacionada con el mercado laboral para no buscar trabajo actualmente. Las personas empleadas a tiempo parcial por razones económicas son las que desean y están disponibles para un trabajo a tiempo completo, pero han tenido que conformarse con un horario a tiempo parcial. Los controles de población actualizados se introducen anualmente con la publicación de los datos de enero.
En otras palabras, si quieres un trabajo (y lo has buscado en el último año) pero no lo has buscado muy recientemente -o si tienes un trabajo a tiempo parcial pero quieres un trabajo a tiempo completo- entonces no cuentas oficialmente como "desempleado" en la definición U3. Esto significa que la realidad económica de los estadounidenses que tienen varios empleos (que tienen más probabilidades de ser mujeres y tener más hijos)6y potencialmente de los trabajadores "gig" (estimados recientemente en hasta un 30% de la mano de obra estadounidense),7 no se reflejan necesariamente en la cifra U3. Como era de esperar, la tasa U6 suele ser varios puntos porcentuales más alta cada mes que la tasa U3.
Para ver cómo se comparan estos índices a lo largo del tiempo, podemos descargarlos del sitio web de la Reserva Federal de San Luis, que proporciona miles de conjuntos de datos económicos para su descarga en diversos formatos, incluidos archivos .xls(x) de tipo tabla y, como veremos más adelante en el Ejemplo 4-12, también formatos de tipo feed.
Puedes descargar los datos de estos ejercicios de la página web de la Base de Datos Económicos de la Reserva Federal (FRED). Muestra la tasa de desempleo U6 actual desde que se creó la medida a principios de los años 90.
Para añadir la tasa U3 a este gráfico, en la parte superior derecha elige Editar gráfico → AÑADIR LÍNEA. En el campo de búsqueda, escribe UNRATE
y selecciona "Tasa de desempleo" cuando aparezca debajo de la barra de búsqueda. Por último, haz clic en Añadir serie. Cierra esta ventana lateral utilizando la X de la parte superior derecha y, a continuación, selecciona Descargar, asegurándote de seleccionar la primera opción, Excel.8 Se tratará de un archivo .xls, del que nos ocuparemos en último lugar porque, aunque sigue estando ampliamente disponible, es un formato de archivo relativamente obsoleto (fue sustituido por .xlsx como formato predeterminado para las hojas de cálculo de Microsoft Excel en 2007).
Para obtener los formatos de archivo adicionales que necesitamos, sólo tienes que abrir el archivo que has descargado con un programa de hojas de cálculo como Google Sheets y elegir "Guardar como", luego seleccionar .xlsx, y después repetir el proceso eligiendo .ods. Ahora deberías tener los tres archivos siguientes, todos ellos con la misma información: fredgraph .xlsx, fredgraph.ods y fredgraph.xls.9
Nota
Si has abierto el archivo original fredgraph.xls, probablemente te habrás dado cuenta de que contiene algo más que los datos de desempleo; también contiene cierta información de cabecera sobre la procedencia de los datos y las definiciones de desempleo U3 y U6, por ejemplo. Aunque hacer un análisis de las tasas de desempleo que contienen estos archivos requeriría separar estos metadatos de los datos tipo tabla más adelante, recuerda que nuestro objetivo por el momento es simplemente convertir todos nuestros diversos archivos a un formato .csv. Abordaremos el proceso de limpieza de datos que implicaría eliminar estos metadatos en el Capítulo 7.
XLSX, ODS y todo lo demás
Para la mayor parte de , es preferible evitar procesar directamente, si es posible, los datos guardados como .xlsx, .ods y la mayoría de los demás formatos de datos no textuales de tipo tabla. Si te encuentras en la fase de exploración de conjuntos de datos, te sugiero que revises estos archivos simplemente abriéndolos con tu programa de hojas de cálculo preferido y guardándolos en formato de archivo .csv o .tsv antes de acceder a ellos en Python. Esto no sólo hará que sea más fácil trabajar con ellos, sino que te dará la oportunidad de ver realmente el contenido de tu archivo de datos y hacerte una idea de lo que contiene.
Volver a guardar y revisar .xls(x) y formatos de datos similares como .csv o un formato de archivo basado en texto equivalente reducirá el tamaño del archivo y te dará una mejor idea de cómo son los datos "reales". Debido a las opciones de formato de los programas de hojas de cálculo, a veces lo que ves en pantalla es sustancialmente diferente de los valores brutos que se almacenan en el archivo real. Por ejemplo, los valores que aparecen como porcentajes en un programa de hoja de cálculo (por ejemplo, 10%) pueden ser en realidad decimales (.1). Esto puede acarrear problemas si intentas basar aspectos de tu procesamiento o análisis en Python en lo que viste en la hoja de cálculo, en contraposición a un formato de datos basado en texto como .csv.
Aún así, habrá situaciones en las que necesites acceder a archivos .xls(x) y similares directamente con Python.10 Por ejemplo, si hay un conjunto de datos . xls que tienes que manejar con regularidad (digamos, cada mes), volver a guardar el archivo manualmente cada vez te llevaría un tiempo innecesario.
Afortunadamente, esa activa comunidad Python de la que hablamos en "Comunidad" ha creado bibliotecas que pueden manejar con facilidad una impresionante variedad de formatos de datos. Para hacerte una idea de cómo funcionan estas bibliotecas con datos de origen (y formatos de datos) más complejos, los siguientes ejemplos de código leen el formato de archivo especificado y luego crean un nuevo archivo .csv que contiene los mismos datos.
Sin embargo, para utilizar estas bibliotecas, primero tendrás que instalarlas en tu dispositivo ejecutando uno a uno los siguientes comandos en una ventana de terminal:11
pip install openpyxl pip install pyexcel-ods pip install xlrd==2.0.1
En los siguientes ejemplos de código, utilizaremos la biblioteca openpyxl para acceder (o analizar) archivos .xlsx, la biblioteca pyexcel-ods para tratar archivos .ods y la biblioteca xlrd para leer archivos .xls (para más información sobre cómo encontrar y seleccionar bibliotecas Python, consulta "Dónde buscar bibliotecas").
Para ilustrar mejor la idiosincrasia de estos distintos formatos de archivo, vamos a hacer algo parecido a lo que hicimos en el Ejemplo 4-3: tomaremos datos de muestra que se proporcionan como archivo .xls y crearemos archivos .xlsx y .ods que contengan exactamente los mismos datos, volviendo a guardar ese archivo fuente en los otros formatos mediante un programa de hoja de cálculo. Por el camino, creo que empezarás a hacerte una idea de cómo estos formatos no textuales complican más (y yo diría que innecesariamente) el proceso de manipulación de datos.
Empezaremos trabajando con un archivo .xlsx en \ref(Ejemplo 4-4), utilizando una versión de los datos de desempleo descargados de FRED. Este ejemplo ilustra una de las primeras diferencias importantes entre tratar con archivos de datos de tipo tabla basados en texto y formatos no textuales: como los formatos no textuales admiten múltiples "hojas", tuvimos que incluir un bucle for
en la parte superior de nuestro script, dentro del cual pusimos el código para crear nuestros archivos de salida individuales (uno por cada hoja).
Ejemplo 4-4. xlsx_parsing.py
# an example of reading data from an .xlsx file with Python, using the "openpyxl"
# library. First, you'll need to pip install the openpyxl library:
# https://pypi.org/project/openpyxl/
# the source data can be composed and downloaded from:
# https://fred.stlouisfed.org/series/U6RATE
# specify the "chapter" you want to import from the "openpyxl" library
# in this case, "load_workbook"
from
openpyxl
import
load_workbook
# import the `csv` library, to create our output file
import
csv
# pass our filename as an ingredient to the `openpyxl` library's
# `load_workbook()` "recipe"
# store the result in a variable called `source_workbook`
source_workbook
=
load_workbook
(
filename
=
'
fredgraph.xlsx
'
)
# an .xlsx workbook can have multiple sheets
# print their names here for reference
(
source_workbook
.
sheetnames
)
# loop through the worksheets in `source_workbook`
for
sheet_num
,
sheet_name
in
enumerate
(
source_workbook
.
sheetnames
)
:
# create a variable that points to the current worksheet by
# passing the current value of `sheet_name` to `source_workbook`
current_sheet
=
source_workbook
[
sheet_name
]
# print `sheet_name`, just to see what it is
(
sheet_name
)
# create an output file called "xlsx_"+sheet_name
output_file
=
open
(
"
xlsx_
"
+
sheet_name
+
"
.csv
"
,
"
w
"
)
# use this csv library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# loop through every row in our sheet
for
row
in
current_sheet
.
iter_rows
(
)
:
# we'll create an empty list where we'll put the actual
# values of the cells in each row
row_cells
=
[
]
# for every cell (or column) in each row....
for
cell
in
row
:
# let's print what's in here, just to see how the code sees it
(
cell
,
cell
.
value
)
# add the values to the end of our list with the `append()` method
row_cells
.
append
(
cell
.
value
)
# write our newly (re)constructed data row to the output file
output_writer
.
writerow
(
row_cells
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
Al igual que la función
DictReader()
de la biblioteca csv, la funciónload_workbook()
deopenpyxl
añade propiedades a nuestros datos de origen, en este caso, una que nos muestra los nombres de todas las hojas de datos de nuestro libro de trabajo.Aunque nuestro libro de trabajo de ejemplo sólo incluye una hoja de cálculo, es posible que tengamos más en el futuro. Utilizaremos la función
enumerate()
para poder acceder tanto a un iterador como al nombre de la hoja. Esto nos ayudará a crear un archivo .csv por hoja de trabajo.Cada hoja de nuestro
source_workbook
necesitará su propio archivo .csv de salida, con un nombre único. Para generarlos, "abriremos" un nuevo archivo con el nombre"xlsx_"+sheet_name+".csv"
y lo haremos escribible pasandow
como argumento "modo" (hasta ahora, hemos utilizado el modor
para leer datos de .csvs).La función
iter_rows()
es específica de la biblioteca openpyxl. Aquí, convierte las filas desource_workbook
en una lista sobre la que se puede iterar, o hacer un bucle.La biblioteca openpyxl trata cada celda de datos como un tipo de datos Python
tuple
. Si intentamos imprimir directamente las filas decurrent_sheet
, obtendremos ubicaciones de celdas poco útiles, en lugar de los valores de datos que contienen. Para solucionarlo, haremos otro bucle dentro de éste para recorrer cada celda de cada fila de una en una y añadir los valores reales de los datos arow_cells
.Observa que este código está alineado a la izquierda con el código
for cell in row
del ejemplo. Esto significa que está fuera de ese bucle y, por tanto, sólo se ejecutará después de que todas las celdas de una fila determinada se hayan añadido a nuestra lista.
Este guión también empieza a demostrar la forma en que, al igual que dos chefs pueden tener formas distintas de preparar el mismo plato, los creadores de bibliotecas pueden tomar decisiones distintas sobre cómo (re)estructurar cada tipo de archivo fuente, con las correspondientes implicaciones para nuestro código. Los creadores de la biblioteca openpyxl, por ejemplo, eligieron almacenar la etiqueta de ubicación de cada celda de datos (por ejemplo, A6
) y el valor que contiene en un archivo Python tuple
. Esa decisión de diseño es la razón por la que necesitamos un segundo bucle for
para recorrer cada fila de datos, porque en realidad tenemos que acceder a los datos celda por celda para construir la lista Python que se convertirá en una única fila en nuestro archivo .csv de salida. Del mismo modo, si utilizas un programa de hoja de cálculo para abrir el archivo xlsx_FRED Graph.csv creado por el script del Ejemplo 4-4, verás que el archivo .xls original muestra los valores de la columna observation_date
en formato AAAA-MM-DD, pero nuestro archivo de salida muestra esos valores en formato AAAA-MM-DD HH:MM:SS. Esto se debe a que el creador o creadores de openpyxl decidieron que convertiría automáticamente cualquier cadena de datos "similar a una fecha" en el tipo de datos de Python datetime
. Obviamente, ninguna de estas opciones es correcta o incorrecta; simplemente tenemos que tenerlas en cuenta al escribir nuestro código para no distorsionar o malinterpretar los datos fuente de .
Ahora que ya hemos manejado la versión .xlsx de nuestro archivo de datos, veamos qué ocurre cuando lo analiza como .ods, como se muestra en el Ejemplo 4-5.
Ejemplo 4-5. ods_parsing.py
# an example of reading data from an .ods file with Python, using the
# "pyexcel_ods" library. First, you'll need to pip install the library:
# https://pypi.org/project/pyexcel-ods/
# specify the "chapter" of the "pyexcel_ods" library you want to import,
# in this case, `get_data`
from
pyexcel_ods
import
get_data
# import the `csv` library, to create our output file
import
csv
# pass our filename as an ingredient to the `pyexcel_ods` library's
# `get_data()` "recipe"
# store the result in a variable called `source_workbook`
source_workbook
=
get_data
(
"
fredgraph.ods
"
)
# an `.ods` workbook can have multiple sheets
for
sheet_name
,
sheet_data
in
source_workbook
.
items
(
)
:
# print `sheet_name`, just to see what it is
(
sheet_name
)
# create "ods_"+sheet_name+".csv" as an output file for the current sheet
output_file
=
open
(
"
ods_
"
+
sheet_name
+
"
.csv
"
,
"
w
"
)
# use this csv library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# now, we need to loop through every row in our sheet
for
row
in
sheet_data
:
# use the `writerow` recipe to write each `row`
# directly to our output file
output_writer
.
writerow
(
row
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
La biblioteca pyexcel_ods convierte nuestros datos de origen en el tipo de datos
OrderedDict
de Python. El método asociadoitems()
nos permite acceder al nombre y a los datos de cada hoja como un par clave/valor que podemos recorrer en bucle. En este caso,sheet_name
es la "clave" y los datos de toda la hoja de cálculo son el "valor".Aquí,
sheet_data
ya es una lista, así que podemos recorrerla con un bucle básicofor
.Esta biblioteca convierte cada fila de una hoja de cálculo en una lista, por lo que podemos pasarlas directamente al método
writerow()
.
En el caso de la biblioteca pyexcel_ods, el contenido de nuestro archivo .csv de salida se parece mucho más a lo que vemos visualmente cuando abrimos el fredgraph.xls original mediante un programa de hojas de cálculo como Google Sheets: el campo observation_date
, por ejemplo, tiene un formato sencillo AAAA-MM-DD. Además, el creador o creadores de la biblioteca decidieron tratar los valores de cada fila como una lista, lo que nos permite escribir cada registro directamente en nuestro archivo de salida sin crear bucles adicionales ni listas.
Por último, veamos qué ocurre cuando utilizamos la biblioteca xlrd para analizar directamente el archivo .xls original en el Ejemplo 4-6.
Ejemplo 4-6. análisis_xls.py
# a simple example of reading data from a .xls file with Python
# using the "xrld" library. First, pip install the xlrd library:
# https://pypi.org/project/xlrd/2.0.1/
# import the "xlrd" library
import
xlrd
# import the `csv` library, to create our output file
import
csv
# pass our filename as an ingredient to the `xlrd` library's
# `open_workbook()` "recipe"
# store the result in a variable called `source_workbook`
source_workbook
=
xlrd
.
open_workbook
(
"
fredgraph.xls
"
)
# an `.xls` workbook can have multiple sheets
for
sheet_name
in
source_workbook
.
sheet_names
(
)
:
# create a variable that points to the current worksheet by
# passing the current value of `sheet_name` to the `sheet_by_name` recipe
current_sheet
=
source_workbook
.
sheet_by_name
(
sheet_name
)
# print `sheet_name`, just to see what it is
(
sheet_name
)
# create "xls_"+sheet_name+".csv" as an output file for the current sheet
output_file
=
open
(
"
xls_
"
+
sheet_name
+
"
.csv
"
,
"
w
"
)
# use the `csv` library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# now, we need to loop through every row in our sheet
for
row_num
,
row
in
enumerate
(
current_sheet
.
get_rows
(
)
)
:
# each row is already a list, but we need to use the `row_value()`
# method to access them
# then we can use the `writerow` recipe to write them
# directly to our output file
output_writer
.
writerow
(
current_sheet
.
row_values
(
row_num
)
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
Observa que esta estructura es similar a la que utilizamos cuando trabajamos con la biblioteca csv.
La función
get_rows()
es específica de la biblioteca xlrd; convierte las filas de nuestra hoja de cálculo actual en una lista sobre la que se puede hacer un bucle.12Habrá algunos errores en las "fechas" escritas en nuestro archivo de salida.13 Veremos cómo arreglar las fechas en "Descifrar fechas de Excel".
Una cosa que veremos en este archivo de salida es cierta rareza en los valores registrados en el campo observation_date
, que refleja el hecho de que, como dicen los creadores de la biblioteca xlrd:14
Fechas en hojas de cálculo Excel: En realidad, no existen. Lo que tienes son números en coma flotante y una esperanza piadosa.
Como resultado, obtener una fecha útil y legible por humanos de un archivo .xls requiere una limpieza importante, que abordaremos en "Descifrar fechas de Excel".
Como espero que hayan demostrado estos ejercicios, con algunas bibliotecas inteligentes y unos pocos ajustes en la configuración básica de nuestro código, es posible manejar datos de una amplia gama de formatos de datos de tipo tabla con Python de forma rápida y sencilla. Al mismo tiempo, espero que estos ejemplos también hayan ilustrado por qué casi siempre es preferible trabajar con formatos basados en texto y/o de código abierto,15 porque a menudo requieren menos "limpieza" y transformación para ponerlos en un estado claro y utilizable .
Por último, Anchura fija
Aunque no lo he mencionado al principio de esta sección, una de las versiones más antiguas de los datos de tipo tabla es lo que se conoce como "anchura fija". Como su nombre indica, cada columna de datos de una tabla de ancho fijo contiene un número concreto y predefinido de caracteres, y siempre ese número de caracteres. Esto significa que los datos significativos de los archivos de ancho fijo suelen estar rellenos de caracteres adicionales, como espacios o ceros.
Aunque son muy poco comunes en los sistemas de datos contemporáneos, es probable que aún te encuentres con formatos de ancho fijo si trabajas con fuentes de datos gubernamentales cuya infraestructura puede tener décadas de antigüedad.16 Por ejemplo, la Administración Nacional Oceánica y Atmosférica de EE.UU. (NOAA), cuyos orígenes se remontan a principios del siglo XIX, ofrece gratuitamente en línea una amplia gama de información meteorológica detallada y actualizada a través de su Red Climatológica Histórica Global, gran parte de la cual se publica en un formato de ancho fijo. Por ejemplo, la información sobre el identificador único de las estaciones, sus ubicaciones y de qué red o redes forman parte se almacena en el archivoghcnd-stations.txt. Para interpretar cualquier lectura de datos meteorológicos reales (muchos de los cuales también se publican como archivos de anchura fija), tendrás que cruzar los datos de la estación con los datos meteorológicos.
Incluso más que con otros archivos de datos de tipo tabla, trabajar con datos de ancho fijo puede ser especialmente complicado si no tienes acceso a los metadatos que describen cómo están organizados el archivo y sus campos. Con los archivos delimitados, a menudo es posible echar un vistazo al archivo en un editor de texto e identificar el delimitador utilizado con un nivel razonable de confianza. En el peor de los casos, puedes probar a analizar el archivo utilizando distintos delimitadores y ver cuál da mejores resultados. Con los archivos de anchura fija -especialmente los grandes- si no hay datos para un campo concreto en la muestra de los datos que inspeccionas, es fácil acabar agrupando inadvertidamente varios campos de datos.
Afortunadamente, los metadatos sobre el archivo ghcnd-stations.txt que estamos utilizando como fuente de datos están incluidos en el archivo readme.txt de la misma carpeta del sitio de la NOAA.
Revisando ese archivo readme.txt, encontramos el encabezamiento IV. FORMAT OF "ghcnd-stations.txt"
, que contiene la siguiente tabla:
------------------------------ Variable Columns Type ------------------------------ ID 1-11 Character LATITUDE 13-20 Real LONGITUDE 22-30 Real ELEVATION 32-37 Real STATE 39-40 Character NAME 42-71 Character GSN FLAG 73-75 Character HCN/CRN FLAG 77-79 Character WMO ID 81-85 Character ------------------------------
Le sigue una descripción detallada de lo que contiene o significa cada campo, incluyendo información como las unidades. Gracias a este sólido diccionario de datos, ahora sabemos no sólo cómo está organizado el archivo ghcnd-stations.txt, sino también cómo interpretar la información que contiene. Como veremos en el Capítulo 6, encontrar (o construir) un diccionario de datos es una parte esencial para evaluar o mejorar la calidad de nuestros datos. De momento, sin embargo, sólo podemos centrarnos en transformar este archivo de ancho fijo en un .csv, como se detalla en el Ejemplo 4-7.
Ejemplo 4-7. fixed_width_parsing.py
# an example of reading data from a fixed-width file with Python.
# the source file for this example comes from NOAA and can be accessed here:
# https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd-stations.txt
# the metadata for the file can be found here:
# https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt
# import the `csv` library, to create our output file
import
csv
filename
=
"
ghcnd-stations
"
# reading from a basic text file doesn't require any special libraries
# so we'll just open the file in read format ("r") as usual
source_file
=
open
(
filename
+
"
.txt
"
,
"
r
"
)
# the built-in "readlines()" method does just what you'd think:
# it reads in a text file and converts it to a list of lines
stations_list
=
source_file
.
readlines
(
)
# create an output file for our transformed data
output_file
=
open
(
filename
+
"
.csv
"
,
"
w
"
)
# use the `csv` library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# create the header list
headers
=
[
"
ID
"
,
"
LATITUDE
"
,
"
LONGITUDE
"
,
"
ELEVATION
"
,
"
STATE
"
,
"
NAME
"
,
"
GSN_FLAG
"
,
"
HCNCRN_FLAG
"
,
"
WMO_ID
"
]
# write our headers to the output file
output_writer
.
writerow
(
headers
)
# loop through each line of our file (multiple "sheets" are not possible)
for
line
in
stations_list
:
# create an empty list, to which we'll append each set of characters that
# makes up a given "column" of data
new_row
=
[
]
# ID: positions 1-11
new_row
.
append
(
line
[
0
:
11
]
)
# LATITUDE: positions 13-20
new_row
.
append
(
line
[
12
:
20
]
)
# LONGITUDE: positions 22-30
new_row
.
append
(
line
[
21
:
30
]
)
# ELEVATION: positions 32-37
new_row
.
append
(
line
[
31
:
37
]
)
# STATE: positions 39-40
new_row
.
append
(
line
[
38
:
40
]
)
# NAME: positions 42-71
new_row
.
append
(
line
[
41
:
71
]
)
# GSN_FLAG: positions 73-75
new_row
.
append
(
line
[
72
:
75
]
)
# HCNCRN_FLAG: positions 77-79
new_row
.
append
(
line
[
76
:
79
]
)
# WMO_ID: positions 81-85
new_row
.
append
(
line
[
80
:
85
]
)
# now all that's left is to use the
# `writerow` function to write new_row to our output file
output_writer
.
writerow
(
new_row
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
Como no tenemos nada dentro del archivo en lo que podamos basarnos para los encabezados de columna, tenemos que "codificarlos" basándonos en la información del archivoreadme.txt. Ten en cuenta que he eliminado los caracteres especiales y he utilizado guiones bajos en lugar de espacios para minimizar las molestias al limpiar y analizar estos datos más adelante.
En realidad, Python ve las líneas de texto como simples listas de caracteres, así que podemos decirle que nos dé los caracteres entre dos posiciones de índice numeradas. Al igual que la función
range()
, se incluye el carácter de la primera posición, pero no el del segundo número. Recuerda también que Python empieza a contar listas de elementos en cero (lo que se suele llamar indexación cero). Esto significa que, para cada entrada, el primer número será uno menos de lo que digan los metadatos, pero el número de la derecha será el mismo.
Si ejecutas el script del Ejemplo 4-7 y abres el archivo .csv de salida en un programa de hojas de cálculo, observarás que los valores de algunas columnas no tienen el mismo formato. Por ejemplo, en la columna ELEVATION
, los números con decimales están justificados a la izquierda, pero los que no tienen decimales están justificados a la derecha. ¿Qué ocurre?
Una vez más, abrir el archivo en un editor de texto es esclarecedor. Aunque el archivo que hemos creado está técnicamente separado por comas, los valores que ponemos en cada una de nuestras columnas recién "delimitadas" siguen conteniendo los espacios extra que existían en el archivo original. Como resultado, nuestro nuevo archivo sigue pareciendo bastante "de ancho fijo".
En otras palabras -al igual que vimos en el caso de las "fechas" de Excel-, convertir nuestro archivo a .csv no genera "automáticamente" tipos de datos sensatos en nuestro archivo de salida. Determinar qué tipo de datos debe tener cada campo -y limpiarlos para que se comporten adecuadamente- forma parte del proceso de limpieza de datos que abordaremos en el Capítulo 7.
Actualizaciones en directo basadas en datos de la web
La estructura de los formatos de datos tipo tabla se adapta bien a un mundo en el que la mayoría de los "datos" ya han sido filtrados, revisados y procesados en una colección relativamente bien organizada de números, fechas y cadenas cortas. Sin embargo, con el auge de Internet surgió la necesidad de transmitir grandes cantidades del tipo de texto "libre" que se encuentra, por ejemplo, en las noticias y en los feeds de las redes sociales. Dado que este tipo de contenido de datos suele incluir caracteres como comas, puntos y comillas que afectan a su significado semántico, encajarlo en un formato delimitado tradicional será, en el mejor de los casos, problemático. Además, el sesgo horizontal de los formatos delimitados (que implica mucho desplazamiento de izquierda a derecha) va en contra de las convenciones de desplazamiento vertical de la web. Los formatos de datos basados en feeds se han diseñado para abordar estas dos limitaciones.
A alto nivel, hay dos tipos principales de formatos de datos basados en feeds: XML y JSON. Ambos son formatos basados en texto que permiten al proveedor de datos definir su propia estructura de datos única, lo que los hace extremadamente flexibles y, en consecuencia, útiles para la gran variedad de contenidos que se encuentran en los sitios web y plataformas conectados a Internet. Tanto si se encuentran en línea como si guardas una copia localmente, reconocerás estos formatos, en parte, por sus extensiones de archivo coordinadas .xml y .json:
- .xml
-
El Lenguaje de Marcado Extensible abarca una amplia gama de formatos de archivo, como .rss, .atom e incluso .html. Al ser el tipo más genérico de lenguaje de marcado, XML es extremadamente flexible y fue quizás el formato de datos original para la alimentación de datos basada en la web.
- .json
-
Archivos JavaScript Object Notation son algo más recientes que los archivos XML, pero tienen una finalidad similar. En general, los archivos JSON son menos descriptivos (y, por tanto, más cortos y concisos) que los archivos XML. Esto significa que pueden codificar una cantidad de datos casi idéntica a la de un archivo XML ocupando menos espacio, lo que es especialmente importante para la velocidad en la web móvil. Igualmente importante es el hecho de que los archivos JSON son esencialmente grandes tipos de datos
object
dentro del lenguaje de programación JavaScript, que es el lenguaje en el que se basan muchos, si no la mayoría, de los sitios web y aplicaciones móviles. Esto significa que analizar datos con formato JSON es muy fácil para cualquier sitio o programa que utilice JavaScript, especialmente si se compara con XML. Afortunadamente, los tipos de datos de JavaScriptobject
son muy similares a los tipos de datos de Pythondict
, lo que también hace que trabajar con JSON en Python sea muy sencillo.
Antes de sumergirnos en cómo trabajar con cada uno de estos tipos de archivo en Python, repasemos cuándo podemos querer datos de tipo feed y dónde encontrarlos cuando los necesitemos.
Cuándo trabajar con datos de tipo feed
En un sentido , los datos de tipo fuente son para el siglo XXI lo que los datos de tipo tabla fueron para el siglo XX: el volumen total de datos de tipo fuente generados, almacenados e intercambiados en la web cada día es probablemente millones de veces mayor que el de todos los datos de tipo tabla del mundo juntos, en gran parte porque los datos de tipo fuente son los que impulsan las redes sociales, las aplicaciones de noticias y todo lo demás.
Desde el punto de vista de la gestión de datos, generalmente querrás datos de tipo feed cuando el fenómeno que estés explorando sea sensible al tiempo y se actualice de forma frecuente y/o impredecible. Normalmente, este tipo de datos se genera en respuesta a un proceso humano o natural, como (una vez más) publicar en las redes sociales, publicar una noticia o registrar un terremoto.
Tanto los datos basados en archivos, de tipo tabla, como los basados en la Web, de tipo fuente, pueden contener información histórica, pero, como hemos comentado al principio de este capítulo, los primeros suelen reflejar los datos tal y como estaban en un momento fijo. Los segundos, por el contrario, suelen estar organizados en un orden "cronológico inverso" (primero el más reciente), siendo la primera entrada el registro de datos que se creó más recientemente en el momento en que accediste a los datos, en lugar de una fecha de publicación predeterminada.
Dónde encontrar datos de tipo feed
Los datos de tipo feed se encuentran casi exclusivamente en la web, a menudo en URL especiales conocidas como puntos finales de interfaz de programación de aplicaciones (API). En el Capítulo 5 entraremos en en los detalles del trabajo con las API, pero por ahora todo lo que necesitas saber es que los puntos finales de las API son en realidad páginas web sólo de datos: puedes ver muchas de ellas utilizando un navegador web normal, pero todo lo que verás son los datos en sí. Algunos puntos finales de la API incluso devolverán datos diferentes dependiendo de la información que les envíes, y esto es parte de lo que hace que trabajar con datos de tipo feed sea tan flexible: ¡cambiando sólo unas pocas palabras o valores en tu código, puedes acceder a un conjunto de datos totalmente diferente!
Encontrar API que ofrezcan datos de tipo feed no requiere demasiadas estrategias de búsqueda especiales, porque normalmente los sitios y servicios que tienen API quieren que los encuentres. ¿Por qué? Sencillamente, cuando alguien escribe código que hace uso de una API, (normalmente) reporta algún beneficio a la empresa que la proporciona, aunque ese beneficio sea sólo una mayor exposición pública. En los primeros días de Twitter, por ejemplo, muchos desarrolladores web escribieron programas utilizando la API de Twitter, lo que hizo más útil la plataforma y ahorró a la empresa el gasto y el esfuerzo de averiguar qué querían los usuarios y construirlo. Al hacer que muchos de los datos de su plataforma estuvieran disponibles gratuitamente (al principio), la API dio lugar a varias empresas que Twitter acabaría comprando, aunque muchas más también se verían abocadas a la quiebra al cambiar la API o sus condiciones de servicio.17 Esto pone de relieve uno de los problemas particulares que pueden surgir al trabajar con cualquier tipo de datos, pero especialmente con los datos de tipo feed puestos a disposición por empresas con ánimo de lucro: tanto los datos como tu derecho a acceder a ellos pueden cambiar en cualquier momento, sin previo aviso. Así que, aunque las fuentes de datos de tipo feed son realmente valiosas, también son efímeras en más de un sentido.
Tratar datos de tipo Feed con Python
Como con los datos de tipo tabla, manejar datos de tipo fuente en Python es posible gracias a una combinación de bibliotecas útiles y al hecho de que formatos como JSON ya se parecen a los tipos de datos existentes en el lenguaje de programación Python. Además, en las secciones siguientes veremos que XML y JSON suelen ser funcionalmente intercambiables para nuestros fines (aunque muchas API sólo ofrecen datos en uno u otro formato).
XML: Un marcado para gobernarlos a todos
Los lenguajes de marcado se encuentran entre las formas más antiguas de formatos de documentos estandarizados en informática, diseñados con el objetivo de crear documentos basados en texto que puedan ser leídos fácilmente tanto por humanos como por máquinas. XML se convirtió en una parte cada vez más importante de la infraestructura de Internet en la década de 1990, a medida que la variedad de dispositivos que accedían a la información basada en la web y la mostraban hizo que la separación del contenido (por ejemplo, texto e imágenes) del formato (por ejemplo, el diseño de la página) se convirtiera en una necesidad. A diferencia de un documento HTML -en el que el contenido y el formato están totalmente mezclados-, un documento XML no dice prácticamente nada sobre cómo debe mostrarse su información. En su lugar, sus etiquetas y atributos actúan como metadatos sobre qué tipo de información contiene, junto con los propios datos.
Para hacerte una idea del aspecto de XML, echa un vistazo al Ejemplo 4-8 en.
Ejemplo 4-8. Un ejemplo de documento XML
<?xml version="1.0" encoding="UTF-8"?>
<mainDoc>
<!--This is a comment-->
<elements>
<element1>
This is some text in the document.</element1>
<element2>
This is some other data in the document.</element2>
<element3
someAttribute=
"aValue"
/>
</elements>
<someElement
anAttribute=
"anotherValue"
>
More content</someElement>
</mainDoc>
Aquí ocurren un par de cosas. La primera línea es , llamada declaración de tipo de documento (o doc-type
); nos indica que el resto del documento debe interpretarse como XML (en contraposición a cualquiera de los otros lenguajes web o de marcado, algunos de los cuales revisaremos más adelante en este capítulo).
Empezando por la línea:
<mainDoc>
entramos en la esencia del propio documento. Parte de lo que hace que XML sea tan flexible es que sólo contiene dos estructuras gramaticales reales, ambas incluidas en el Ejemplo 4-8:
- tags
-
Las etiquetas pueden ser tanto pareadas (como
element1
,element2
,someElement
, o inclusomainDoc
) como autocerradas (comoelement3
). El nombre de una etiqueta siempre va encerrado entre signos de intercalación (<>
). En el caso de una etiqueta de cierre, el signo de apertura va seguido inmediatamente de una barra oblicua (/
). Un par de etiquetas emparejadas, o una etiqueta autocerrada, también se describen como elementos XML . - atributos
-
Los atributos sólo pueden existir dentro de etiquetas (como
anAttribute
). Los atributos son un tipo de par clave/valor en el que el nombre del atributo (o clave) va seguido inmediatamentede un signo igual (=
), seguido del valor rodeado de comillas dobles(""
).
Un elemento XML es lo que está contenido entre una etiqueta de apertura y su etiqueta de cierre correspondiente (por ejemplo, <elements>
y </elements>
). Como tal, un elemento XML dado puede contener muchas etiquetas, cada una de las cuales puede contener también otras etiquetas. Cualquier etiqueta puede tener también cualquier número de atributos (incluido ninguno). Una etiqueta autocerrada también se considera un elemento.
La única otra regla con sentido para estructurar documentos XML es que cuando las etiquetas aparecen dentro de otras etiquetas, la etiqueta abierta más recientemente debe cerrarse primero. En otras palabras, aunque ésta es una estructura XML legítima:
<outerElement>
<!-- Notice that that the `innerElement1` is closed
before the `innerElement2` tag is opened -->
<innerElement1>
Some content</innerElement1>
<innerElement2>
More content</innerElement2>
</outerElement>
esto no lo es:
<outerElement>
<!-- NOPE! The `innerElement2` tag was opened
before the `innerElement1` tag was closed -->
<innerElement1>
Some content<innerElement2>
More content</innerElement1>
</innerElement2>
</outerElement>
Este principio de último abierto, primero cerrado también se describe como anidamiento, similar a los bucles "anidados" for...in
de la Figura 2-3.18 La anidación es especialmente importante en los documentos XML porque rige uno de los principales mecanismos que utilizamos para leer o analizar documentos XML (y otros lenguajes de marcado) con código. En un documento XML, el primer elemento tras la declaración doc-type
se conoce como elemento raíz. Si el documento XML ha sido formateado, el elemento raíz siempre estará justificado a la izquierda, y cualquier elemento que esté anidado directamente dentro de ese elemento tendrá una sangría de un nivel a la derecha y se denomina elemento hijo. En el Ejemplo 4-8, por tanto, <mainDoc>
se consideraría el elemento raíz, y <elements>
sería su elemento hijo. Del mismo modo, <mainDoc>
es el elemento padre de <elements>
(Ejemplo 4-9).
Ejemplo 4-9. Un documento XML anotado
<?xml version="1.0" encoding="UTF-8"?>
<mainDoc>
<!--`mainDoc` is the *root* element, and `elements` is its *child*-->
<elements>
<!-- `elements` is the *parent* of `element1`, `element2`, and
`element3`, which are *siblings* of one another -->
<element1>
This is text data in the document.</element1>
<element2>
This is some other data in the document.</element2>
<element3
someAttribute=
"aValue"
/>
</elements>
<!-- `someElement` is also a *child* of `mainDoc`,
and a *sibling* of `elements` -->
<someElement
anAttribute=
"anotherValue"
>
More content</someElement>
</mainDoc>
Dada esta tendencia a la jerga genealógica, puede que te preguntes: si <elements>
es el progenitor de <element3>
, y <mainDoc>
es el progenitor de <elements>
, ¿eso hace que <mainDoc>
sea el abuelo de <element3>
? La respuesta es: sí, pero no. Aunque <mainDoc>
es el "padre" del "padre" de <element3>
, el término "abuelo" nunca se utiliza para describir una estructura XML, ¡podría complicarse rápidamente! En su lugar, simplemente describimos la relación como exactamente eso: <mainDoc>
es el padre del padre de <element3>
.
Afortunadamente, no existe tal complejidad asociada a los atributos XML: son simplemente pares clave/valor, y sólo pueden existir dentro de etiquetas XML, así:
<element3
someAttribute=
"aValue"
/>
Ten en cuenta que no hay espacio a ambos lados del signo igual, igual que no hay espacio entre las caritas y las barras de una etiqueta de elemento.
Al igual que escribir en inglés (o en Python), la cuestión de cuándo utilizar etiquetas frente a atributos para una determinada información es en gran medida una cuestión de preferencia y estilo. Tanto el Ejemplo 4-10 como el 4-11, por ejemplo, contienen la misma información sobre este libro, pero cada uno está estructurado de forma ligeramente diferente.
Ejemplo 4-10. Ejemplo de atributos XML data-more del libro
<aBook>
<bookURL
url=
"https://www.oreilly.com/library/view/practical-python-data/
9781492091493"
/>
<bookAbstract>
There are awesome discoveries to be made and valuable stories to be told in datasets--and this book will help you uncover them.</bookAbstract>
<pubDate
date=
"2022-02-01"
/>
</aBook>
Ejemplo 4-11. Ejemplo de elementos data-more de un libro XML
<aBook>
<bookURL>
https://www.oreilly.com/library/view/practical-python-data/9781492091493</bookURL>
<bookAbstract>
There are awesome discoveries to be made and valuable stories to be told in datasets--and this book will help you uncover them.</bookAbstract>
<pubDate>
2022-02-01</pubDate>
</aBook>
Este grado de flexibilidad significa que XML es muy adaptable a una gran variedad de fuentes de datos y preferencias de formato. Al mismo tiempo, puede crear fácilmente una situación en la que cada nueva fuente de datos requiera escribir código personalizado. Obviamente, esto sería un sistema bastante ineficaz, especialmente si muchas personas y organizaciones publicaran tipos de datos bastante similares.
No es de extrañar, por tanto, que exista un gran número de especificaciones XML que definen reglas adicionales para dar formato a documentos XML destinados a contener determinados tipos de datos. Destaco aquí algunos ejemplos notables, ya que son formatos con los que puedes encontrarte en el curso de tu trabajo de gestión de datos. Sin embargo, a pesar de sus distintos nombres de formato y extensiones de archivo, podemos analizarlos todos con el mismo método que veremos en el Ejemplo 4-12:
- RSS
-
Really Simple Syndication es una especificación XML introducida por primera vez a finales de los 90 para la información de noticias. El formato .atom XML también se utiliza ampliamente para estos fines.
- KML
-
Keyhole Markup Language es una norma aceptada internacionalmente para codificar datos geográficos bidimensionales y tridimensionales, y es compatible con herramientas como Google Earth.
- SVG
-
Gráficos vectoriales escalables es un formato muy utilizado para gráficos en la web, gracias a su capacidad para escalar dibujos sin pérdida de calidad. Muchos programas gráficos comunes pueden generar archivos .svg, que luego pueden incluirse en páginas web y otros documentos que se verán bien en una amplia variedad de tamaños de pantalla y dispositivos.
- EPUB
-
El formato de publicación electrónica (.epub) es el estándar abierto ampliamente aceptado para la publicación de libros digitales.
Como puedes ver en la lista anterior, algunos formatos XML comunes indican claramente su relación con XML; muchos otros no.19
Ahora que tenemos una idea general de cómo funcionan los archivos XML, veamos qué se necesita para analizar uno con Python. Aunque Python tiene algunas herramientas integradas para analizar XML, utilizaremos una biblioteca llamada lxml, que es especialmente buena para analizar rápidamente archivos XML grandes. Aunque nuestros archivos de ejemplo que siguen son bastante pequeños, debes saber que podríamos utilizar básicamente el mismo código aunque nuestros archivos de datos fueran considerablemente mayores.
Para empezar, utilizaremos una versión XML de los mismos datos de desempleo "U6" que ya he descargado del sitio web de FRED utilizando su API.20 Tras descargar una copia de este archivo de Google Drive, puedes utilizar el script del Ejemplo 4-12 para convertir el XML fuente en un .csv. Empieza con el pip install
:
pip install lxml
Ejemplo 4-12. xml_parsing.py
# an example of reading data from an .xml file with Python, using the "lxml"
# library.
# first, you'll need to pip install the lxml library:
# https://pypi.org/project/lxml/
# a helpful tutorial can be found here: https://lxml.de/tutorial.html
# the data used here is an instance of
# https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE& \
# api_key=YOUR_API_KEY_HERE
# specify the "chapter" of the `lxml` library you want to import,
# in this case, `etree`, which stands for "ElementTree"
from
lxml
import
etree
# import the `csv` library, to create our output file
import
csv
# choose a filename
filename
=
"
U6_FRED_data
"
# open our data file in read format, using "rb" as the "mode"
xml_source_file
=
open
(
filename
+
"
.xml
"
,
"
rb
"
)
# pass our xml_source_file as an ingredient to the `lxml` library's
# `etree.parse()` method and store the result in a variable called `xml_doc`
xml_doc
=
etree
.
parse
(
xml_source_file
)
# start by getting the current xml document's "root" element
document_root
=
xml_doc
.
getroot
(
)
# let's print it out to see what it looks like
(
etree
.
tostring
(
document_root
)
)
# confirm that `document_root` is a well-formed XML element
if
etree
.
iselement
(
document_root
)
:
# create our output file, naming it "xml_"+filename+".csv
output_file
=
open
(
"
xml_
"
+
filename
+
"
.csv
"
,
"
w
"
)
# use the `csv` library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# grab the first element of our xml document (using `document_root[0]`)
# and write its attribute keys as column headers to our output file
output_writer
.
writerow
(
document_root
[
0
]
.
attrib
.
keys
(
)
)
# now, we need to loop through every element in our XML file
for
child
in
document_root
:
# now we'll use the `.values()` method to get each element's values
# as a list and then use that directly with the `writerow` recipe
output_writer
.
writerow
(
child
.
attrib
.
values
(
)
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
En este caso, no hay nada dentro del archivo de datos (como un nombre de hoja) que podamos utilizar como nombre de archivo, así que crearemos uno propio y lo utilizaremos tanto para cargar nuestros datos de origen como para etiquetar nuestro archivo de salida.
¡He mentido! Los valores que hemos estado utilizando para el "modo" de la función
open()
suponen que queremos interpretar el archivo fuente como texto. Pero como la biblioteca lxml espera datos en bytes en lugar de texto, utilizaremosrb
("leer bytes") como "modo".¡Hay mucho XML malformado por ahí! Para asegurarnos de que lo que parece un buen XML realmente lo es, recuperaremos el elemento "raíz" del documento XML actual y nos aseguraremos de que funciona.
Como nuestro XML está almacenado actualmente como datos de bytes, tenemos que utilizar el método
etree.tostring()
para verlo como uno solo.Gracias al lxml, cada elemento XML (o "nodo") de nuestro documento tiene una propiedad llamada
attrib
, cuyo tipo de dato es un diccionario Python (dict
). El método.keys()
devuelve todas las claves de atributo de nuestro elemento XML en forma de lista. Como todos los elementos de nuestro archivo fuente son idénticos, podemos utilizar las claves del primero para crear una fila de "cabecera" para nuestro archivo de salida.La biblioteca lxml convierte los elementos XML en listas, por lo que podemos utilizar un simple bucle
for...in
para recorrer los elementos de nuestro documento.
Resulta que la versión XML de nuestros datos de desempleo está estructurada de forma muy sencilla: es sólo una lista de elementos, y todos los valores a los que queremos acceder están almacenados como atributos. Como resultado, pudimos extraer los valores de los atributos de cada elemento como una lista y escribirlos directamente en nuestro archivo .csv con una sola línea de código.
Por supuesto, en hay muchas ocasiones en las que querremos extraer datos de formatos XML más complejos, especialmente como RSS o Atom. Para ver lo que se necesita para manejar algo ligeramente más complejo, en el Ejemplo 4-13 analizaremos el canal RSS de la BBC de noticias sobre ciencia y medio ambiente, del que puedes descargar una copia desde mi Google Drive.
Ejemplo 4-13. rss_parsing.py
# an example of reading data from an .xml file with Python, using the "lxml"
# library.
# first, you'll need to pip install the lxml library:
# https://pypi.org/project/lxml/
# the data used here is an instance of
# http://feeds.bbci.co.uk/news/science_and_environment/rss.xml
# specify the "chapter" of the `lxml` library you want to import,
# in this case, `etree`, which stands for "ElementTree"
from
lxml
import
etree
# import the `csv` library, to create our output file
import
csv
# choose a filename, for simplicity
filename
=
"
BBC News - Science & Environment XML Feed
"
# open our data file in read format, using "rb" as the "mode"
xml_source_file
=
open
(
filename
+
"
.xml
"
,
"
rb
"
)
# pass our xml_source_file as an ingredient to the `lxml` library's
# `etree.parse()` method and store the result in a variable called `xml_doc`
xml_doc
=
etree
.
parse
(
xml_source_file
)
# start by getting the current xml document's "root" element
document_root
=
xml_doc
.
getroot
(
)
# if the document_root is a well-formed XML element
if
etree
.
iselement
(
document_root
)
:
# create our output file, naming it "rss_"+filename+".csv"
output_file
=
open
(
"
rss_
"
+
filename
+
"
.csv
"
,
"
w
"
)
# use the `csv` library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# document_root[0] is the "channel" element
main_channel
=
document_root
[
0
]
# the `find()` method returns *only* the first instance of the element name
article_example
=
main_channel
.
find
(
'
item
'
)
# create an empty list in which to store our future column headers
tag_list
=
[
]
for
child
in
article_example
.
iterdescendants
(
)
:
# add each tag to our would-be header list
tag_list
.
append
(
child
.
tag
)
# if the current tag has any attributes
if
child
.
attrib
:
# loop through the attribute keys in the tag
for
attribute_name
in
child
.
attrib
.
keys
(
)
:
# append the attribute name to our `tag_list` column headers
tag_list
.
append
(
attribute_name
)
# write the contents of `tag_list` to our output file as column headers
output_writer
.
writerow
(
tag_list
)
# now we want to grab *every* <item> element in our file
# so we use the `findall()` method instead of `find()`
for
item
in
main_channel
.
findall
(
'
item
'
)
:
# empty list for holding our new row's content
new_row
=
[
]
# now we'll use our list of tags to get the contents of each element
for
tag
in
tag_list
:
# if there is anything in the element with a given tag name
if
item
.
findtext
(
tag
)
:
# append it to our new row
new_row
.
append
(
item
.
findtext
(
tag
)
)
# otherwise, make sure it's the "isPermaLink" attribute
elif
tag
==
"
isPermaLink
"
:
# grab its value from the <guid> element
# and append it to our row
new_row
.
append
(
item
.
find
(
'
guid
'
)
.
get
(
"
isPermaLink
"
)
)
# write the new row to our output file!
output_writer
.
writerow
(
new_row
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
Como siempre, querremos equilibrar lo que manejamos programáticamente y lo que revisamos visualmente. Al mirar nuestros datos, está claro que la información de cada artículo se almacena en un elemento
item
independiente. Sin embargo, como copiar los nombres de las etiquetas y atributos individuales nos llevaría mucho tiempo y sería propenso a errores, revisaremos un elementoitem
y haremos una lista de todas las etiquetas (y atributos) que contiene, que luego utilizaremos como cabeceras de columna para nuestro archivo .csv de salida.El método
iterdescendants()
es particular de la biblioteca lxml. Devuelve sólo los descendientes de un elemento XML, mientras que el método más comúniter()
devolvería tanto el elemento en sí como sus hijos o "descendientes".Si utilizas
child.tag
recuperarás el texto del nombre del elemento hijo. Por ejemplo, para el elemento<pubDate>`
devolverápubDate
.Sólo una etiqueta de nuestro elemento
<item>
tiene un atributo, pero aun así queremos incluirlo en nuestro resultado.El método
keys()
nos dará una lista de todas las claves de la lista de atributos que pertenecen a la etiqueta. Asegúrate de obtener su nombre como una cadena (en lugar de una lista de un elemento).Todo ese bucle de
article_example
for
era sólo para construirtag_list
-¡pero valió la pena!
Como puedes ver en el Ejemplo 4-13, con la ayuda de la biblioteca lxml, analizar incluso XML ligeramente más complejo en Python sigue siendo razonablemente sencillo.
Aunque XML sigue siendo un formato de datos popular para los feeds de noticias y un puñado de otros tipos de archivos, hay una serie de características que lo hacen menos que ideal para manejar los feeds de datos de gran volumen de la web moderna.
En primer lugar, está la simple cuestión del tamaño. Aunque los archivos XML pueden ser maravillosamente descriptivos -reduciendo la necesidad de diccionarios de datos separados-, el hecho de que la mayoría de los elementos contengan una etiqueta de apertura y su correspondiente etiqueta de cierre (por ejemplo, <item>
y </item>
) también hace que el XML sea algo verboso: hay mucho texto en un documento XML que no es contenido. Esto no es un gran problema cuando tu documento tiene unas pocas docenas o incluso unos pocos miles de elementos, pero cuando intentas manejar millones o miles de millones de publicaciones en la web social, todo ese texto redundante puede ralentizar mucho las cosas.
En segundo lugar, aunque XML no es exactamente difícil de transformar en otros formatos de datos, el proceso tampoco es exactamente fluido. La biblioteca lxml (entre otras) hace que el análisis sintáctico de XML con Python sea bastante sencillo, pero realizar la misma tarea con lenguajes centrados en la web como JavaScript es enrevesado y oneroso. Dada la prevalencia de JavaScript en la web, no es de extrañar que en algún momento se desarrolle un formato de datos tipo feed que funcione perfectamente con JavaScript. Como veremos en la siguiente sección, muchas de las limitaciones de XML como formato de datos se solucionan con la naturaleza similar a object
del formato .json, que es en este momento el formato más popular para datos de tipo feed en Internet .
JSON: Datos web, la próxima generación
En principio, JSON es similar a XML en que utiliza la anidación para agrupar piezas de información relacionadas en registros y campos. JSON también es bastante legible para las personas, aunque el hecho de que no admita comentarios significa que los feeds JSON pueden requerir diccionarios de datos más robustos que los documentos XML.
Para empezar, echemos un vistazo al pequeño documento JSON del Ejemplo 4-14.
Ejemplo 4-14. Ejemplo de documento JSON
{
"author"
:
"Susan E. McGregor"
,
"book"
:
{
"bookURL"
:
"https://www.oreilly.com/library/view/practical-python-data/
9781492091493/"
,
"bookAbstract"
:
"There are awesome discoveries to be made and valuable
stories to be told in datasets--and this book will help you uncover
them."
,
"pubDate"
:
"2022-02-01"
},
"papers"
:
[{
"paperURL"
:
"https://www.usenix.org/conference/usenixsecurity15/
technical-sessions/presentation/mcgregor"
,
"paperTitle"
:
"Investigating the computer security practices and needs
of journalists"
,
"pubDate"
:
"2015-08-12"
},
{
"paperURL"
:
"https://www.aclweb.org/anthology/W18-5104.pdf"
,
"paperTitle"
:
"Predictive embeddings for hate speech detection on
twitter"
,
"pubDate"
:
"2018-10-31"
}
]
}
Al igual que XML, las "reglas" gramaticales de JSON son bastante sencillas: sólo hay tres estructuras de datos distintas en los documentos JSON, todas las cuales aparecen en el Ejemplo 4-14:
- Pares clave/valor
-
Técnicamente, todo lo que hay en un documento JSON es un par clave/valor, siendo la clave entre comillas a la izquierda de los dos puntos (
:
) y el valor lo que aparezca a la derecha de los dos puntos. Ten en cuenta que, mientras que las claves deben ser siempre cadenas, los valores pueden ser cadenas (como enauthor
), objetos (como enbook
) o listas (como enpapers
). - Objetos
-
Éstos se abren y cierran en mediante pares de llaves (
{}
). En el Ejemplo 4-14, hay cuatro objetos en total: el propio documento (indicado por las llaves justificadas a la izquierda), el objetobook
y los dos objetos sin nombre de la listapapers
. - Listas
-
Son entre corchetes (
[]
) y sólo pueden contener objetos separados por comas.
Aunque XML y JSON pueden utilizarse para codificar los mismos datos, existen algunas diferencias notables en lo que permite cada uno. Por ejemplo, los archivos JSON no contienen una especificación doc-type
, ni pueden incluir comentarios. Además, mientras que las listas XML son en cierto modo implícitas (cualquier elemento repetido funciona como una lista), en JSON las listas deben especificarse mediante corchetes ([]
).
Por último, aunque JSON se diseñó pensando en JavaScript, te habrás dado cuenta de que sus estructuras son muy similares a los tipos dict
y list
de Python. Esto es parte de lo que hace que el análisis sintáctico de JSON sea muy sencillo tanto con Python como con JavaScript (y con otros muchos lenguajes).
Para ver lo sencillo que es, en el Ejemplo 4-15 analizaremos los mismos datos que en el Ejemplo 4-12, pero en el formato .json que también proporciona la API FRED. Puedes descargar el archivo desde este enlace de Google Drive: https://drive.google.com/file/d/1Mpb2f5qYgHnKcU1sTxTmhOPHfzIdeBsq/view?usp=sharing.
Ejemplo 4-15. json_parsing.py
# a simple example of reading data from a .json file with Python,
# using the built-in "json" library. The data used here is an instance of
# https://api.stlouisfed.org/fred/series/observations?series_id=U6RATE& \
# file_type=json&api_key=YOUR_API_KEY_HERE
# import the `json` library, since that's our source file format
import
json
# import the `csv` library, to create our output file
import
csv
# choose a filename
filename
=
"
U6_FRED_data
"
# open the file in read format ("r") as usual
json_source_file
=
open
(
filename
+
"
.json
"
,
"
r
"
)
# pass the `json_source_file` as an ingredient to the json library's `load()`
# method and store the result in a variable called `json_data`
json_data
=
json
.
load
(
json_source_file
)
# create our output file, naming it "json_"+filename
output_file
=
open
(
"
json_
"
+
filename
+
"
.csv
"
,
"
w
"
)
# use the `csv` library's "writer" recipe to easily write rows of data
# to `output_file`, instead of reading data *from* it
output_writer
=
csv
.
writer
(
output_file
)
# grab the first element (at position "0"), and use its keys as the column headers
output_writer
.
writerow
(
list
(
json_data
[
"
observations
"
]
[
0
]
.
keys
(
)
)
)
for
obj
in
json_data
[
"
observations
"
]
:
# we'll create an empty list where we'll put the actual values of each object
obj_values
=
[
]
# for every `key` (which will become a column), in each object
for
key
,
value
in
obj
.
items
(
)
:
# let's print what's in here, just to see how the code sees it
(
key
,
value
)
# add the values to our list
obj_values
.
append
(
value
)
# now we've got the whole row, write the data to our output file
output_writer
.
writerow
(
obj_values
)
# officially close the `.csv` file we just wrote all that data to
output_file
.
close
(
)
Como la biblioteca json interpreta cada objeto como un objeto de vista de diccionario, tenemos que decirle a Python que lo convierta en una lista normal utilizando la función
list()
.En la mayoría de los casos, la forma más sencilla de encontrar el nombre (o "clave") del objeto JSON principal de nuestro documento es simplemente mirarlo. Sin embargo, como los datos JSON suelen presentarse en una sola línea, podemos hacernos una mejor idea de su estructura pegándolos en JSONLint. Esto nos permite ver que nuestros datos de destino son una lista cuya clave es
observations
.Debido a la forma en que funciona la biblioteca json, si intentamos escribir directamente las filas, obtendremos los valores etiquetados con
dict
, en lugar de los valores de datospropiamente dichos. Así que tenemos que hacer otro bucle que recorra cada valor de cada objetojson
de uno en uno y añada ese valor a nuestra listaobj_values
.
Aunque JSON no es tan legible como XML, tiene otras ventajas que ya hemos mencionado, como un tamaño de archivo más pequeño y una compatibilidad de código más amplia. Del mismo modo, aunque JSON no es tan descriptivo como XML, las fuentes de datos JSON (a menudo API) suelen estar razonablemente bien documentadas; esto reduce la necesidad de deducir simplemente lo que describe un determinado parclave/valor. Sin embargo, como todo trabajo con datos, el primer paso para manejar datos con formato JSON es comprender su contexto tanto como sea posible .
Trabajar con datos no estructurados
Como comentamos en "Datos estructurados frente a datos no estructurados", el proceso de creación de datos depende de la introducción de cierta estructura en la información; de lo contrario, no podemos analizarla metódicamente ni deducir su significado. Aunque estos últimos suelen incluir grandes segmentos de texto "libre" escrito por humanos, tanto los datos de tipo tabla como los de tipo fuente están relativamente estructurados y, lo que es más importante, son legibles por máquinas.
Por el contrario, cuando tratamos con datos no estructurados, nuestro trabajo siempre implica aproximaciones: no podemos estar seguros de que nuestros esfuerzos de manipulación programática nos devuelvan una interpretación exacta de la información subyacente. Esto se debe a que la mayoría de los datos no estructurados son una representación de contenidos diseñados para ser percibidos e interpretados por humanos. Y, como expusimos en el Capítulo 2, aunque pueden procesar grandes cantidades de datos mucho más rápido y con menos errores que los humanos, los ordenadores aún pueden ser engañados por datos no estructurados que nunca engañarían a un humano, como confundir una señal de stop ligeramente modificada con una señal de límite de velocidad. Naturalmente, esto significa que cuando tratamos con datos que no son legibles por una máquina, siempre tenemos que hacer pruebas y verificaciones adicionales, pero Python puede ayudarnos a convertir esos datos en un formato más utilizable.
Texto basado en imágenes: Acceder a datos en PDF
El Formato de Documento Portátil (PDF) se creó a principios de la década de 1990 como mecanismo para preservar la integridad visual de los documentos electrónicos, ya fueran creados en un programa de edición de texto o capturados a partir de materiales impresos.22 Preservar el aspecto visual de los documentos también significaba que, a diferencia de los formatos legibles por máquina (como los documentos de procesamiento de textos), era difícil alterar o extraer su contenido, una característica importante para crear cualquier cosa, desde versiones digitales de contratos hasta cartas formales.
En otras palabras, manejar los datos de los PDF fue, al principio, algo difícil por diseño. Sin embargo, dado que el acceso a los datos de los documentos impresos es un problema compartido, el trabajo en el reconocimiento óptico de caracteres (OCR) de comenzó ya a finales del siglo XIX.23 Incluso las herramientas digitales de OCR están ampliamente disponibles en paquetes de software y en Internet desde hace décadas, por lo que, aunque distan mucho de ser perfectas, los datos contenidos en este tipo de archivos tampoco están totalmente fuera de nuestro alcance.
Cuándo trabajar con texto en PDF
En general, trabajar con PDF es un último recurso (muy parecido, como veremos en el Capítulo 5, a lo que debería ser el web scraping). En general, si puedes evitar confiar en la información de los PDF, deberías hacerlo. Como ya se ha dicho, el proceso de extracción de información de los PDF suele producir una aproximación al contenido del documento, por lo que comprobar su exactitud es una parte innegociable de cualquier flujo de trabajo de manipulación de datos basados en .pdf. Dicho esto, hay una enorme cantidad de información que sólo está disponible como imágenes o PDF de documentos escaneados, y Python es una forma eficaz de extraer una primera versión razonablemente precisa del texto de ese documento.
Dónde encontrar los PDF
Si estás seguro de que los datos que quieres sólo pueden encontrarse en formato PDF, entonces puedes (y debes) utilizar los consejos de "Búsqueda inteligente de tipos de datos específicos" para localizar este tipo de archivo mediante una búsqueda en línea. Lo más probable es que solicites información a una persona u organización, y que ésta te la proporcione en formato PDF, dejándote a ti el problema de cómo extraer la información que necesitas. Como resultado, la mayoría de las veces no necesitarás ir a buscar PDFs; lo más frecuente es que, desgraciadamente, te encuentren a ti.
Manipular PDFs con Python
Como los PDF pueden generarse tanto a partir de texto legible por máquina (como documentos de procesadores de texto) como a partir de imágenes escaneadas, a veces es posible extraer el texto "vivo" del documento mediante programación con relativamente pocos errores. Sin embargo, aunque parece sencillo, este método puede ser poco fiable porque los archivos .pdf pueden generarse con una amplia gama de codificaciones que pueden ser difíciles de detectar con precisión. Así que, aunque éste puede ser un método de alta precisión para extraer texto de un .pdf, la probabilidad de que funcione para cualquier archivo concreto es baja.
Por eso, voy a centrarme aquí en utilizar el OCR para reconocer y extraer el texto de los archivos .pdf. Esto requerirá dos pasos:
-
Convierte las páginas del documento en imágenes individuales.
-
Ejecuta el OCR en las imágenes de las páginas, extrae el texto y escríbelo en archivos de texto individuales.
Como era de esperar, necesitaremos instalar unas cuantas bibliotecas Python más para que todo esto sea posible. En primer lugar, instalaremos un par de bibliotecas para convertir nuestras páginas .pdf en imágenes. La primera es una biblioteca de propósito general llamada poppler
que es necesaria para que funcione nuestra biblioteca específica de Python pdf2image
. Utilizaremos pdf2image
para (¡lo has adivinado!) convertir nuestro archivo . pdf en una serie de imágenes:
sudo apt install poppler-utils
Entonces:
pip install pdf2image
A continuación, tenemos que instalar las herramientas para realizar el proceso de OCR. La primera es una biblioteca general llamada tesseract-ocr, que utiliza el aprendizaje automático para reconocer el texto de las imágenes; la segunda es una biblioteca de Python que se basa en tesseract-ocr llamada pytesseract:
sudo apt-get install tesseract-ocr
Entonces:
pip install pytesseract
Por último, necesitamos una biblioteca de ayuda para Python que pueda realizar la visión por ordenador necesaria para salvar la distancia entre nuestras imágenes de página y nuestra biblioteca de OCR:
pip install opencv-python
¡Uf! Si esto te parece un montón de bibliotecas adicionales, ten en cuenta que lo que técnicamente estamos utilizando aquí es aprendizaje automático, una de esas tecnologías de ciencia de datos de moda que impulsa gran parte de la "inteligencia artificial". Afortunadamente para nosotros, Tesseract en particular es relativamente robusto e inclusivo: aunque fue desarrollado originalmente por Hewlett-Packard como un sistema propietario a principios de los años 80, fue de código abierto en 2005 y actualmente es compatible con más de 100 idiomas, ¡así que no dudes en probar la solución del Ejemplo 4-16 también con texto que no esté en inglés!
Ejemplo 4-16. pdf_parsing.py
# a basic example of reading data from a .pdf file with Python,
# using `pdf2image` to convert it to images, and then using the
# openCV and `tesseract` libraries to extract the text
# the source data was downloaded from:
# https://files.stlouisfed.org/files/htdocs/publications/page1-econ/2020/12/01/ \
# unemployment-insurance-a-tried-and-true-safety-net_SE.pdf
# the built-in `operating system` or `os` Python library will let us create
# a new folder in which to store our converted images and output text
import
os
# we'll import the `convert_from_path` "chapter" of the `pdf2image` library
from
pdf2image
import
convert_from_path
# the built-in `glob`library offers a handy way to loop through all the files
# in a folder that have a certain file extension, for example
import
glob
# `cv2` is the actual library name for `openCV`
import
cv2
# and of course, we need our Python library for interfacing
# with the tesseract OCR process
import
pytesseract
# we'll use the pdf name to name both our generated images and text files
pdf_name
=
"
SafetyNet
"
# our source pdf is in the same folder as our Python script
pdf_source_file
=
pdf_name
+
"
"
# as long as a folder with the same name as the pdf does not already exist
if
os
.
path
.
isdir
(
pdf_name
)
==
False
:
# create a new folder with that name
target_folder
=
os
.
mkdir
(
pdf_name
)
# store all the pages of the PDF in a variable
pages
=
convert_from_path
(
pdf_source_file
,
300
)
# loop through all the converted pages, enumerating them so that the page
# number can be used to label the resulting images
for
page_num
,
page
in
enumerate
(
pages
)
:
# create unique filenames for each page image, combining the
# folder name and the page number
filename
=
os
.
path
.
join
(
pdf_name
,
"
p
"
+
str
(
page_num
)
+
"
.png
"
)
# save the image of the page in system
page
.
save
(
filename
,
'
PNG
'
)
# next, go through all the files in the folder that end in `.png`
for
img_file
in
glob
.
glob
(
os
.
path
.
join
(
pdf_name
,
'
*.png
'
)
)
:
# replace the slash in the image's filename with a dot
temp_name
=
img_file
.
replace
(
"
/
"
,
"
.
"
)
# pull the unique page name (e.g. `p2`) from the `temp_name`
text_filename
=
temp_name
.
split
(
"
.
"
)
[
1
]
# now! create a new, writable file, also in our target folder, that
# has the same name as the image, but is a `.txt` file
output_file
=
open
(
os
.
path
.
join
(
pdf_name
,
text_filename
+
"
.txt
"
)
,
"
w
"
)
# use the `cv2` library to interpret our image
img
=
cv2
.
imread
(
img_file
)
# create a new variable to hold the results of using pytesseract's
# `image_to_string()` function, which will do just that
converted_text
=
pytesseract
.
image_to_string
(
img
)
# write our extracted text to our output file
output_file
.
write
(
converted_text
)
# close the output file
output_file
.
close
(
)
Aquí pasamos la ruta a nuestro archivo fuente (en este caso, es sólo el nombre del archivo, porque está en la misma carpeta que nuestro script) y la resolución de puntos por pulgada (PPP) deseada de las imágenes de salida a la función
convert_from_path()
. Aunque establecer un DPI más bajo será mucho más rápido, las imágenes de peor calidad pueden producir resultados de OCR significativamente menos precisos. 300 PPP es una resolución estándar de calidad de "impresión".Aquí utilizamos la función
.join
de la biblioteca os para guardar los nuevos archivos en nuestra carpeta de destino. También tenemos que utilizar la funciónstr()
para convertir el número de página en una cadena para utilizarla en el nombre del archivo.Ten en cuenta que
*.png
puede traducirse como "cualquier archivo que termine en .png". La funciónglob()
crea una lista de todos los nombres de archivo de la carpeta donde están almacenadas nuestras imágenes (que en este caso tiene el valorpdf_name
).La manipulación de cadenas es complicada. Para generar nombres de archivo únicos (¡pero coincidentes!) para nuestros archivos de texto con OCR, tenemos que sacar un nombre de página único de
img_file
cuyo valor empiece porSafetyNet/
y termine en.png
. Así que sustituiremos la barra oblicua por un punto para obtener algo comoSafetyNet.p1.png
, y luego sisplit()
que en el punto, obtendremos una lista como:["SafetyNet", "p1", "png"]
. Por último, podemos acceder al "nombre de la página" en la posición 1. Tenemos que hacer todo esto porque no podemos estar seguros de queglob()
, por ejemplo, extraiga primerop1.png
de la carpeta de imágenes, o de que extraiga las imágenes en orden.
En su mayor parte, la ejecución de este script sirve a nuestros propósitos: con unas pocas docenas de líneas de código, convierte un archivo PDF de varias páginas primero en imágenes y luego escribe (la mayor parte) del contenido en una serie de nuevos archivos de texto.
Sin embargo, este enfoque "todo en uno" también tiene sus limitaciones. Convertir un PDF en imágenes -o imágenes en texto- es el tipo de tarea que podemos necesitar hacer con bastante frecuencia, pero no siempre al mismo tiempo. En otras palabras, probablemente sería mucho más útil a largo plazo disponer de dos scripts separados para resolver este problema, y ejecutarlos uno tras otro. De hecho, con un pequeño ajuste, probablemente podríamos dividir el script anterior de tal forma que pudiéramos convertir cualquier PDF en imágenes o cualquier imagen en texto sin tener que escribir ningún código nuevo. Suena bastante ingenioso, ¿verdad?
Este proceso de replanteamiento y reorganización del código de trabajo se conoce en como refactorización del código. En inglés escrito, lo describiríamos como revisar o editar, y el objetivo en ambos casos es el mismo: hacer tu trabajo más sencillo, claro y eficaz. Y al igual que la documentación, la refactorización es en realidad otra forma importante de escalar tu trabajo de manipulación de datos, porque hace que la reutilización de tu código sea mucho más sencilla. Veremos varias estrategias de refactorización de código y reutilización de scripts en el Capítulo 8.
Acceder a tablas PDF con Tabula
Si has mirado los archivos de texto producidos en la sección anterior, probablemente te habrás dado cuenta de que hay muchos "extras" en esos archivos: números de página y encabezados, saltos de línea y otras "tonterías". También faltan algunos elementos clave, como imágenes y tablas.
Aunque nuestro trabajo con datos no se extienda al análisis de imágenes (eso es un área mucho más especializada), no es raro encontrar tablas dentro de los PDF que contienen datos con los que podríamos querer trabajar. De hecho, este problema es lo suficientemente común en mi campo de trabajo, el periodismo, como para que un grupo de periodistas de investigación diseñara y construyera una herramienta llamada Tabula específicamente para tratar este problema.
Tabula no es una biblioteca de Python, sino un software independiente. Para probarlo, descarga el instalador para tu sistema; si utilizas un Chromebook o una máquina Linux, tendrás que descargar el archivo .zip y seguir las instrucciones del archivo README.txt. Sea cual sea el sistema que utilices, probablemente tendrás que instalar primero la biblioteca de programación Java, lo que puedes hacer ejecutando el siguiente comando en una ventana de terminal:
sudo apt install default-jre
Al igual que otras herramientas de código abierto de las que hablaremos en capítulos posteriores (como OpenRefine, que utilicé para preparar algunos de los datos de muestra en el Capítulo 2 y que trataré brevemente en el Capítulo 11), Tabula realiza su trabajo entre bastidores (aunque parte de él es visible en la ventana del terminal), y tú interactúas con ella en un navegador web. Es una forma de acceder a una interfaz gráfica más tradicional, dejando libres la mayor parte de los recursos de tu ordenador para hacer el trabajo pesado con los datos de .
Conclusión
Esperamos que los ejemplos de codificación de este capítulo hayan empezado a darte una idea de la gran variedad de problemas de manejo de datos que puedes resolver con relativamente poco código Python, gracias a una combinación de bibliotecas cuidadosamente seleccionadas y a esos pocos conceptos esenciales de scripting en Python que se introdujeron en el Capítulo 2.
También te habrás dado cuenta de que, a excepción de nuestro texto en PDF, el resultado de todos los ejercicios de este capítulo era esencialmente un archivo .csv. Esto no es casualidad. No sólo los archivos . csv son eficientes y versátiles, sino que resulta que para hacer casi cualquier análisis estadístico básico o visualización, necesitamos datos de tipo tabla con los que trabajar. Eso no quiere decir que no sea posible analizar datos que no sean tablas; de hecho, de eso trata gran parte de la investigación informática contemporánea (como el aprendizaje automático). Sin embargo, como esos sistemas suelen ser complejos y opacos, no son realmente adecuados para el tipo de trabajo de manipulación de datos en el que nos centramos aquí. Por ello, dedicaremos nuestra energía a los tipos de análisis que pueden ayudarnos a comprender, explicar y comunicar nuevos conocimientos sobre el mundo.
Por último, aunque nuestro trabajo en este capítulo se ha centrado en datos basados en archivos y en versiones preguardadas de datos basados en feeds, en el Capítulo 5 exploraremos cómo podemos utilizar Python con API y web scraping para extraer datos de sistemas en línea y, cuando sea necesario, ¡de las propias páginas web!
1 Compáralos con otros que quizá conozcas, como mp4 o png, que suelen asociarse a música e imágenes, respectivamente.
2 ¡Aunque dentro de poco sabrás cómo arreglártelas!
3 En realidad, no necesitarás tanta suerte: veremos cómo hacerlo en "Manejar PDFs con Python".
4 En informática, los términos datos e información se aplican exactamente al revés: los datos son los hechos en bruto recogidos sobre el mundo, y la información es el producto final significativo de organizarlos y estructurarlos. Sin embargo, en los últimos años, a medida que las conversaciones sobre "grandes datos" han dominado muchos campos, la interpretación que utilizo aquí se ha hecho más común, por lo que me ceñiré a ella a lo largo de este libro para mayor claridad.
5 De la Oficina de Estadísticas Laborales de EEUU.
6 "Multiple Jobholders" de Stéphane Auray, David L. Fuller y Guillaume Vandenbroucke, publicado el 21 de diciembre de 2018, https://research.stlouisfed.org/publications/economic-synopses/2018/12/21/multiple-jobholders.
7 Véase "New Recommendations on Improving Data on Contingent and Alternative Work Arrangements", https://blogs.bls.gov/blog/tag/contingent-workers;"The Value of Flexible Work: Evidence from Uber Drivers" de M. Keith Chen y otros, Nber Working Paper Series nº 23296, https://nber.org/system/files/working_papers/w23296/w23296.pdf.
8 También puedes encontrar instrucciones al respecto en el sitio web de FRED.
9 También puedes descargar copias de estos archivos directamente de mi Google Drive.
10 En el momento de escribir esto, LibreOffice puede manejar el mismo número de filas que Microsoft Excel (220), pero muchas menos columnas. Mientras que Google Sheets puede manejar más columnas que Excel, sólo puede manejar unas 40.000 filas.
11 En el momento de escribir esto, todas estas bibliotecas ya están disponibles y listas para usar en Google Colab.
12 Para más información sobre get_rows()
, consulta la documentaciónde xlrd.
13 Consulta la documentaciónde xlrd para saber más sobre este tema.
14 De Stephen John Machin y Chris Withers, "Fechas en hojas de cálculo Excel".
15 Si abres los archivos de salida de los tres ejemplos de código anteriores en un editor de texto, te darás cuenta de que el formato .ods de código abierto es el más sencillo y limpio.
16 Como, por ejemplo, en Pensilvania o Colorado.
17 Véase el post de Vassili van der Mersch, "Twitter's 10 Year Struggle with Developer Relations" de Nordic APIs.
18 A diferencia del código Python, los documentos XML no necesitan una indentación adecuada para funcionar, ¡aunque sin duda los hace más legibles!
19 Dato curioso: ¡la segunda x del formato .xlsx se refiere en realidad a XML!
20 De nuevo, recorreremos paso a paso el uso de APIs como ésta en el Capítulo 5, pero utilizar este documento nos permite ver cómo influyen los distintos formatos de datos en nuestras interacciones con ellos.
21 Si se ha aplicado una hoja de estilo, como en el caso del feed de la BBC que utilizamos en el Ejemplo 4-13, puedes hacer clic contextual en la página y seleccionar "Ver fuente" para ver el XML "en bruto".
22 Consulta la página Acerca del PDF de Adobe para obtener más información.
23 George Nagy, "Desarrollos perturbadores en el reconocimiento de documentos", Pattern Recognition Letters 79 (2016): 106–112, https://doi.org/10.1016/j.patrec.2015.11.024. Disponible en https://ecse.rpi.edu/~nagy/PDF_chrono/2016_PRL_Disruptive_asPublished.pdf.
Get Tramitación práctica de datos y calidad de datos en Python now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.