Leer y escribir ficheros json en python

De ChuWiki


Veamos como convertir un objeto python a json, tanto en memoria, como escribiendo o leyendo de un fichero. Para ellos necesitamos el módulo json que viene por defecto en la instalación de python. Tienes el código de ejemplo de este tutorial en python-json

Importar librería json[editar]

Lo primero, como siempre, importar la librería que nos hace falta

import json

El módulo json viene por defecto con la instalación de python. Hay otras librerías adicionales que podríamos instalarnos, pero lo dejamos para otros tutoriales.

Convertir entre dict y JSON[editar]

Un JSON no es más que una cadena de texto, bien leída de un fichero, bien contenida en una variable string. Para manejarla en nuestro programa y acceder o rellenar sus datos, es mejor hacerlo como dict. Afortunadamente, con la librería json la conversión de uno a otro es bastante rápida.

Convertir dict a JSON : dumps()[editar]

El módulo tiene la función dumps() a la que se le pasa un diccionario y devuelve un string json.

# Un dict
data = {"nombre": "juan", "apellido": "Perez", "fecha_nacimiento": {"año": 2002, "mes": 12, "dia": 1}}

# Convertir dict a json string
json_data = json.dumps(data)
print(type(json_data))
print(json_data)

# json_data es de tipo string. La salida de este código es 
# <class 'str'>
# {"nombre": "juan", "apellido": "Perez", "fecha_nacimiento": {"a\u00f1o": 2002, "mes": 12, "dia": 1}}

Hemos llamado a json.dumps() pasando como parámetro un dict (diccionario) de python. Obtenemos de resultado un objeto de tipo cadena cuyo contenido es el dict convertido a json. Fíjate que caracteres extraños como la ñ de año los convierte a formato unicode, puesto que json no los acepta.

Convertir JSON a dict : loads()[editar]

La función loads() permite leer la cadena y volver a convertirla en dict.

# Utilizamos la cadena generada antes
dict_data = json.loads(json_data)
print(type(dict_data))
print(dict_data)

# dict_data es un dict. La salida de este código es
# <class 'dict'>
# {'nombre': 'juan', 'apellido': 'Perez', 'fecha_nacimiento': {'año': 2002, 'mes': 12, 'dia': 1}}

Leer y escribir en un fichero JSON[editar]

Sabiendo convertir de JSON a dict y de dict a JSON, sería fácil leer y escribir de un fichero JSON. Bastaría leerlo o escribirlo como string y luego hacer la conversión a o desde dict. Pero la librería json nos ofrece funciones para hacerlo directamente.

Las funciones dump() y load() funcionan igual que dumps() y loads(), pero en vez de sobre un string, lo hacen sobre un fichero. Para ello debemos pasarles un descriptor de fichero abierto.

Escribir fichero json con python: dump()[editar]

El siguiente ejemplo usa dump() para escribir un fichero JSON a partir de un dict

# un dict con los datos
data = {"nombre": "juan", "apellido": "Perez", "fecha_nacimiento": {"año": 2002, "mes": 12, "dia": 1}}

# Apertura y escritura del fichero JSON
with open("json-data.txt", "w") as file:
   json.dump(data, file)

Hemos abierto el fichero con la función open() como fichero de escritura modo texto y obtenemos su descriptor como file. Nos basta llamar a dump() para salvar el dict python en formato json en un fichero. El contenido del fichero sería

{"nombre": "juan", "apellido": "Perez", "fecha_nacimiento": {"a\u00f1o": 2002, "mes": 12, "dia": 1}}

Leer fichero json con python: load()[editar]

Para leer el fichero, basta con abrilo para lectura y usar la función load()

# Abrimos de lectura el fichero generado antes
with open("json-data.txt", "r") as file:
    data_object = json.load(file)

# Sacamos por pantalla lo leído, un dict.
print(type(data_object))
print(data_object)

# La salida de este programa es
# <class 'dict'>
# {'nombre': 'juan', 'apellido': 'Perez', 'fecha_nacimiento': {'año': 2002, 'mes': 12, 'dia': 1}}

Parámetros avanzados de dump() y dumps()[editar]

Hemos usado dump() y dumps() da la forma más sencilla, dejando por defecto los parámetros opcionales de dichas funciones. Veamos cuales son y su significado. Se pone el valor que tiene por defecto.

  • skipkeys=False. Si hay algún valor en el dict que no es un tipo serializable a json, da un error. Por ejemplo, python admite números complejos, pero json no los admite. Si uno de los valores del dict en un número complejo, estas funciones darán una excepción. Poniendo a True este parámetro no saltará la excepción, simplemente se saltará el valor que no irá al json.
  • ensure_ascii=True. Si hay caracteres extraños en el dict, como la ñ que pusimos antes, se convierte a unicode para asegurar que es compatible con json. Si ponemos esta opción a False, no se hará esta conversión y en json se escribirá la ñ tal cual.
  • check_circular=True. Si dict contiene una referencia circular salta un error. Una referencia circular es que el objeto dict, dentro de una de sus claves, tenga directamente o indirecamente una referencia a sí mismo. En nuestros ejemplos, si justo después de definir data como un dict, hacemos data["circular"] = data estamos incluyendo en la clave "circular" una referencia a sí mismo. Daría error de recursión. Si ponemos a False este parámetro, no se hace esta verificación, por lo que el json crecerá indefinidamente hasta comerse toda la memoria.
  • allow_nan=True. json no admite valores nan, inf o -inf. Con la opción por defecto se generará el json con los textos NaN, Infinity y -Infinity, aunque no son estrictamente válidos en json. Si ponemos esta opción a False, si dict tiene alguno de estos valores, saltará un error.
  • cls=None. Usa el encoder a json por defecto. En este parámetro podríamos pasar un encoder propio, por ejemplo, que sea capaz de convertir a json de una forma válida los números complejos.
  • indent=None. Con el valor por defecto crea el json de la forma más compacta posible, es decir, una sola línea. Si ponemos un valor positivo, hará un "pretty print" del json indentando el número de espacios que le indiquemos como valor positivo. Si ponemos un string, indentará usando ese string.
  • separators=None. Los separadores por defecto para json son ", " y ": ", con espacio detrás. Podemos poner aquí los separadores que queramos como una tupla, por ejemplo, podemos quitar los espacios (",",":")
  • default=None. Como hemos comentado antes, si hay un valor en el dict que no se puede convertir a json salta un error. Por ejemplo un número complejo. Si pasamos como parámetro default una función que admita un parámetro y devuelva su representación en json, se usará esta función en vez de dar error. El json contendrá como valor lo que nuestra función default() haya decidido.
  • sort_keys=False. Si se pone a True el json tendrá ordenada alfabéticamente las claves.

json pretty print con python[editar]

Si queremos una cadena de texto json correctamente indentada, se hace con el parámetro indent que acabamos de ver. Ponemos aquí un ejemplo puesto que suele ser una consulta muy habitual por la gente que empieza.

data = {"nombre": "juan", "apellido": "Perez", "fecha_nacimiento": {"año": 2002, "mes": 12, "dia": 1}}

string = json.dumps(data, indent=3)
print(string)

Simplemente hemos añadido a dumps el parámetro indent=3 para obtener el json indentado de tres en tres espacios. La salida de este programa sería

{
   "nombre": "juan",
   "apellido": "Perez",
   "fecha_nacimiento": {
      "a\u00f1o": 2002,
      "mes": 12,
      "dia": 1
   }
}

Parámetros avanzados de load() y loads()[editar]

Para leer un json y convertirlo en dict hemos usado la forma fácil, las funciones load() y loads() sin parámetros opcionales. Pero hay parámetros opcionales que podemos cambiar. Los ponemos a continuación, junto con su valor por defecto.

  • cls=None Usa el decoder json por defecto. Aquí podemos pasar un decoder propio que hagamos.
  • object_hook=None Aquí podemos pasar una función mi_funcion() propia a la que la función loads() llamará pasando como parámetro cada dict que vaya parseando del json. Nuestra función puede modificar ese dict y devolverlo modificado. La función loads() usará el valor devuelto por nuestra función en vez de el dict que ella ha obtenido del json. Esto nos permite hacer un decoder propio sin necesidad de parsear el json desde cero.
  • object_pairs_hook=None Similar a object_hook pero nuestra función recibirá una lista de pares clave:valor. Podemos modificar y devolver lo modificado. La función loads() usará lo que le devolvamos en vez de lo que ella ha obtenido.
  • parse_float=None. Podemos pasar una función propia para parsear los números flotantes. Por defecto se usa la función propia de python float(), pero podemos hacer una función con otro tipo de conversión de str a float si queremos.
  • parse_int=None. Idem a parse_float pero para enteros.
  • parse_constant=None. Idem a las anteriores, pero se le llamará cuando de json se lean los valores NaN, Infinity y -Infinity

Encoder y Decoder de json: JSONEncoder y JSONDecoder[editar]

Las clases JSONEncoder y JSONDecoder son las que usan por defecto las funciones mencionadas hasta ahora. Sin embargo, estas clases están accesibles y podemos hacer dos cosas con ellas:

  • Instanciarlas y usarlas. No tiene demasiado sentido teniendo las funciones fáciles load(), loads(), dump() y dumps(). Tienes no obstante un ejemplo en la función con_encoders() de json-string.py
  • Heredar de ellas para hacer nuestro propio coder o encoder con características particulares en las que tengamos interés. De alguna manera podemos hacer lo mismo con los parámetros avanzados de las funciones load(), loads(), dump() y dumps(), pero aquí sería cuestión de sobreescribir métodos de JSONEncoder y JSONDecoder

Un ejemplo para números complejos de y a json[editar]

Aunque el estándar json no permite números complejos, vamos a ver cómo se haría el decoder y el encoder para que esté soportado. Vamos primero con el encoder

class MyEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, complex):
            return {"complex": '%d+%dj' % (o.real, o.imag)}
        return json.JSONEncoder.default(self, o)

Hemos heredado de json.JSONEncoder. Sobreescribimos el método default(). Se llamará a este método uno de los tipos del dict de python no tenga conversión estándar a json, por ejemplo, un número complejo. En el métoco comprobamos si el objeto o que nos pasan es un complejo y en caso afirmativo devolvemos un dict {"complex": "2+3j"}. El valor será el que sea, no el que he puesto aquí y va como string, de forma que luego podamos hacer fácilmente la conversión contraria. Si el valor no es de tipo complejo, llamamos al método de la clase padre y devolvemos lo que esta devuelva. Es de interés notar que como a este método sólo nos llaman si el tipo no tiene conversión estándar a json, la llamada a la clase padre dará un error.

La salida en json para un número complejo sería un texto {"complex": "2+3j"}

Vamos ahora con el decoder.

class MyDecoder(json.JSONDecoder):
    def __init__(self):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook)

    def object_hook(self, dct):
        if "complex" in dct:
            value = complex(dct["complex"])
            return value
        return dct

Heredamos de json.JSONDecoder. Ponemos un constructor llamando al de la clase padre para que el parámetro object_hook llame a nuestro método object_hook(). Podríamos ponerle cualquier otro nombre. La clase padre llamará al método object_hook() cada vez que haya decodificado un dict. En el método comprobamos que la clave "complex" está en el dict. En caso afirmativo, convertimos dct["complex"] que será el string "2+3j" en un número complejo. Para ello usamos la función complex() que admite como parámetro el string y devuelve el número complejo. Nuesta función object_hook() devuelve entonces el número complejo como tipo complex de python. Si el dict no contiene la clave "complex", lo devolvemos tal cual.

En json-custom-encoders.py tienes el ejemplo completo con un main de prueba.

Enlaces[editar]