将时间差格式化为字符串

437

我在格式化一个datetime.timedelta对象时遇到了问题。

这是我的尝试: 我有一个对象列表,对象的类成员之一是一个timedelta对象,它显示事件的持续时间。我想以小时:分钟的格式显示该持续时间。

我尝试了各种方法,但都遇到了困难。我目前的方法是为我的对象添加返回小时和分钟的方法。我可以通过将timedelta.seconds除以3600并四舍五入来得到小时数。我在获取剩余秒数和将其转换为分钟方面遇到了麻烦。

顺便说一下,我正在使用Google AppEngine和Django模板进行演示。


118
如果timedelta有一个类似于strftime()方法的等价物就好了。 - JS.
@JS。如果您使用datetime.utcfromtimestamp(),您就可以在一定程度上实现。请参见下面的我的回答 - sjngm
@JS. - 百分之百同意。然后,timedelta__str__相当不错,与__repr__(即针对人类!)相反。例如:datetime.timedelta(minutes=6, seconds=41) * 2618 / 48会得到datetime.timedelta(seconds=21871, microseconds=208333),但是str(datetime.timedelta(minutes=6, seconds=41) * 2618 / 48)会得到'6:04:31.208333',这相当容易阅读。 - Tomasz Gandor
@JS. 在Python3中,datetime模块在文件/usr/lib/python3.7/datetime.py中以纯Python实现。在该文件的末尾,通过“import from _datetime”将纯Python实现覆盖为已编译的实现。但是,如果您注释掉“import”,则该模块将正常工作,并且您可以直接在该文件中添加datetime.timedelta.__format__方法或通过Monkey Patching添加。 - NameOfTheRose
很棒的问题! - Sunghee Yun
1
当然,注释掉 import(https://dev59.com/6HRB5IYBdhLWcg3wuZQ1#WqMtoYgBc1ULPQZFDMWQ)就像我自己建议的那样,会有一些影响:性能下降(strptime 慢了 2 倍),不兼容性问题出现(时区模块崩溃)。 - NameOfTheRose
35个回答

300

你可以使用 str() 方法将 timedelta 转换为字符串。以下是一个例子:

import datetime
start = datetime.datetime(2009,2,10,14,00)
end   = datetime.datetime(2009,2,10,16,00)
delta = end-start
print(str(delta))
# prints 2:00:00

14
更像是使用timedelta作为参数调用str()方法。 - joeforker
16
不需要使用str(),print会自动完成转换。 - Zitrax
5
如果您要将delta与另一个字符串连接起来,那么这是必要的。例如:print 'something ' + str(delta) - Plinio.Santos
19
当数据类型被定义为'datetime.timedelta'且使用ConvertDuration=datetime.timedelta(milliseconds=int(254459))时,如果需要获取毫秒数,则可以使用分割操作将其分离出来。例如,要从时间戳0:03:43.765000中获取0:03:43,只需运行TotalDuration=str(ConvertDuration).split('.', 2)[0]即可。 - DarkXDroid
40
这可能会将增量打印为字符串,但它并未按照原帖作者的要求进行格式化。 - Dannid
显示剩余4条评论

264

正如你所知,通过访问.seconds属性,可以从时间差对象中获取总秒数。

Python提供了内置函数divmod(),允许进行:

s = 13420
hours, remainder = divmod(s, 3600)
minutes, seconds = divmod(remainder, 60)
print('{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)))
# result: 03:43:40

或者你可以通过使用取模和减法的组合将其转换为小时和余数:

# arbitrary number of seconds
s = 13420
# hours
hours = s // 3600 
# remaining seconds
s = s - (hours * 3600)
# minutes
minutes = s // 60
# remaining seconds
seconds = s - (minutes * 60)
# total time
print('{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)))
# result: 03:43:40

7
对于负的时间差,你应该先确定符号,然后执行 abs(s) - mbarkhau
30
请注意,您可能实际上希望使用 '%d:%02d:%02d' 来在输出字符串中添加前导零。 - ShinNoNoir
13
对于 Python 2.7 及以上版本,请使用 .total_seconds() 方法。 - sk8asd123
43
如果差值可能为负数或超过24小时,请勿使用.seconds(该属性忽略.days)。请改用.total_seconds()或其类似方法。 - jfs
为了获得积极的差异,我会不时地重新实现它。感谢您只需要这一次搜索 :) - Wolf
我稍微编辑了一下,John,请随意恢复到以前的状态,如果你觉得那样更好。 - Ciro Santilli OurBigBook.com

84
>>> str(datetime.timedelta(hours=10.56))
10:33:36

>>> td = datetime.timedelta(hours=10.505) # any timedelta object
>>> ':'.join(str(td).split(':')[:2])
10:30
timedelta 对象传递给 str() 函数,将调用与直接键入 print td 使用的相同格式化代码。由于不需要秒数,我们可以通过冒号(3部分)拆分字符串,并仅使用前两部分重新组合。

1
无论你从哪里获取timedelta对象,它的格式都是相同的。 - joeforker
14
如果时间长度超过一天,在分割和合并处理后,它将被格式化为例如“4 天,8:00”。 - joeforker
6
对于负数,str(my_timedelta) 的表现不佳。 - Catskul
3
当超过24小时时,显示天数,例如“4天18:48:22.330000”。许多建议的方法在这里无法实现。 - Alex Martian
@joeforker 很棒的解决方案。谢谢! - Sunghee Yun
显示剩余2条评论

64
def td_format(td_object):
    seconds = int(td_object.total_seconds())
    periods = [
        ('year',        60*60*24*365),
        ('month',       60*60*24*30),
        ('day',         60*60*24),
        ('hour',        60*60),
        ('minute',      60),
        ('second',      1)
    ]

    strings=[]
    for period_name, period_seconds in periods:
        if seconds > period_seconds:
            period_value , seconds = divmod(seconds, period_seconds)
            has_s = 's' if period_value > 1 else ''
            strings.append("%s %s%s" % (period_value, period_name, has_s))

    return ", ".join(strings)

9
很好,我建议将 if seconds > period_seconds: 更改为 if seconds >= period_seconds: - CBenni
1
这会返回空字符串以表示负时间差,不确定这是否是有意的? - Dirk
2
我尝试了5473天,得到的结果是:“14年12个月3天”。为什么不是“15年3天”呢?此外,根据谷歌,5473是“14.99452个日历年”,“0.99451999988374个日历年是11.93422692000003593个月”,“0.93422692000003593193个月是28.416099957565091216天。因此,正确答案不应该是:“14年11个月28天”吗? - Bruno Ambrozio

63

我个人使用humanize库来实现此功能:

>>> import datetime
>>> humanize.naturalday(datetime.datetime.now())
'today'
>>> humanize.naturalday(datetime.datetime.now() - datetime.timedelta(days=1))
'yesterday'
>>> humanize.naturalday(datetime.date(2007, 6, 5))
'Jun 05'
>>> humanize.naturaldate(datetime.date(2007, 6, 5))
'Jun 05 2007'
>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=1))
'a second ago'
>>> humanize.naturaltime(datetime.datetime.now() - datetime.timedelta(seconds=3600))
'an hour ago'

当然,humanize并不能给出你想要的确切答案(实际上,那就是 str(timeA - timeB)),但我发现一旦时间超过几个小时,结果会很难读。 humanize 支持更大的人类可读值,并且本地化也很好。

它启发自 Django 的 contrib.humanize 模块,所以如果你在使用 Django,最好直接使用它。


3
好的,+1。但是:humanize.naturaldelta(pd.Timedelta('-10秒'))-->'10秒' :S... - ntg
6
好吧,这确实是一个10秒的时间差。 :) 如果你想要时间,naturaltime就是你想要使用的。 - anarcat
这是一个有用的模块,但结果有些奇怪,diff 变成了“x 分钟前” :) 应该只是“x 分钟 <某些东西>”。 - ati ince

41

这里有一个通用的函数,可以将一个 timedelta 对象或常规数字(以秒或分钟等形式)转换为格式化良好的字符串。 我使用mpounsett的绝妙答案对重复问题进行了改进,使其更具灵活性,易读性和文档性。

您会发现它是迄今为止最灵活的答案,因为它允许您:

  1. 即时自定义字符串格式,而不是硬编码它。
  2. 轻松省略某些时间间隔(请参见下面的示例)。

函数:

from string import Formatter
from datetime import timedelta

def strfdelta(tdelta, fmt='{D:02}d {H:02}h {M:02}m {S:02}s', inputtype='timedelta'):
    """Convert a datetime.timedelta object or a regular number to a custom-
    formatted string, just like the stftime() method does for datetime.datetime
    objects.

    The fmt argument allows custom formatting to be specified.  Fields can 
    include seconds, minutes, hours, days, and weeks.  Each field is optional.

    Some examples:
        '{D:02}d {H:02}h {M:02}m {S:02}s' --> '05d 08h 04m 02s' (default)
        '{W}w {D}d {H}:{M:02}:{S:02}'     --> '4w 5d 8:04:02'
        '{D:2}d {H:2}:{M:02}:{S:02}'      --> ' 5d  8:04:02'
        '{H}h {S}s'                       --> '72h 800s'

    The inputtype argument allows tdelta to be a regular number instead of the  
    default, which is a datetime.timedelta object.  Valid inputtype strings: 
        's', 'seconds', 
        'm', 'minutes', 
        'h', 'hours', 
        'd', 'days', 
        'w', 'weeks'
    """

    # Convert tdelta to integer seconds.
    if inputtype == 'timedelta':
        remainder = int(tdelta.total_seconds())
    elif inputtype in ['s', 'seconds']:
        remainder = int(tdelta)
    elif inputtype in ['m', 'minutes']:
        remainder = int(tdelta)*60
    elif inputtype in ['h', 'hours']:
        remainder = int(tdelta)*3600
    elif inputtype in ['d', 'days']:
        remainder = int(tdelta)*86400
    elif inputtype in ['w', 'weeks']:
        remainder = int(tdelta)*604800

    f = Formatter()
    desired_fields = [field_tuple[1] for field_tuple in f.parse(fmt)]
    possible_fields = ('W', 'D', 'H', 'M', 'S')
    constants = {'W': 604800, 'D': 86400, 'H': 3600, 'M': 60, 'S': 1}
    values = {}
    for field in possible_fields:
        if field in desired_fields and field in constants:
            values[field], remainder = divmod(remainder, constants[field])
    return f.format(fmt, **values)

演示:

>>> td = timedelta(days=2, hours=3, minutes=5, seconds=8, microseconds=340)

>>> print strfdelta(td)
02d 03h 05m 08s

>>> print strfdelta(td, '{D}d {H}:{M:02}:{S:02}')
2d 3:05:08

>>> print strfdelta(td, '{D:2}d {H:2}:{M:02}:{S:02}')
 2d  3:05:08

>>> print strfdelta(td, '{H}h {S}s')
51h 308s

>>> print strfdelta(12304, inputtype='s')
00d 03h 25m 04s

>>> print strfdelta(620, '{H}:{M:02}', 'm')
10:20

>>> print strfdelta(49, '{D}d {H}h', 'h')
2d 1h

1
非常好的、干净的代码!我想知道如何扩展这段代码以处理格式化器中的最后一小部分余数秒... - kfmfe04
1
@kfmfe04,我修改了这个解决方案,包括一秒钟的分数部分。https://dev59.com/6HRB5IYBdhLWcg3wuZQ1#63198084 - tomatoeshift

36
他已经有一个时间差(timedelta)对象,为什么不使用它内置的 total_seconds() 方法将其转换为秒,然后使用 divmod() 函数获取小时数和分钟数呢?

他已经有一个时间差(timedelta)对象,为什么不使用它内置的total_seconds()方法将其转换为秒,然后使用divmod()函数获取小时数和分钟数呢?

hours, remainder = divmod(myTimeDelta.total_seconds(), 3600)
minutes, seconds = divmod(remainder, 60)

# Formatted only for hours and minutes as requested
print '%s:%s' % (hours, minutes)
无论时间差是几天还是几年,此方法都有效。

秉承共享的精神,我采用了这个答案,并使用它创建了一个更紧凑的字符串格式,适用于过去和未来:https://gist.github.com/davidmankin/872388157e562dda8b90f631cc33657d - DaveMan
尝试使用只有一位数字分钟的时间进行此操作。 - Konrad Rudolph

30

我知道这是一个旧的已回答问题,但我使用datetime.utcfromtimestamp()来实现。它接受秒数并返回一个可以像任何其他datetime一样格式化的datetime对象。

duration = datetime.utcfromtimestamp((end - begin).total_seconds())
print(duration.strftime('%H:%M'))

只要您在时间部分保持合法范围,这个程序就能正常工作,例如不会返回1234:35,因为小时数应该是小于等于23的。

3
你应该使用 print timedelta(seconds=end - begin) - jfs
@J.F.Sebastian 那你需要手动在小时数前面添加前导零。 - sjngm
我在问题中没有看到关于填充的内容。如果需要,可以使用.zfill(8) - jfs
3
需要调用 .total_seconds() 方法:>>> datetime.utcfromtimestamp((t2-t1).total_seconds()).strftime("%H:%M:%S") <<<>>> '00:00:05' - cagney
3
我更喜欢这个解决方案,它让你可以控制格式。注意,你也可以直接使用字符串格式化器,例如:"{0:%H}:{0:%M}".format(duration)。 - toes
5
如果你有一个 timedelta 对象,你可以直接使用它:datetime.utcfromtimestamp(delta.total_seconds()) - squaregoldfish

20

我会认真考虑奥卡姆剃刀原理的方法:

td = str(timedelta).split('.')[0]

这将返回一个没有微秒的字符串。

如果你想重新生成datetime.timedelta对象,只需要这样做:

h,m,s = re.split(':', td)
new_delta = datetime.timedelta(hours=int(h),minutes=int(m),seconds=int(s))

两年过去了,我爱上了这门语言!


6
不包含这些天。 - Waschbaer

18

我使用了 humanfriendly Python 库来完成这个任务,它运行得非常好。

import humanfriendly
from datetime import timedelta
delta = timedelta(seconds = 321)
humanfriendly.format_timespan(delta)

'5 minutes and 21 seconds'

可在https://pypi.org/project/humanfriendly/获得。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接