在Python中解析日期而不使用默认值

17 投票
4 回答
10825 浏览
提问于 2025-04-17 07:59

我正在使用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

4 个回答

0

simple-date这个工具可以帮你处理日期格式的问题(它会尝试多种格式,但没有你想象的那么多,因为它使用的模式是在Python的日期模式基础上加了一些可选部分,就像正则表达式一样)。

你可以查看这个链接: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

顺便说一下,invert()这个函数只是用来切换%的存在与否,因为在指定复杂的日期模式时,如果不处理好会变得非常麻烦。所以在这里,只有字面上的T字符需要加%前缀(在标准的Python日期格式中,它是唯一一个没有前缀的字母数字字符)。

3

这可能算是一种“技巧”,但看起来dateutil只关注你传入的默认值中的少数几个属性。你可以提供一个“假”的日期时间,这样它就会按照你想要的方式显示。

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

根据你的具体情况,下面这个解决方案可能会有效:

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

另一种方法是对解析器类进行“猴子补丁”(这种做法有点黑科技,所以如果你有其他选择,我不太推荐这样做):

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 转换成日期时间对象:

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

撰写回答