有没有一种简单的方法将ISO 8601持续时间转换为timedelta?

54

1
“datetime.timedelta格式的字符串”没有意义,我想你是指一个datetime.timedelta对象。标准库没有解析时间差的功能,但是在PyPI上有一些包可以通过pip install安装。如果你想知道如何自己做,我认为这个问题太广泛了,不适合在SO上提问;你应该自己尝试一下,看看在哪里(如果有的话)卡住了无法继续。 - jonrsharpe
是的,你说得对。我想将它转换为timedelta对象。我可以编写自己的解析器,但我正在寻找是否有简单的解决方案。 - Alkindus
14
我不明白为什么这个问题被关闭为“过于宽泛”。它非常专注。 - gerrit
datetime.timedelta 是什么?它是一个包的一部分吗?你能举个具体的例子说明你想要的和你得到的有什么不同吗? - SherylHohman
7个回答

69

我发现isodate库完全符合我的要求

isodate.parse_duration('PT1H5M26S')
  • 您可以在此处阅读该函数的源代码

8

如果你正在使用Pandas,你可以使用pandas.Timedelta。该构造函数接受一个ISO 8601字符串,同时pandas.Timedelta.isoformat可以将实例格式化回字符串:

>>> import pandas as pd
>>> dt = pd.Timedelta("PT1H5M26S")
>>> dt
Timedelta('0 days 01:05:26')
>>> dt.isoformat()
'P0DT1H5M26S'

需要pandas >= 1.2.0才能解析ISO 8601持续时间字符串,请参阅pandas发行说明 - hertzsprung
Pandas 1.2已经发布超过2年了,所以我希望每个人都已经升级了。 (尽管我痛苦地意识到大多数数据科学家仍在使用Python 2.7...) - Joren

4
这里提供一种无需使用新包的解决方案,但仅适用于最大持续时间以天为单位的情况。不过这个限制是有道理的,因为正如其他人指出的那样:1,如果timedelta持续时间超过“一个月”的天数,你将如何使用ISO8601持续时间符号表示它,而无需引用特定的时间点?相反,给定你的例子“P3Y6M4DT12H30M5S”,如果不知道这段时间涉及哪些确切的年份和月份,你将如何将其转换为timedelta对象?Timedelta对象是非常精确的实体,这几乎肯定是它们在构造函数中不支持“年”和“月”参数的原因。
import datetime


def get_isosplit(s, split):
    if split in s:
        n, s = s.split(split)
    else:
        n = 0
    return n, s


def parse_isoduration(s):
        
    # Remove prefix
    s = s.split('P')[-1]
    
    # Step through letter dividers
    days, s = get_isosplit(s, 'D')
    _, s = get_isosplit(s, 'T')
    hours, s = get_isosplit(s, 'H')
    minutes, s = get_isosplit(s, 'M')
    seconds, s = get_isosplit(s, 'S')

    # Convert all to seconds
    dt = datetime.timedelta(days=int(days), hours=int(hours), minutes=int(minutes), seconds=int(seconds))
    return int(dt.total_seconds())

> parse_isoduration("PT1H5M26S")
3926

请注意,这些值不一定是整数(例如,用于Web上MP4的DASH媒体流的MPD清单),这假设它们都是整数并强制转换为整数。 - Louis Maddox

2
很好的问题,显然“正确”的解决方案取决于您对输入的期望(更可靠的数据源不需要太多的输入验证)。
我的方法是解析ISO8601持续时间戳,只检查“PT”前缀是否存在,并且不会假定任何单位的整数值:
from datetime import timedelta

def parse_isoduration(isostring, as_dict=False):
    """
    Parse the ISO8601 duration string as hours, minutes, seconds
    """
    separators = {
        "PT": None,
        "W": "weeks",
        "D": "days",
        "H": "hours",
        "M": "minutes",
        "S": "seconds",
    }
    duration_vals = {}
    for sep, unit in separators.items():
        partitioned = isostring.partition(sep)
        if partitioned[1] == sep:
            # Matched this unit
            isostring = partitioned[2]
            if sep == "PT":
                continue # Successful prefix match
            dur_str = partitioned[0]
            dur_val = float(dur_str) if "." in dur_str else int(dur_str)
            duration_vals.update({unit: dur_val})
        else:
            if sep == "PT":
                raise ValueError("Missing PT prefix")
            else:
                # No match for this unit: it's absent
                duration_vals.update({unit: 0})
    if as_dict:
        return duration_vals
    else:
        return tuple(duration_vals.values())

dur_isostr = "PT3H2M59.989333S"
dur_tuple = parse_isoduration(dur_isostr)
dur_dict = parse_isoduration(dur_isostr, as_dict=True)
td = timedelta(**dur_dict)
s = td.total_seconds()

>>> dur_tuple
(0, 0, 3, 2, 59.989333)
>>> dur_dict
{'weeks': 0, 'days': 0, 'hours': 3, 'minutes': 2, 'seconds': 59.989333}
>>> td
datetime.timedelta(seconds=10979, microseconds=989333)
>>> s
10979.989333

1
基于@r3robertson的更完整但不完美版本。
def parse_isoduration(s):
""" Parse a str ISO-8601 Duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
Originally copied from:
https://dev59.com/eVoU5IYBdhLWcg3w5p2a
:param s:
:return:
"""

# ToDo [40]: Can't handle legal ISO3106 ""PT1M""

def get_isosplit(s, split):
    if split in s:
        n, s = s.split(split, 1)
    else:
        n = '0'
    return n.replace(',', '.'), s  # to handle like "P0,5Y"

s = s.split('P', 1)[-1]  # Remove prefix
s_yr, s = get_isosplit(s, 'Y')  # Step through letter dividers
s_mo, s = get_isosplit(s, 'M')
s_dy, s = get_isosplit(s, 'D')
_, s = get_isosplit(s, 'T')
s_hr, s = get_isosplit(s, 'H')
s_mi, s = get_isosplit(s, 'M')
s_sc, s = get_isosplit(s, 'S')
n_yr = float(s_yr) * 365  # These are approximations that I can live with
n_mo = float(s_mo) * 30.4  # But they are not correct!
dt = datetime.timedelta(days=n_yr+n_mo+float(s_dy), hours=float(s_hr), minutes=float(s_mi), seconds=float(s_sc))
return dt  # int(dt.total_seconds())  # original code wanted to return as seconds, we don't.

0
这是我对代码进行的修改(Martin,rer answers),以支持“weeks”属性并返回毫秒。一些时间段可能使用“PT15.460S”小数。
def parse_isoduration(str):
## https://dev59.com/eVoU5IYBdhLWcg3w5p2a
## Parse the ISO8601 duration as years,months,weeks,days, hours,minutes,seconds
## Returns: milliseconds
## Examples: "PT1H30M15.460S", "P5DT4M", "P2WT3H"
    def get_isosplit(str, split):
        if split in str:
            n, str = str.split(split, 1)
        else:
            n = '0'
        return n.replace(',', '.'), str  # to handle like "P0,5Y"

    str = str.split('P', 1)[-1]  # Remove prefix
    s_yr, str = get_isosplit(str, 'Y')  # Step through letter dividers
    s_mo, str = get_isosplit(str, 'M')
    s_wk, str = get_isosplit(str, 'W')
    s_dy, str = get_isosplit(str, 'D')
    _, str    = get_isosplit(str, 'T')
    s_hr, str = get_isosplit(str, 'H')
    s_mi, str = get_isosplit(str, 'M')
    s_sc, str = get_isosplit(str, 'S')
    n_yr = float(s_yr) * 365   # approx days for year, month, week
    n_mo = float(s_mo) * 30.4
    n_wk = float(s_wk) * 7
    dt = datetime.timedelta(days=n_yr+n_mo+n_wk+float(s_dy), hours=float(s_hr), minutes=float(s_mi), seconds=float(s_sc))
    return int(dt.total_seconds()*1000) ## int(dt.total_seconds()) | dt

0
你可以简单地使用正则表达式来解析ISO 8601时间持续,而不需要引入外部依赖。
以下代码适用于常见的D/H/M/S标识符。但不支持Y/M/W标识符。
import datetime
import re


def parse_iso8601_duration(duration: str) -> datetime.timedelta:    
    pattern = r"^P(?:(?P<days>\d+\.\d+|\d*?)D)?T?(?:(?P<hours>\d+\.\d+|\d*?)H)?(?:(?P<minutes>\d+\.\d+|\d*?)M)?(?:(?P<seconds>\d+\.\d+|\d*?)S)?$"
    match = re.match(pattern, duration)
    if not match:
        raise ValueError(f"Invalid ISO 8601 duration: {duration}")
    parts = {k: float(v) for k, v in match.groupdict("0").items()}
    return datetime.timedelta(**parts)

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