在 Python 中检测日期时间字符串的格式

Detect the format of a datetime string in Python

提问人:pietz 提问时间:5/29/2023 更新时间:6/1/2023 访问量:300

问:

我正在寻找一种方法来检测 Python 中日期时间字符串的 -style 格式。我发现的所有日期时间库都具有解析字符串以创建日期时间对象的功能,但我想检测可与 format 参数一起使用的格式或模式。strftimedatetime.strptime

为什么?我正在处理日期时间字符串的长列表(或系列),并且用于解析它们太不准确且太慢。dateutil.parser

  • :它每次都会检查所有可能的格式,尽管每个列表的所有条目都具有相同的格式(在我的情况下)。
  • 不准确:不明确的条目将以多种方式中的一种进行解析,而不会从其他不明确的条目中汲取知识。

因此,我想检测格式。一旦我有了它,我就可以使用该函数以更快的方式创建日期时间序列。to_datetimepolars

我在更现代的日期时间库(如钟摆)中找不到这样的功能。我还实现了自己的版本,该版本遍历了固定的格式列表,并检查是否可以使用以下方法读取:datetime.strptime

patterns = [
        "%Y.%m.%d %H:%M:%S",
        "%Y-%m-%d %H:%M",
        "%Y-%m-%d",
        ...
    ]

    for pattern in patterns:
        try:
            for val in col:
                assert datetime.datetime.strptime(val, pattern)
            return pattern
        except:
            continue

这对我来说不是一个优雅的解决方案,我想知道是否有更好的方法可以做到这一点,或者甚至有一个可用的库来做这种事情。

python datetime 解析 格式 python-polars pandas

评论

0赞 FObersteiner 5/29/2023
关于“不准确”;我认为无论您使用什么库/算法,都要记住有些情况是模棱两可的:例如,10-11-2012 是 10-11-2012 10 还是 11 月 10 日?由您来制定明确的规范。
0赞 pietz 5/30/2023
这正是我的观点。这些例子本身是模棱两可的,但由于我使用该函数来分析一长串值(所有值都具有相同的格式),因此所有值模棱两可的概率基本上为零。在某一时刻,如果值是 DMY 或 MDY,则会放弃。这就是我想利用的,而 dateutil 不能。

答:

2赞 Timeless 5/29/2023 #1

将您的任务外包给 以使用其新的日期解析怎么样mixed

dt_strs = [
    "Mon, 29 May 2023 13:15:09 +0000",  # Day, DD Month YYYY HH:MM:SS +0000
    "10/01/2020 12:15:33",              # MM/DD/YYYY HH:MM:SS
    "2020-08-01",                       # YYYY-MM-DD
    "08:55",                            # HH:MM
    "2019.09.18T18:51:57",              # YYYY.MM.DDTHH:MM:SS
    "11:29:10",                         # HH:MM:SS
    "23/05/2022 03:30:00 +0500",        # DD/MM/YYYY HH:MM:SS +0000
    "02.28.19",                         # MM.DD.YY
    "2023-01-01 22:23",                 # YYYY-MM-DD HH:MM
    "31 jul, 2022",                     # DD Month, YYYY
    "2021/12/18 06:13:08",              # YYYY/MM/DD HH:MM:SS
    "2023",                             # YYYY
]

pl_ser = pl.from_pandas(pd.to_datetime(dt_strs, format="mixed").to_series(name="dts"))

输出:

print(pl_ser)

shape: (12,)
Series: 'dts' [datetime[μs, UTC]]
[
    2023-05-29 13:15:09 UTC
    2020-10-01 12:15:33 UTC
    2020-08-01 00:00:00 UTC
    2023-05-29 08:55:00 UTC
    2019-09-18 18:51:57 UTC
    2023-05-29 11:29:10 UTC
    2022-05-22 22:30:00 UTC
    2019-02-28 00:00:00 UTC
    2023-01-01 22:23:00 UTC
    2022-07-31 00:00:00 UTC
    2021-12-18 06:13:08 UTC
    2023-01-01 00:00:00 UTC
]

评论

0赞 Timeless 5/29/2023
注意pd.to_datetime返回的索引的每个元素都是一个对象datetime.datetime
0赞 Corralien 5/29/2023
我删除了 Pandas 的答案部分。你的更完整format='mixed'
0赞 Timeless 5/29/2023
谢谢 Corralien,混合解析似乎可以完成这项工作;)
1赞 Corralien 5/29/2023
我更喜欢纯python;)的答案哈哈。
0赞 FObersteiner 5/29/2023
同样需要注意的是:“”混合“,以单独推断每个元素的格式。这是有风险的,你可能应该把它和第一天一起使用。(文档)
2赞 Corralien 5/29/2023 #2

我没有看到另一种仅使用 Python 将您的值转换为日期时间的方法。但是,我认为您应该使用另一个变量来记住最后一个成功的模式以提高速度。像这样:

import datetime

patterns = [
        "%Y-%m-%d %H:%M:%S",
        "%Y.%m.%d %H:%M:%S",
        "%Y-%m-%d",
]


def to_datetime(values, errors='raise'):

    last_success_pattern = patterns[0] 
    dti = []

    for value in values:
        try:
            dt = datetime.datetime.strptime(value, last_success_pattern)
        except ValueError:
            for pattern in [last_success_pattern] + patterns:
                try:
                    dt = datetime.datetime.strptime(value, pattern)
                except ValueError:
                    if errors == 'raise':
                        raise ValueError(f'Unknown format for "{value}"')
                    elif errors == 'coerce':
                        dt = None  # invalid value is set to None
                    elif errors == 'ignore':
                        dt = value  # invalid value returns the input
                else:
                    last_success_pattern = pattern
                    break

        dti.append(dt)
    return dti

if __name__ == '__main__':
    print(to_datetime(['2023.01.01 11:22:33', '2023.01.02 14:15:16', '2023-01-03', '2023-01-01T21:12:33.078196+00:00'], errors='coerce'))

输出:

>>> to_datetime(['2023.01.01 11:22:33', '2023.01.02 14:15:16', 
                 '2023-01-03', '2023-01-01T21:12:33.078196+00:00'],
                errors='coerce')

[datetime.datetime(2023, 1, 1, 11, 22, 33),
 datetime.datetime(2023, 1, 2, 14, 15, 16),
 datetime.datetime(2023, 1, 3, 0, 0),
 None]

评论

1赞 FObersteiner 5/29/2023
我更喜欢这种方法,因为它在某种意义上是确定性的;它留下了更少的歧义空间/对正在发生的事情有更多的控制权。但是,可能仍然存在解析成功但结果错误的情况,例如,第一天与月优先的情况。
0赞 pietz 6/1/2023
我找到了一种解决 strptime 的方法,它本身非常慢,并且也在此处发布了基于正则表达式的答案。谢谢。
0赞 Dean MacGregor 5/31/2023 #3

总之,您会看到各种 dfs,每个 dfs 都具有内部一致的日期时间格式,但每个 dfs 都与下一个不一致。如何处理?

你有:

 patterns = [
        "%Y.%m.%d %H:%M:%S",
        "%Y-%m-%d %H:%M",
        "%Y-%m-%d",
    ]

strptime 文档中,如果内部格式不一致,则可以使用 coalesce:

一个想法(可能不适合你)

(
    df
    .with_columns(Date=pl.coalesce(
        pl.col('Date').str.strptime(pl.Datetime(), x, strict=False) for x in patterns
        ))
)

如果您混合了多种格式,其中许多格式可以工作,但只有一种是正确的,这可能会有问题。

第二种选择(可能对你有好处)

for pattern in patterns:
    try:
        df=df.with_columns(Date=pl.col('Date').str.strptime(pl.Datetime(), pattern))
        break
    except:
        pass

第三种选择(可能对你有好处)

您也可以使用 pandas,而无需将整个 df 转换为 pandas。to_datetime

(
    df
        .with_columns(
            Date=pl.col('Date')
              .map(lambda x: pl.Series(pd.to_datetime(x.to_numpy(), format='mixed'))))
)

如果您只在 lambda 中定义辅助函数而不是像这样使用 lambda,您可以看起来更整洁一些:

def mixed_strptime(x):
    return pl.Series(pd.to_datetime(x.to_numpy(), format='mixed'))
(
    df
        .with_columns(
            Date=pl.col('Date').map(mixed_strptime))
)

评论

0赞 pietz 6/1/2023
谢谢你。选项 2 和 3 看起来不错,但对于我想要支持的模式数量来说,它们太慢了。我改用正则表达式方法。也张贴在这里。
0赞 pietz 6/1/2023 #4

感谢所有做出贡献的人。我自己下了兔子洞,才发现它确实是一个很深的兔子洞。

第一种方法是基于我最初的想法,该想法由 create_dt_patterns() 函数扩展,该函数生成广泛的模式组合。在这个列表超过 500 个模式之后,我对代码进行了一些调整,只生成适合字符串中分隔符的模式。我将其与计数器结合使用,以仅返回最常用的模式。这有助于产生模棱两可的结果。如果你对我可能丑陋和不完整的代码感兴趣,请点击这里:

def get_dt_format(col: list, patterns: list, n: int = 100):
    for val in col[:n]:
        for pattern in patterns.keys():
            try:
                _ = datetime.strptime(val, pattern)
                patterns[pattern] += 1
            except:
                pass

    if sum(patterns.values()) == 0:
        return False

    return max(patterns, key=patterns.get)

def create_dt_patterns(dt_str: str):
    dates = create_date_patterns(dt_str)
    times = create_time_patterns(dt_str)
    seps = create_dt_separators(dt_str)
    dts = [d + s + t for d in dates for s in seps for t in times]
    dts += dates + times
    return {x: 0 for x in dts}


def create_dt_separators(dt_str: str):
    seps = [x for x in ["T", " "] if x in dt_str]
    return [""] if len(seps) == 0 else seps


def create_time_patterns(dt_str: str):
    time_opts = ["%H{0}%M{0}%S.%f", "%H{0}%M{0}%S", "%H{0}%M"]
    return [t.format(":" if ":" in dt_str else "") for t in time_opts]


def create_date_patterns(dt_str: str):
    date_opts = [
        "%Y{0}%m{0}%d",
        "%m{0}%d{0}%Y",
        "%d{0}%m{0}%Y",
        "%y{0}%m{0}%d",
        "%m{0}%d{0}%y",
        "%d{0}%m{0}%y",
    ]
    dates = [d.format(s) for d in date_opts for s in ["-", "/", "."] if s in dt_str]
    if len(dates) == 0:
        dates = [d.format("") for d in date_opts]
    return dates

然后我洗了个澡,因为我觉得写这段代码很脏,并且还注意到这不是一个非常快的功能。对在列上使用不同模式的条目执行此操作需要一些时间。strptimenmk

下一站是正则表达式方法,它要快得多,让我对自己感觉好一点。这比尝试不同的模式快大约 30 倍。

由于我计划随着时间的推移测试和开发此逻辑,因此我为其创建了一个 GitHub 存储库。也在这里发布它,我可能不会更新:

DATE_RE = r"(?P<date>\d{2,4}[-/.]\d{2}[-/.]\d{2,4})?"
SEP_RE = r"(?P<sep>\s|T)?"
TIME_RE = r"(?P<time>\d{2}:\d{2}(:\d{2})?\s*([AP]M)?)?"
FULL_RE = DATE_RE + SEP_RE + TIME_RE
YMD_RE = r"^(?P<ay>(?:[12][0-9])?[0-9]{2})(?P<bs>[-/.])(?P<cm>0[1-9]|1[0-2])(?P<ds>[-/.])(?P<ed>0[1-9]|[12][0-9]|3[01])$"
DMY_RE = r"^(?P<ad>0[1-9]|[12][0-9]|3[01])(?P<bs>[-/.])(?P<cm>0[1-9]|1[0-2])(?P<ds>[-/.])(?P<ey>(?:[12][0-9])?[0-9]{2})$"
MDY_RE = r"^(?P<am>0[1-9]|1[0-2])(?P<bs>[-/.])(?P<cd>0[1-9]|[12][0-9]|3[01])(?P<ds>[-/.])(?P<ey>(?:[12][0-9])?[0-9]{2})$"
HMS_RE = r"^(?P<aH>\d{1,2})(?P<bs>:?)(?P<cM>\d{2})(?:(?P<ds>:?)(?P<eS>\d{2}))?(?:(?P<fs>\s)?(?P<ga>[AP]M))?$"


def guess_datetime_format(values: list[str], n=100, return_dict=False):
    di = {}
    for val in values[:n]:
        if val is None:
            continue
        fmts = datetime_formats(val)
        for fmt in fmts:
            if fmt not in di:
                di[fmt] = 1
            di[fmt] += 1

    if len(di) == 0:
        return None

    if return_dict:
        return di

    return max(di, key=di.get)


def datetime_formats(value: str) -> list:
    assert "," not in value  # TODO: handle these cases
    m = re.match(FULL_RE, value)
    dates = "" if m["date"] is None else date_formats(m["date"])
    sep = "" if m["sep"] is None else m["sep"]
    time = "" if m["time"] is None else time_format(m["time"])
    return [date + sep + time for date in dates]


def date_formats(date_value: str) -> list:
    matches = []
    for p in [YMD_RE, MDY_RE, DMY_RE]:
        m = re.match(p, date_value)
        if m is None:
            continue
        fmt = ""
        for c in sorted(m.groupdict().keys()):
            if c[1] == "s":  # separator character
                fmt += "" if m[c] is None else m[c]
            else:  # year, month, day
                fmt += "%" + c[1] if len(m[c]) == 2 else "%Y"
        matches.append(fmt)
    return matches


def time_format(time_value: str) -> str:
    m = re.match(HMS_RE, time_value)
    fmt = ""
    for c in sorted(m.groupdict().keys()):
        if c[1] == "s":  # separator character
            fmt += "" if m[c] is None else m[c]
        else:
            fmt += "" if m[c] is None else "%" + c[1]
    if "M" in time_value:  # AM or PM
        fmt = fmt.replace("%H", "%I")
    return fmt

评论

0赞 Corralien 6/1/2023
您是否比较了您的正则表达式解决方案和我的解决方案之间的性能,以及最后一个成功模式的缓存?
0赞 Dean MacGregor 6/1/2023
你能发布你的基准测试代码,使用 python 正则表达式而不是使用原生极坐标获得 30 倍的改进吗?
0赞 pietz 6/1/2023
明天会发布。需要明确的是,30x 是指我的两个解决方案之间的比较。是的,正则表达式的组合比尝试大量模式并尝试用每种模式解析一列要快。
0赞 pietz 6/1/2023
@DeanMacGregor我在存储库中添加了一个快速基准测试。在我的 macbook 上,我的方法比使用 2800 种模式的第二种替代方案快 348 倍。Corraliens 的回答给了我错误,所以我删除了它。正则表达式方法也比我的后续想法快 300 倍。我想当我提到 30 倍时,我错过了一个量级。