2011-12-08 12 views
13

Używam narzędzie Pythona dateutil.parser do parsowania niektórych dat, które otrzymuję z kanału innej firmy. Pozwala na określenie domyślnej daty, która jest domyślna na dzień dzisiejszy, do wypełniania brakujących elementów analizowanej daty. Chociaż jest to ogólnie pomocne, nie ma rozsądnego ustawienia domyślnego dla mojego przypadku użycia i wolałbym traktować częściowe daty tak, jakbym nigdy nie dostał daty (ponieważ prawie zawsze oznacza to, że otrzymałem zniekształcone dane). Pisałem następujące prace wokół:Parsowanie daty w python bez użycia domyślnego

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 

(. Ten fragment wygląda tylko na dzień, jak to wszystko zależy mi na mój wniosek, ale podobna logika może być rozszerzony o komponent czasowy)

Zastanawiam się (mając nadzieję), że jest lepszy sposób na zrobienie tego. Parsowanie tego samego ciągu dwa razy tylko po to, by zobaczyć, czy wypełnia on różne wartości domyślne, wydaje się jak marnowanie zasobów.

Oto zestaw testów (z wykorzystaniem nosetest generatory) do oczekiwanego zachowania:

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 

Odpowiedz

8

zależności od domeny następujące rozwiązanie może działać:

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 

Innym rozwiązaniem byłoby małpa plastra parsera klasa (jest to bardzo hackasz, więc nie polecam jej, jeśli masz inne opcje):

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 

Można go używać w następujący sposób:

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

Zaznaczając której członkowie dostępny w wyniku (ddd) można określić po powrocie None. Kiedy wszystkie pola dostępne ddd można przekonwertować do obiektu 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

To rozwiązuje tylko pusty przypadek łańcucha. Kiedy mam częściową datę, nadal domyślnie nie określone pola, ale dostaje inną ostateczną datę niż domyślna. Dodałem kilka testów jednostkowych do pytania, aby zilustrować wymagania i gdzie ten przykład zawodzi. Dzięki za spojrzenie! –

+1

Uważaj, najwyraźniej w swoim pierwszym przykładzie porównujesz obiekt daty z obiektem datetime. Zawsze będzie nierówne. –

0

wpadłem na dokładnie taki sam problem z dateutil pisałem tę funkcję i doszliśmy do wniosku, że po to jest dla dobra potomności. Zasadniczo Wykorzystując bazę _parse metodę jak @ILYA Khlopotov proponuje:

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 

Zwracany obiekt nie jest instancją datetime, ale ma .year, .month, a .day atrybuty, które było wystarczająco dobre dla moich potrzeb. Przypuszczam, że można go łatwo przekonwertować na instancję datetime.

0

prosta-data robi to za Ciebie (testuje wiele formatów wewnętrznych, ale nie aż tak dużo, jak mogłoby się wydawać, ponieważ używane przez niego wzorce rozszerzają wzory datowania Pythona o opcjonalne części, takie jak wyrazy regularne).

- patrz https://github.com/andrewcooke/simple-date - ale tylko python 3.2 i nowsze (przepraszam).

to łagodniejsze niż to, co chcesz domyślnie:

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

ale można określić własny format.Np

>>> 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 invert() prostu przełącza obecność % które w przeciwnym razie staje się prawdziwą bałagan, określając złożone wzory daty. więc tutaj tylko dosłowne T postać potrzebuje % prefiks (w standardowym terminie Pythona formatowania byłoby jedynym alfanumeryczny znak bez prefiksu)

3

Prawdopodobnie jest to „hack”, ale wygląda na to dateutil wygląda na bardzo kilka atrybutów poza domyślną wartością. Możesz podać "fałszywą" datetime, która eksploduje w pożądany sposób.

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

Miły, czysty hack, nawet jeśli jest to hack! +1 – tzaman

+0

Również czytając kwargs funkcji 'replace' mogę sprawdzić, które elementy daty zostały określone w przekazanym ciągu. Tylko rok lub rok w miesiącu itp. Dokładnie to, czego potrzebowałem. – Winand

+0

To wyglądało dobrze, ale nie działało dla mnie obecnie. Zmodyfikowałem taką funkcję i wydaje się, że to naprawię: def def_parse (v): try: _actual = ... except AttributeError: _actual = None' – user2205380

Powiązane problemy