2011-12-08 29 views
13

Estoy usando la herramienta dateutil.parser de python para analizar algunas fechas que obtengo de un feed de terceros. Permite especificar una fecha predeterminada, que por defecto es la actual, para completar los elementos faltantes de la fecha analizada. Si bien esto es útil en general, no existe una falla en el sistema para mi caso de uso, y preferiría tratar las fechas parciales como si no hubiera tenido una fecha (ya que casi siempre significa que obtuve datos confusos). He escrito el siguiente trabajo en torno a:Analizando una fecha en python sin usar un valor predeterminado

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 sólo se ve en la fecha, ya que eso es todo lo que importa para mi aplicación, pero la lógica similar podría ampliarse para incluir el componente de tiempo)

Me pregunto (con la esperanza) que hay una mejor manera de hacerlo. Analizar la misma cadena dos veces solo para ver si cumple diferentes valores predeterminados parece una gran pérdida de recursos, por decir lo menos.

Aquí está el conjunto de pruebas (usando generadores nosetest) 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 

Respuesta

8

Dependiendo de su dominio 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 la de mono analizador parche clase (esto es muy hackiesh, entonces no lo recomendaría si tiene 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 

Se puede utilizar 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 comprobar que los miembros disponible en resultado (DDD) se podía determinar cuándo retorno Ninguno. Cuando todos los campos que se puede convertir en un objeto de fecha y hora ddd:

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

Eso solo resuelve la caja vacía. Cuando tengo una fecha parcial, todavía está incumpliendo los campos no especificados, pero obtiene una fecha final diferente a la predeterminada. He agregado algunas pruebas unitarias a la pregunta para ilustrar los requisitos y dónde falla este ejemplo. ¡Gracias por echar un vistazo! –

+1

Tenga cuidado, aparentemente en su primer ejemplo está comparando un objeto de fecha con un objeto de fecha y hora. Siempre va a ser no igual. –

0

me encontré con el mismo problema con dateutil, escribí esta función y pensé que iba a publicar por el bien de la posteridad. Básicamente mediante el método _parse subyacentes 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 datetime, pero tiene la .year, .month y, .day atributos, que era lo suficientemente bueno para mis necesidades. Supongo que podría convertirlo fácilmente en una instancia datetime.

0

simple-date hace esto por usted (intenta múltiples formatos, internamente, pero no tantos como podría pensar, porque los patrones que usa amplían los patrones de fecha de python con partes opcionales, como expresiones regulares).

ver https://github.com/andrewcooke/simple-date - pero solo Python 3.2 y superior (lo siento).

Es más indulgente que lo que desea de forma predeterminada:

>>> 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 puede especificar su propio formato.por ejemplo:

>>> 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 la invert() simplemente conmuta la presencia de % que de otro modo se convierta en un verdadero desastre al especificar patrones de fecha complejos. por lo que aquí sólo el literal T personaje necesita un prefijo % (en la fecha de Python estándar del formato sería el único carácter alfanumérico sin prefijo)

3

Esto es probablemente un "Hack", pero parece que dateutil mira muy pocos atributos fuera del valor predeterminado que ingresa. Podría proporcionar una fecha de reproducción '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 
+0

¡Cortar agradable, limpio incluso si es un truco! +1 – tzaman

+0

También leyendo kwargs de la función 'replace' puedo averiguar qué elementos de fecha se especificaron en la cadena pasada. Solo año, año año/mes, etc. Exactamente lo que necesitaba. – Winand

+0

Esto se veía bien pero no funcionó para mí actualmente. Modifiqué la función de esta manera y parece que la soluciona: 'def wrap_parse (v): try: _actual = ... excepto AttributeError: _actual = None' – user2205380

Cuestiones relacionadas