2011-12-08 14 views
13

Sto usando lo strumento dateutil.parser di python per analizzare alcune date che sto ricevendo da un feed di terze parti. Permette di specificare una data di default, che a sua volta è impostata per default, per riempire gli elementi mancanti della data analizzata. Sebbene questo sia in generale di aiuto, non esiste un predefinito corretto per il mio caso d'uso, e preferirei trattare date parziali come se non avessi ottenuto una data (poiché quasi sempre ho ottenuto dati confusi). Ho scritto il seguente lavoro intorno:Analizzare una data in python senza usare un valore predefinito

from dateutil import parser 
import datetime 

def parse_no_default(dt_str): 
    dt = parser.parse(dt_str, default=datetime.datetime(1900, 1, 1)).date() 
    dt2 = parser.parse(dt_str, default=datetime.datetime(1901, 2, 2)).date() 
    if dt == dt2: 
    return dt 
    else: 
    return None 

(. Questo frammento guarda solo alla data, come è tutto quello che mi interessa per la mia domanda, ma la logica simile potrebbe essere esteso per includere la componente temporale)

Mi chiedo (sperando) ci sia un modo migliore per farlo. L'analisi della stessa stringa due volte solo per vedere se si riempie di valori predefiniti diversi sembra un enorme spreco di risorse, per non dire altro.

Ecco la serie di test (utilizzando generatori nosetest) per il comportamento previsto:

import nose.tools 
import lib.tools.date 

def check_parse_no_default(sample, expected): 
    actual = lib.tools.date.parse_no_default(sample) 
    nose.tools.eq_(actual, expected) 

def test_parse_no_default(): 
    cases = ( 
     ('2011-10-12', datetime.date(2011, 10, 12)), 
     ('2011-10', None), 
     ('2011', None), 
     ('10-12', None), 
     ('2011-10-12T11:45:30', datetime.date(2011, 10, 12)), 
     ('10-12 11:45', None), 
     ('', None), 
    ) 
    for sample, expected in cases: 
    yield check_parse_no_default, sample, expected 

risposta

8

A seconda del dominio seguente soluzione potrebbe funzionare:

DEFAULT_DATE = datetime.datetime(datetime.MINYEAR, 1, 1) 

def parse_no_default(dt_str):  
    dt = parser.parse(dt_str, default=DEFAULT_DATE).date() 
    if dt != DEFAULT_DATE: 
     return dt 
    else: 
     return None 

Un altro approccio sarebbe quello di scimmia patch di parser classe (questo è molto hackiesh, quindi non lo consiglio se avete altre opzioni):

import dateutil.parser as parser 
def parse(self, timestr, default=None, 
      ignoretz=False, tzinfos=None, 
      **kwargs): 
    return self._parse(timestr, **kwargs) 
parser.parser.parse = parse 

Si può usare come segue:

>>> ddd = parser.parser().parse('2011-01-02', None) 
>>> ddd 
_result(year=2011, month=01, day=02) 
>>> ddd = parser.parser().parse('2011', None) 
>>> ddd 
_result(year=2011) 

Controllando cui i membri disponibili a risultato (ddd) è possibile determinare quando il ritorno Nessuno. Quando tutti i campi disponibili che è possibile convertire ddd in oggetto datetime:

# ddd might have following fields: 
# "year", "month", "day", "weekday", 
# "hour", "minute", "second", "microsecond", 
# "tzname", "tzoffset" 
datetime.datetime(ddd.year, ddd.month, ddd.day) 
+0

Questo risolve solo la stringa vuota. Quando ho una data parziale, è ancora predefinito i campi non specificati, ma ottiene una data finale diversa da quella predefinita. Ho aggiunto alcuni test unitari alla domanda per illustrare i requisiti e dove questo esempio fallisce. Grazie per aver dato un'occhiata però! –

+1

Fai attenzione, apparentemente nel tuo primo esempio stai confrontando un oggetto data con un oggetto datetime. Sarà sempre non-uguale. –

0

Ho incontrato lo stesso problema con dateutil, ho scritto questa funzione e pensato che avrei post-it per il bene di posteri. Fondamentalmente utilizzando il metodo _parse sottostante come suggerisce @ILYA Khlopotov:

from dateutil.parser import parser 
import datetime 
from StringIO import StringIO 

_CURRENT_YEAR = datetime.datetime.now().year 
def is_good_date(date): 
    try: 
     parsed_date = parser._parse(parser(), StringIO(date)) 
    except: 
     return None 
    if not parsed_date: return None 
    if not parsed_date.year: return None 
    if parsed_date.year < 1890 or parsed_date.year > _CURRENT_YEAR: return None 
    if not parsed_date.month: return None 
    if parsed_date.month < 1 or parsed_date.month > 12: return None 
    if not parsed_date.day: return None 
    if parsed_date.day < 1 or parsed_date.day > 31: return None 
    return parsed_date 

L'oggetto restituito non è un'istanza datetime, ma ha il .year, .month, e, .day attributi, che era abbastanza buono per le mie esigenze. Suppongo che potresti facilmente convertirlo in un'istanza datetime.

0

simple-date fa questo per te (prova più formati, internamente, ma non tutti quelli che potresti pensare, perché i pattern che usa estendono i pattern data di python con parti opzionali, come espressioni regolari).

vedere https://github.com/andrewcooke/simple-date - ma solo python 3.2 e versioni successive (mi dispiace).

è più indulgente di quello che si desidera per impostazione predefinita:

>>> for date in ('2011-10-12', '2011-10', '2011', '10-12', '2011-10-12T11:45:30', '10-12 11:45', ''): 
... print(date) 
... try: print(SimpleDate(date).naive.datetime) 
... except: print('nope') 
... 
2011-10-12 
2011-10-12 00:00:00 
2011-10 
2011-10-01 00:00:00 
2011 
2011-01-01 00:00:00 
10-12 
nope 
2011-10-12T11:45:30 
2011-10-12 11:45:30 
10-12 11:45 
nope 

nope 

ma è possibile specificare il proprio formato.ad esempio:

>>> from simpledate import SimpleDateParser, invert 
>>> parser = SimpleDateParser(invert('Y-m-d(%T|)?(H:M(:S)?)?')) 
>>> for date in ('2011-10-12', '2011-10', '2011', '10-12', '2011-10-12T11:45:30', '10-12 11:45', ''): 
... print(date) 
... try: print(SimpleDate(date, date_parser=parser).naive.datetime) 
... except: print('nope') 
... 
2011-10-12 
2011-10-12 00:00:00 
2011-10 
nope 
2011 
nope 
10-12 
nope 
2011-10-12T11:45:30 
2011-10-12 11:45:30 
10-12 11:45 
nope 

nope 

ps il invert() appena commuta la presenza di % che altrimenti diventa un vero disastro quando specificare i pattern data complessi. ecco solo il letterale T personaggio ha bisogno di un prefisso % (data standard di Python formattazione sarebbe l'unico personaggio alfanumerico senza prefisso)

3

Questo è probabilmente un "hack", ma sembra che dateutil guarda molto pochi attributi fuori dal valore predefinito in cui passi. Potresti fornire un datetime 'falso' che esplode nel modo desiderato.

>>> import datetime 
>>> import dateutil.parser 
>>> class NoDefaultDate(object): 
...  def replace(self, **fields): 
...   if any(f not in fields for f in ('year', 'month', 'day')): 
...    return None 
...   return datetime.datetime(2000, 1, 1).replace(**fields) 
>>> def wrap_parse(v): 
...  _actual = dateutil.parser.parse(v, default=NoDefaultDate()) 
...  return _actual.date() if _actual is not None else None 
>>> cases = (
... ('2011-10-12', datetime.date(2011, 10, 12)), 
... ('2011-10', None), 
... ('2011', None), 
... ('10-12', None), 
... ('2011-10-12T11:45:30', datetime.date(2011, 10, 12)), 
... ('10-12 11:45', None), 
... ('', None), 
... ) 
>>> all(wrap_parse(test) == expected for test, expected in cases) 
True 
+0

Nice, clean hack anche se è un hack! +1 – tzaman

+0

Inoltre, leggendo kwargs della funzione 'replace' posso scoprire quali elementi della data sono stati specificati nella stringa passata. Solo anno, o anno w/mese ecc. Esattamente quello di cui avevo bisogno. – Winand

+0

Questo sembrava buono ma non ha funzionato per me al momento. Ho modificato la funzione in questo modo e questo sembra risolverlo: 'def wrap_parse (v): try: _actual = ... tranne AttributeError: _actual = None' – user2205380