提问人:pietz 提问时间:5/29/2023 更新时间:6/1/2023 访问量:300
在 Python 中检测日期时间字符串的格式
Detect the format of a datetime string in Python
问:
我正在寻找一种方法来检测 Python 中日期时间字符串的 -style 格式。我发现的所有日期时间库都具有解析字符串以创建日期时间对象的功能,但我想检测可与 format 参数一起使用的格式或模式。strftime
datetime.strptime
为什么?我正在处理日期时间字符串的长列表(或系列),并且用于解析它们太不准确且太慢。dateutil.parser
- 慢:它每次都会检查所有可能的格式,尽管每个列表的所有条目都具有相同的格式(在我的情况下)。
- 不准确:不明确的条目将以多种方式中的一种进行解析,而不会从其他不明确的条目中汲取知识。
因此,我想检测格式。一旦我有了它,我就可以使用该函数以更快的方式创建日期时间序列。to_datetime
polars
我在更现代的日期时间库(如钟摆)中找不到这样的功能。我还实现了自己的版本,该版本遍历了固定的格式列表,并检查是否可以使用以下方法读取: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
这对我来说不是一个优雅的解决方案,我想知道是否有更好的方法可以做到这一点,或者甚至有一个可用的库来做这种事情。
答:
将您的任务外包给 pandas 以使用其新的日期解析怎么样?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
]
评论
format='mixed'
我没有看到另一种仅使用 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]
评论
总之,您会看到各种 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))
)
评论
感谢所有做出贡献的人。我自己下了兔子洞,才发现它确实是一个很深的兔子洞。
第一种方法是基于我最初的想法,该想法由 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
然后我洗了个澡,因为我觉得写这段代码很脏,并且还注意到这不是一个非常快的功能。对在列上使用不同模式的条目执行此操作需要一些时间。strptime
n
m
k
下一站是正则表达式方法,它要快得多,让我对自己感觉好一点。这比尝试不同的模式快大约 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
评论