Analizar una fecha en python sin utilizar un valor predeterminado

Estoy usando la herramienta dateutil.parser de python para analizar algunas fechas que dateutil.parser de un feed de terceros. Permite especificar una fecha predeterminada, que a su vez está predeterminada a la fecha actual, para completar los elementos faltantes de la fecha analizada. Si bien esto es útil en general, no hay un valor predeterminado sano para mi caso de uso, y preferiría tratar las fechas parciales como si no hubiera tenido una cita (ya que casi siempre significa que obtuve datos confusos). He escrito el siguiente trabajo alrededor:

 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 

(Este fragmento solo se ve en la fecha, ya que eso es lo único que me importa para mi aplicación, pero se podría extender una lógica similar para incluir el componente de tiempo).

Me pregunto (esperando) que hay una mejor manera de hacer esto. Analizar la misma cadena dos veces solo para ver si rellena los valores predeterminados diferentes parece ser una gran pérdida de recursos, por decir lo menos.

Aquí está el conjunto de pruebas (usando los generadores más novedosos) para el comportamiento esperado:

 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 

Dependiendo de su dominio, la siguiente solución podría funcionar:

 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 

Otro enfoque sería utilizar la clase de analizador de parches de mono (esto es muy complicado, por lo que no lo recomendaría si tuviera otras opciones):

 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 

Puedes usarlo de la siguiente manera:

 >>> 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) 

Al marcar qué miembros están disponibles en el resultado (ddd), puede determinar cuándo devolver Ninguno. Cuando todos los campos disponibles pueden convertir ddd en un objeto datetime:

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

Probablemente sea un “truco”, pero parece que dateutil ve muy pocos atributos fuera del valor predeterminado que usted pasa. Puede proporcionar una fecha “falsa” que explote de la manera deseada.

 >>> 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 

Me encontré con el mismo problema exacto con dateutil, escribí esta función y pensé que lo publicaría por la posteridad. Básicamente, usar el método subyacente _parse como @ILYA Khlopotov sugiere:

 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 

El objeto devuelto no es una instancia de datetime y datetime , pero tiene los .year , .month y .day , que fue lo suficientemente bueno para mis necesidades. Supongo que podría convertirlo fácilmente en una instancia de datetime y datetime .

simple-date lo hace por usted (intenta varios formatos, internamente, pero no tantos como pueda pensar, porque los patrones que utiliza extienden los patrones de fecha de python con partes opcionales, como las expresiones regulares).

vea https://github.com/andrewcooke/simple-date – pero solo python 3.2 y superior (lo siento).

es más indulgente que lo que quiere por defecto:

 >>> 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 

pero podrías especificar tu propio formato. por ejemplo:

 >>> from simpledate import SimpleDateParser, invert >>> parser = SimpleDateParser(invert('Ymd(%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 the invert() simplemente cambia la presencia de % que, de lo contrario, se convierte en un verdadero desastre al especificar patrones de fecha complejos. así que aquí solo el carácter T literal necesita un % prefijo (en el formato de fecha estándar de Python sería el único carácter alfanumérico sin un prefijo)