Convertire una data JSON in un oggetto datetime Python

Abbiamo una stringa JSON che contiene una data:

fonte = '{"ragione_sociale": "CIR 2000", "aggiornato_il": "Wed, 06 Jun 2012 14:19:53 UTC"}'

Vediamo che succede se la convertiamo in un dizionario Python:

import simplejson as json

json.loads(fonte)
{'aggiornato_il': 'Wed, 06 Jun 2012 14:19:53 UTC ', 'ragione_sociale': 'CIR 2000'}

Facile, vero? C’è però un piccolo problema: aggiornato_il è ancora una stringa mentre a noi, per poterlo elaborare comodamente, serve un campo datetime.datetime. Come mai il pur potente modulo simplejson non converte correttamente la nostra data?

Il problema

Il fatto è che il JSON originale non fa nulla per informarci del fatto che il campo aggiornato_il esprime in realtà una data. La radice del problema sta nello standard JSON il quale non contempla la dichiarazione esplicita dei tipi di dati. In effetti la stessa questione si pone nella conversione di un numero non intero: va considerato un float, oppure un decimal? In quest’ultimo caso simplejson ci viene in aiuto con il parametro parse_float:

import simplejson as json

json.loads('1.1', parse_float=decimal.Decimal)
Decimal('1.1')

Purtroppo, per il motivo visto prima, non esiste un equivalente per le date.

La soluzione classica

In questi casi la prassi comune è rendere esplicito, già all’interno della stringa JSON, il formato del campo. Qualcosa del genere:

{"aggiornato_il": "$date: Wed, 06 Jun 2012 14:19:53 UTC"}

Così facendo possiamo in seguito manipolare il dizionario restituito dal metodo loads: intercettare la direttiva $date e sostituire finalmente la stringa con una data. Una soluzione più raffinata è quella di sfruttare l’opzione object_hook che consente di invocare una nostra funzione ad ogni chiamata del metodo loads.

La mia soluzione

Nel mio caso il provider JSON è esterno, e non desidero obbligarlo a pre-processare le stringhe JSON inserendo clausole $date arbitrarie solo per soddisfare le esigenze della mia applicazione. Sfruttando l’opzione object_hook già accennata ho ottenuto una soluzione del tutto trasparente:

fonte = '{"aggiornato_il": "Thu, 1 Mar 2012 10:00:49 UTC"}'
dct = json.loads(fonte, object_hook=datetime_parser)
dct
{u'aggiornato_il': datetime.datetime(2012, 3, 1, 10, 0, 49)}


def datetime_parser(dct):
    for k, v in dct.items():
        if isinstance(v, basestring) and re.search(" UTC", v):
            try:
                dct[k] = datetime.datetime.strptime(v, DATE_FORMAT)
            except:
                pass
    return dct

La funzione datetime_parser esamina gli elementi della stringa JSON. In caso di corrispondenza alla espressione regolare indicata in re.search tentiamo una conversione diretta al formato datetime. Data la specificità della espressione regolare la conversione dovrebbe avere successo. Il blocco try... except ci permette di ignorare un eventuale errore: in questo caso infatti presumiamo che si tratti di una stringa vera e propria e non, malgrado la somiglianza, di una data. L’unico vincolo imposto al provider JSON è l’adozione di un formato standard per rappresentare le date. Nel mio caso è il seguente:

DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'

Per approfondimenti vi consiglio:

Per completezza c’è da aggiungere che l’adozione di un json schema consentirebbe la specifica dei tipi di dati JSON. Quest’ultima soluzione però, data la sua complessità, non è applicabile nel mio e in molti altri casi.

PS: ne ho scritto anche su Stack Overflow.

  • http://www.lucabacchi.it Luca

    Non conoscevo l’attributo “object_hook”.

    In generale ho cominciato ha gestire le date in formato json usando banalmente gli epoch, cioè degli interi.

    La conversione da epoch in datetime.datetime Python o in Date javascript è sempre banale. Inoltre l’epoch è un riferimento temporale assoluto, non ho bisogno di portarmi dietro le informazioni sulla time-zone.

    • http://nicolaiarocci.com/about/ Nicola Iarocci

      Per usi interni l’epoch va alla grande! Questo è un caso diverso perché JSON viaggia su un canale esterno (inviato via http a una RESTful API) e non volevo costringere i client in conversioni forzate.

      Internamente le cose funzionano in maniera diversa anche per noi. Salviamo le date in mongodb (che a sua volta converte il datetime in BSON usando il trucco del $date) e addirittura i decimal (importi in valuta) in millesimi, figurati.

  • http://www.lucabacchi.it Luca

    In realtà questo tuo articolo solleva una questione molto interessante su cui mi sono scontrato varie volte e che non ho mai visto molto affrontata in rete (forse però qui sono io che pecco di ignoranza):

    Se fosse sensato accompagnare un JSON con dei metadati che permettessero di interpretare correttamente alcuni campi. Quello che tu ad esempio fai con “$date” in quella che chiami “la soluzione classica”.

    L’approccio che spesso ho in realtà adottato è quello di non usare metadati, ma assumere che il ricevente abbia contezza del dato che sta ricevendo: in pratica, chi riceve il JSON sa (deve sapere) che il campo “aggiornato_il” è una data e interpretarla come tale.

    Mi chiedo se esista un dibattito in proposito su questi due approcci:
    – uso di metadati
    vs
    – so cosa mi devo aspettare

    p.s. In questo caso tu adotti un terzo approccio che, semplificando, potremmo chiamare “indovino il dato analizzandone il contenuto”…

    • http://nicolaiarocci.com/about/ Nicola Iarocci

      Senz’altro migliore il tuo approccio a patto che, appunto, sia mittente che destinatario conoscano a priori il formato dei dati che stanno transitando. In caso contrario preferisco la soluzione qui proposta all’uso dei meta-dati, che mi sembrano la soluzione più costosa sia in termini di banda utilizzata che, probabilmente, di lavoro da fare (ricevente e mittente sono entrambi impegnati, da un lato a inserire i meta-dati e dall’altro a interpretarli). Come al solito molto dipende dallo scenario (il dominio) che si sta affrontando.

  • Pingback: Decodificare le date in un flusso JSON (Python) | Nicola Iarocci()