Подтвердить что ты не робот

Разбор даты в python без использования значения по умолчанию

Я использую инструмент python dateutil.parser для анализа некоторых дат, которые я получаю от стороннего канала. Он позволяет указать дату по умолчанию, которая по умолчанию используется по умолчанию, для заполнения отсутствующих элементов разбора даты. Хотя это вообще полезно, для моего варианта использования нет нормального значения по умолчанию, и я предпочел бы обрабатывать частичные даты, как если бы я вообще не получил дату (так как это почти всегда означает, что я получил искаженные данные). Я написал следующую работу:

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

(Этот фрагмент относится только к дате, поскольку все это касается моего приложения, но аналогичная логика может быть расширена, чтобы включить компонент времени.)

Мне интересно (надеется), что это лучший способ сделать это. Разбор одной и той же строки дважды для того, чтобы увидеть, заполняет ли она различные значения по умолчанию, по-видимому, представляет собой грубую трату ресурсов, по меньшей мере.

Здесь набор тестов (с использованием генераторов nosetest) для ожидаемого поведения:

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

Ответ 1

В зависимости от вашего домена следующее решение может работать:

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

Другим подходом будет класс parser для патчей (это очень hackiesh, поэтому я бы не рекомендовал его, если у вас есть другие параметры):

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

Вы можете использовать его следующим образом:

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

Проверяя, какие члены доступны в результате (ddd), вы можете определить, когда возвращается None. Когда доступны все поля, вы можете преобразовать ddd в объект datetime:

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

Ответ 2

Это, вероятно, "взломать", но похоже, что dateutil смотрит на очень мало атрибутов из по умолчанию, в котором вы проходите. Вы могли бы предоставить "фальшивое" datetime, которое взрывается желаемым способом.

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

Ответ 3

Я столкнулся с той же проблемой с dateutil, я написал эту функцию и решил, что я опубликую ее для потомков. В основном, используя базовый метод _parse, такой как @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

Возвращаемый объект не является экземпляром datetime, но имеет атрибуты .year, .month и .day, что было достаточно для моих нужд. Я полагаю, вы можете легко преобразовать его в экземпляр datetime.

Ответ 4

simple-date делает это для вас (он пытается использовать несколько форматов, внутренне, но не так много, как вы могли бы подумать, потому что используемые им шаблоны расширяют шаблоны дат питона с необязательными частями, такими как регулярные выражения).

см. https://github.com/andrewcooke/simple-date - но только python 3.2 и выше (извините).

он более снисходителен, чем вы хотите по умолчанию:

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

но вы можете указать свой собственный формат. например:

>>> 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() просто переключает присутствие %, которое в противном случае становится реальным беспорядком при указании сложных шаблонов дат. поэтому здесь только буквальный символ T нуждается в префиксе % (при стандартном форматировании даты на python он будет единственным буквенно-цифровым символом без префикса)