Reordenar columnas de texto con una sola sentencia Python

Hace días que no hago elucubraciones en Python, así que hoy voy a enseñaros algo que hice ayer por pura necesidad. Se trata de tomar un texto que contiene columnas separadas por tabuladores y transformarlo cambiando de orden unas columnas y suprimiendo otras.

Tengo una hoja Excel donde llevo mis cuentas, y que relleno copiando directamente los asientos desde la página web de mi banco. Las columnas del Excel coinciden con el orden de la web, y por eso basta la copia directa. Pero ayer hacía dos meses que no hacía cuentas (efecto colateral de EQ17KT), y tuve que recurrir al histórico del banco. Y en el histórico, qué mala suerte, las columnas son distintas.

En las dos imágenes siguientes se ve más claro:

datos recientes
Formato de los datos recientes
Formato de los datos históricos
Formato de los datos históricos

Para trabajar con más comodidad, tomé los datos copiados y los guardé en el fichero VISA.CSV. Nada impide insertarlos directamente como texto multilinea en Python, usando la sintaxis de triple comillas:

[code language=”python”]
texto = """
Este es un texto
multilínea
copiado directamente
"""
[/code]

Abrir y leer un fichero en Python es sencillo:

[code language=”python”]
f = open("visa.csv", "r")
texto = f.read()
f.close()
[/code]

Otra forma es usando un bloque with, que hace lo mismo pero evitando tener que cerrar el fichero de forma explícita:

[code language=”python”]
with open("visa.csv", "r") as f:
texto = f.read()
[/code]

El texto contiene líneas de texto y otras en blanco:

[code]
09/08/2014 1234 1234 1234 5678 CAPRABO – 17 RAMBLA MARIN 44,81 EUR

12/08/2014 1234 1234 1234 5678 BSM ZONA BLAVA 1,54 EUR

13/08/2014 1234 1234 1234 5678 CAPRABO – 17 RAMBLA MARIN 34,34 EUR

16/08/2014 1234 1234 1234 5678 CAPRABO – 17 RAMBLA MARIN 31,02 EUR

03/08/2014 1234 1234 1234 5678 PARKEON 7,40 EUR

18/08/2014 1234 1234 1234 5678 CAPRABO – 17 RAMBLA MARIN 28,82 EUR

[/code]

Lo que queremos hacer con estos datos es:

  1. Trocear el texto en líneas.
  2. Eliminar las líneas en blanco.
  3. Trocear los campos de cada línea.
  4. Seleccionar los campos deseados.
  5. Volver a crear la línea.
  6. Volver a crear el texto entero.

Vamos allá paso a paso.

Trocear un texto en líneas

Esto es muy fácil en Python con la función split(). A esta función le pasamos un parámetro que es la cadena que queremos usar como separador, y nos devuelve una lista con todos los trozos encontrados.

[code language=”python”]
lineas = texto.split("n")
print(lineas)
## [‘línea 1’, ‘línea 2’, …]
[/code]

Eliminar líneas en blanco

La forma convencional sería un bucle que creara una nueva lísta que contuviera sólo las líneas con contenido. En Python quedaría así:

[code language=”python”]
lineas_datos = [] ## Inicializar lista vacía
for linea in lineas:
if linea:
lineas_datos.append(linea)
[/code]

Pero Python tiene un mecanismo compacto para generar listas, que convierte el código anterior en una sola línea:

[code language=”python”]
lineas_datos = [linea.strip() for linea in lineas if linea]
[/code]

Esta sintaxis contiene el bucle (for linea in lineas), la expresión condicional (if linea) y la expresión a aplicar a cada elemento antes de incorporarlo a la lista (linea.strip()). La función strip() la añado porque algunas líneas vienen con espacios en blanco al principio y/o al final y así las limpio de paso.

Trocear los campos de cada línea.

Puesto que tenemos una lista con cada una de las líneas, tendríamos que hacer un bucle línea por línea aplicando a cada una la función split(“t”), que nos trocearía usando el tabulador como división.

Podría volver a usar la forma larga del apartado anterior, pero ya puedes imaginar que preferiré la compacta de nuevo:

[code language=”python”]
lineas_campos = [linea.strip(‘t’) for linea in lineas_datos]
[/code]

Fíjate que la expresión linea.strip(‘t’) va a generar una lista de campos, y el generador compacto de listas una lista con un elemento por fila. Así, tendremos una estructura de lista de listas:

[code language=”python”]
## [[campo1.1, campo1.2, …], [campo2.1, campo2.2, …], …]
[/code]

Seleccionar los campos deseados.

Usaremos otro generador compacto para recorrer la lista anterior. El elemento del bucle será en este caso la lista interior, la de los campos. Lo que haremos será crear una lista nueva con los elementos deseados y en el orden correcto:

[code language=”python”]
lista_campos = [[lista[2], lista[0], lista[3][:-5]] for lista in lineas_campos]
[/code]

El tercer elemento lista[3][:-5] no es más que una cadena de texto de la que quito los 5 últimos caracteres. La sintaxis para trocear un texto en Python es texto_cortado = texto[origen:final], donde origen y final son índices numéricos, pero estos parámetros pueden ser negativos o no ponerse y Python es capaz de imaginarse qué quiso uno decir en cada caso.

Llegados aquí ya tenemos una lista de listas, conteniendo éstas últimas los campos deseados y en el orden correcto. Nos queda revertir proceso, convirtiendo las listas en cadenas con separadores (primero con tabuladores y después con retornos de carro).

Volver a crear la línea.

Para esto tenemos la función inversa de split(), join(). La sintaxis es un poco particular:

[code language=”python”]
print("***".join(["hola", "mundo", "cruel"])
## hola***mundo***cruel
[/code]

La cadena a intercalar es a la que se aplica el método, y la lista de cadenas a concatenar aparece como parámetro. Aplicaremos el método a cada sublista de lista_campos, con lo que la lista volverá a tener cadenas (las nuevas líneas).

[code language=”python”]
lista_lineas = ["t".join(campos) for campos in lista_campos]
[/code]

Volver a crear el texto entero.

Esto es fácil de adivinar: volveremos a aplicar join() a la lista de líneas:

[code language=”python”]
texto_nuevo = "n".join(lista_lineas)
[/code]

Verás que como aquí sólo hay una lista que se convierte en texto, no se usa el constructor de listas.

Todo de una tacada

En plan lucimiento, se pueden integrar todos los constructores de listas en una sola secuencia. Aquí está el código, por si alguien quiere comprobar que funciona. Los comentarios ilustran la secuencia que se sigue a la hora de ejecutar el código.

[code language=”python”]
with open("visa.csv", "r") as f: ## 0. Aquí se abre el fichero de texto
print( "n".join( ## 12. Aquí se unen todas las líneas de la lista en el texto completo y se imprime
[‘t’.join(L4) ## 11. Aquí volvemos a unir los campos de L4 con tabuladores (saldrá lista de filas)
for L4 ## 10. L4 es cada una de esas nuevas listas de campos en 9.
in [[L3[2], L3[0], L3[3][:-5]] ## 9. Esto es una nueva lista de campos, con selecc.reordenada de la original L3

## El tercer elemento es la descripción
## La fecha es el primero
## El cuarto es el importe, quitando los últimos 5 caracteres (" EUR")

for L3 ## 8. L3 es cada lista de campos obtenida de la línea L2
in [L2.split("t") ## 7. Trocear cada línea L2 por tabs
for L2 ## 6. L2 es cada línea destripada de blancos (excluidas las vacías)
in [L.strip() ## 5. Quitar blancos por delante (y detrás)
for L ## 3. Para cada línea
in f.read(). ## 1. Leer el texto desde el fichero
split("n") ## 2. Trocear la cadena en líneas
if len(L) > 0] ## 4. Quitar lineas vacías
]
]
]
))
[/code]

Tuitear El Quijote!

[code language=”python”]
import re
import codecs

LIBRO = ‘QUIJOTE’
num_tweet = 1
PATRON = ‘(W)’
LONGITUD_FRAGMENTO = 180
LONGITUD_TWEET = 140
CABECERA = ‘{libro} {num_tweet}. ‘

## Cargo el texto. El original en
## http://www.gutenberg.org/files/2000/old/2donq10.txt
## He suprimido los encabezados, incluyendo el prólogo
## de Cervantes y los sonetos, para empezar por el
## emblemático ‘En un lugar de La Mancha…’ —-
fichero = codecs.open(‘quijote.txt’, ‘r’, ‘utf-8’)
quijote = fichero.read()
fichero.close()

## En este algoritmo, el texto se va recortando por
## su principio según se va usando —-
while quijote:

## Se toman 180 caracteres, que sobren, para recortar —-
quijote_fragmento = quijote[:LONGITUD_FRAGMENTO]

## La cabecera es ‘QUIJOTE 125. ‘, por ejemplo —-
cabecera = CABECERA.format(libro=LIBRO, num_tweet=num_tweet)
longitud_cabecera = len(cabecera)

## Esto serán los caracteres disponibles para tuitear —-
longitud_disponible = LONGITUD_TWEET – longitud_cabecera

## Aquí troceamos por palabras. Si la plantilla incluye
## paréntesis en los grupos de captura, también se guardan
## los separadores. Ver documentación Python:
## https://docs.python.org/3.4/library/re.html —-
pedazos_fragmento = re.split(PATRON, quijote_fragmento)

## Tomamos los, digamos, 20 fragmentos, y vamos quitando
## del final (19, 18…), hasta que el número de caracteres
## que quedan encajan en el espacio disponible —-
for i in range(len(pedazos_fragmento), 0, -1):

tweet_menguante = ”.join(pedazos_fragmento[:i])

if len(tweet_menguante) <= longitud_disponible:
## Cuando ya cabe, unimos cabecera y cuerpo y listo —-
tweet = cabecera + tweet_menguante
num_tweet += 1
quijote = quijote.replace(tweet_menguante, ”, 1)

## Ya podemos salir del bucle de trozos menguantes —-
break

## Aquí imprimimos el tuit, o lo guardamos en una base de datos
## para usarlos ya pretroceados más adelante —-
#print(tweet)
if num_tweet % 100 == 0: print(num_tweet)

[/code]