Saltar al contenido

Trabajando con ficheros csv usando el módulo csv de Python

Dentro de la biblioteca estándar tenemos un módulo que sirve para leer ficheros csv. Este módulo no es excesivamente útil si usamos numpy o pandas pero como no siempre disponemos de estos módulos está bien que le echemos un vistazo.

Primero de todo, ¿qué es un fichero con formato csv? En principio, es un fichero tabular de texto con diferentes columnas separadas por comas. Leer más sobre esto aquí.

Vamos a empezar importando el módulo y vemos lo que nos proporciona:

import csv

print(dir(csv))

Lo anterior nos mostrará:

['Dialect', 'DictReader', 'DictWriter', 'Error', 'OrderedDict', 
'QUOTE_ALL', 'QUOTE_MINIMAL', 'QUOTE_NONE', 'QUOTE_NONNUMERIC', 
'Sniffer', 'StringIO', '_Dialect', '__all__', '__builtins__', 
'__cached__', '__doc__', '__file__', '__loader__', '__name__', 
'__package__', '__spec__', '__version__', 'excel', 'excel_tab', 
'field_size_limit', 'get_dialect', 'list_dialects', 're', 'reader',
'register_dialect', 'unix_dialect', 'unregister_dialect', 'writer']

Hay dos nombres que parecen evidentes, reader y writer. Luego hay otras cosas que iremos viendo poco a poco.

La ayuda para reader y writer dice lo siguiente:

help(csv.reader)
Help on built-in function reader in module _csv:

reader(...)
    csv_reader = reader(iterable [, dialect='excel']
                            [optional keyword args])
        for row in csv_reader:
            process(row)
    
    The "iterable" argument can be any object that returns a line
    of input for each iteration, such as a file object or a list.  The
    optional "dialect" parameter is discussed below.  The function
    also accepts optional keyword arguments which override settings
    provided by the dialect.
    
    The returned object is an iterator.  Each iteration returns a row
    of the CSV file (which can span multiple input lines).
help(csv.writer)
Help on built-in function writer in module _csv:

writer(...)
    csv_writer = csv.writer(fileobj [, dialect='excel']
                                [optional keyword args])
        for row in sequence:
            csv_writer.writerow(row)
    
        [or]
    
        csv_writer = csv.writer(fileobj [, dialect='excel']
                                [optional keyword args])
        csv_writer.writerows(rows)
    
    The "fileobj" argument can be any object that supports the file API.

csv.reader sirve para leer ficheros en formato csv y csv.writer sirve para escribir ficheros en formato csv. Vemos que para ambas opciones existe una keyword que se llama dialect. Dentro del módulo tenemos varios dialects definidos:

print(csv.list_dialects())

Lo anterior me muestra en pantalla:

['excel', 'excel-tab', 'unix']

De serie a mi me salen tres dialectos distintos.

Pero, ¿qué es esto de un dialecto?

Vamos a ver en qué se basa un dialect.

print(csv.excel)
print(dir(csv.excel))

Lo anterior mostrará esto:

<class 'csv.excel'>
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', 
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', 
'__init__', '__init_subclass__', '__le__', '__lt__', '__module__', 
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
'__weakref__', '_name', '_valid', '_validate', 'delimiter', 
'doublequote', 'escapechar', 'lineterminator', 'quotechar', 
'quoting', 'skipinitialspace']

Un dialecto contiene formas de definir cómo será el fichero en formato csv que hemos de leer o que hemos de escribir. Por ejemplo, el dialect excel presenta lo siguiente:

print(f"Separador: {csv.excel.delimiter}")
print(f"Doble comilla: {csv.excel.doublequote}")
print(f"Caracter de escape: {csv.excel.escapechar}")
print(f"Caracter de fin de línea: {repr(csv.excel.lineterminator)}")
print(f"Caracter de comillas: {csv.excel.quotechar}")
print(f"Control de cuando generar comillas: {csv.excel.quoting}")
print(f"Saltas espacio inicial: {csv.excel.skipinitialspace}")

Que mostrará en pantalla algo parecido a esto:

Separador: ,
Doble comilla: True
Caracter de escape: None
Caracter de fin de línea: '\r\n'
Caracter de comillas: "
Control de cuando generar comillas: 0
Saltas espacio inicial: False

Es decir, como csv es un formato que deja muchas cosas en el aire y es texto plano podemos definir tipos del mismo usando dialectos. Dependiendo del problema que tengamos que resolver podremos adecuar nuestro dialecto a las necesidades.

Algunas de estas cosas quedarán más claras leyendo la documentación aquí y/o viendo los ejemplos que vamos a ver a continuación.

Vamos a crear un fichero con formato csv con el que trabajar. Yo lo estoy haciendo con IPython o Jupyter Notebook, que tiene un comando mágico llamado writefile, que me permite escribir un fichero de texto a disco. Tú lo puedes hacer igual o puedes usar un editor de texto como Geany, Notepad, Notepad++, vim,… Lo único que tienes que hacer es guardarlo en el mismo sitio desde donde estés ejecutando el código que mostraré a continuación y con el mismo nombre que estoy usando, ‘datos.csv‘:

%%writefile datos.csv
"a","b","c"
1,2,3
11,22,33

Para leer el fichero anterior podemos usar lo siguiente, donde definimos que el delimitador de campos es la coma, ‘,’:

with open("./datos.csv", newline="") as csvfile:
    reader = csv.reader(csvfile, delimiter=",")
    for row in reader:
        print(row)

Que mostrará en pantalla lo siguiente:

['a', 'b', 'c']
['1', '2', '3']
['11', '22', '33']

Podéis ver que los valores los ha leído como cadenas de texto, a pesar de que hay números, porque, por defecto, no hace ningún tipo de conversión de datos. Esta función acepta una serie de parámetros que son similares a lo que tenemos disponible en el dialect. En un dialecto, como hemos visto más arriba, podemos definir el delimiter, doublequote,… Y eso mismo lo podemos usar como argumentos para csv.reader. El valor por defecto para quoting es csv.QUOTE_MINIMAL.

Si queremos que los valores que son números se transformen en valores numéricos una vez que los hemos leído podemos usar el el argumento quoting y pasarle el parámetro csv.QUOTE_NONNUMERIC que transformará a float todo lo que no esté entrecomillado:

with open("./datos.csv", newline="") as csvfile:
    reader = csv.reader(csvfile, delimiter=",", quoting=csv.QUOTE_NONNUMERIC)
    for row in reader:
        print(row)

Que mostrará en pantalla lo siguiente:

['a', 'b', 'c']
[1.0, 2.0, 3.0]
[11.0, 22.0, 33.0]

Si creo un nuevo fichero csv (fíjate que se llama datos_.csv) con una de las letras sin entrecomillar (fijaos en la primera ‘a’) y uso el código anterior me dirá que no lo puede leer:

%%writefile datos_.csv
a,"b","c"
1,2,3
11,22,33

Y lo leo con el siguiente código:

with open("./datos_.csv", newline="") as csvfile:
    reader = csv.reader(csvfile, delimiter=",", quoting=csv.QUOTE_NONNUMERIC)
    for row in reader:
        print(row)

Me mostrará un error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-11-81b6f7ce2940> in <module>
      1 with open("./datos_.csv", newline="") as csvfile:
      2     reader = csv.reader(csvfile, delimiter=",", quoting=csv.QUOTE_NONNUMERIC)
----> 3     for row in reader:
      4         print(row)

ValueError: could not convert string to float: 'a'

Si ahora uso csv.QUOTE_NONE me entrecomillará todo y mantendrá las comillas originales (aquí vuelvo a usar el fichero datos.csv del principio).

with open("./datos.csv", newline="") as csvfile:
    reader = csv.reader(csvfile, delimiter=",", quoting=csv.QUOTE_NONE)
    for row in reader:
        print(row)

Lo anterior mostrará en pantalla:

['"a"', '"b"', '"c"']
['1', '2', '3']
['11', '22', '33']

En lugar de usar reader podríamos usar DictReader que nos permite leer las filas como un diccionario usando el cabecero como clave:

with open("./datos.csv", newline="") as csvfile:
    reader = csv.DictReader(csvfile, delimiter=",", quoting=csv.QUOTE_NONNUMERIC)
    for row in reader:
        print(row)
        print(row['a'])

Lo anterior mostrará esto en pantalla:

OrderedDict([('a', 1.0), ('b', 2.0), ('c', 3.0)])
1.0
OrderedDict([('a', 11.0), ('b', 22.0), ('c', 33.0)])
11.0

Podemos crear nuestro propio dialecto para, por ejemplo, escribir nuestro propio formato a disco:

class MiDialecto(csv.Dialect):
    delimiter = "|"
    quoting = csv.QUOTE_NONE
    quotechar = '"'
    lineterminator = "\n"

mi_dialecto = MiDialecto()

Vamos a usar el nuevo dialecto creado. Leemos el fichero en formato csv que creamos antes y lo vamos a guardar usando nuestro nuevo dialecto:

with open("./datos.csv", newline="") as csvin, open("./datos_new.csv", "w") as csvout:
    reader = csv.DictReader(csvin, delimiter=",", quoting=csv.QUOTE_NONNUMERIC)
    writer = csv.writer(csvout, dialect=mi_dialecto)
    writer.writerow(reader.fieldnames)
    for row in reader:
        writer.writerow(row.values())

Ahora deberíamos tener el nuevo fichero, ‘datos_new.csv‘, separado por barras verticales. Os debería haber quedado algo parecido a lo siguiente:

a|b|c
1.0|2.0|3.0
11.0|22.0|33.0

Dentro del módulo csv hay una utilidad muy interesante que se llama Sniffer. Imaginemos que nos llegan ficheros que pueden tener diferentes separadores (por ejemplo, comas o puntos y comas,…) pero a priori no sabemos de qué tipo será o si tendrá un cabecero,… Sniffer nos puede ayudar con ello. Veamos como.

snf = csv.Sniffer()

Leo el fichero que hemos creado.

with open("./datos_new.csv", "r") as f:
    sample = "".join(f.readlines())
    print(f"¿Tiene cabecero? {snf.has_header(sample)}")
    dialecto = snf.sniff(sample)
    print(f"El delimitador es: {dialecto.delimiter}")

Y me mostrará en pantalla lo siguiente:

¿Tiene cabecero? True
El delimitador es: |

Un segundo ejemplo:

sample = """1,2,3
11,22,33
111,222,333
"""
print(f"¿Tiene cabecero? {snf.has_header(sample)}")
dialecto = snf.sniff(sample)
print(f"El delimitador es: {dialecto.delimiter}")

Que mostrará:

¿Tiene cabecero? False
El delimitador es: ,

De esta forma podríamos ver que lo que nos llega parece que NO tiene un cabecero y parece que el separador es ,. Por supuesto, esto no es infalible pero es mejor que nada.

Para terminar os dejo un EJERCICIO. Tenéis que escribir una función que sea capaz de recibir datos en formato csv que puedan estar separados por ,, ; o (espacio) y puedan tener o no cabecero, lo pueda leer y nos devuelva un diccionario donde la clave sea el campo del cabecero y el valor sea una lista con los valores de la columna. Si no tiene cabecero, que nombre las columnas en orden alfabético comenzando por ‘a’. Por ejemplo, si recibo esto:

col1,col2,col3
1,2,3
11,22,33

que me devuelva esto:

resultado = {
    'col1': [1,11],
    'col2': [2,22],
    'col3': [3,33],
}

O, si recibo esto:

1,2,3
11,22,33

que me devuelva esto:

resultado = {
    'a': [1,11],
    'b': [2,22],
    'c': [3,33],
}

Usad los comentarios para mandar el código.

Un saludo.

4 comentarios en «Trabajando con ficheros csv usando el módulo csv de Python»

  1. si tengo un archivo csv que cargué y no tiene encabezados, cómo lo puedo guardar agregando encabezados? Sin la necesidad de usar pandas…Saludos

    1. Hola, Rocío.

      En el código donde hablo del writer casi tienes lo que necesitas. Es muy fácil adaptar ese código a lo que necesitas. Inténtalo y si no lo consigues vuelve a comentar 😉

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

+ forty eight = fifty five

Pybonacci