日期时间解析本不应让代码出错,但实际开发中却常常踩坑。这5个DIY Python函数可帮助你将现实中的杂乱日期时间数据,转化为干净可用的格式。
作者:巴拉·普里娅·C(Bala Priya C),KDnuggets特约编辑兼技术内容专家
发布时间:2026年1月26日
领域:Python

引言
日期时间解析看似简单,实际操作起来却处处是难点。Python的datetime模块能很好地处理标准格式,但现实中的数据往往杂乱无章——用户输入、网页爬取的数据、遗留系统输出,总能出现各种意想不到的格式,让人防不胜防。
本文将带你一步步实现5个实用函数,解决日期时间解析中的常见问题。读完本文,你将学会如何构建灵活的解析器,应对项目中遇到的各种杂乱日期格式。
1. 解析相对时间字符串
社交媒体应用、聊天软件和活动动态中,常会显示“5分钟前”“2天前”这类时间戳。当你爬取或处理这类数据时,需要将这些相对时间字符串转换为实际的datetime对象。
以下函数可处理常见的相对时间表达式:
from datetime import datetime, timedelta
import re
def parse_relative_time(time_string, reference_time=None):
"""
将相对时间字符串转换为datetime对象。
示例:"2 hours ago"(2小时前)、"3 days ago"(3天前)、"1 week ago"(1周前)
"""
if reference_time is None:
reference_time = datetime.now()
# 标准化字符串(统一转为小写、去除首尾空格)
time_string = time_string.lower().strip()
# 正则表达式:匹配“数字 + 时间单位 + ago”格式
pattern = r'(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago'
match = re.match(pattern, time_string)
if not match:
raise ValueError(f"无法解析该时间字符串: {time_string}")
amount = int(match.group(1)) # 提取数字部分
unit = match.group(2) # 提取时间单位
# 时间单位与timedelta参数的映射关系
unit_mapping = {
'second': 'seconds',
'minute': 'minutes',
'hour': 'hours',
'day': 'days',
'week': 'weeks',
}
if unit in unit_mapping:
# 对timedelta支持的单位,直接计算时间差
delta_kwargs = {unit_mapping[unit]: amount}
return reference_time - timedelta(**delta_kwargs)
elif unit == 'month':
# 月份近似处理:按每月30天计算
return reference_time - timedelta(days=amount * 30)
elif unit == 'year':
# 年份近似处理:按每年365天计算
return reference_time - timedelta(days=amount * 365)
该函数使用正则表达式提取字符串中的数字和时间单位:(\d+)捕获一个或多个数字,(second|minute|hour|day|week|month|year)匹配时间单位,s?让复数后缀“s”可选,因此“hour”和“hours”均可正常解析。
对于timedelta直接支持的单位(秒到周),我们创建对应的时间差对象,再从参考时间中减去该时间差;对于月份和年份,由于其天数不固定,我们分别按30天和365天进行近似处理——这种方式虽不绝对精确,但能满足大多数使用场景。
reference_time参数允许你指定自定义的“当前时间”,方便测试或处理历史数据时使用。
测试示例:
result1 = parse_relative_time("2 hours ago")
result2 = parse_relative_time("3 days ago")
result3 = parse_relative_time("1 week ago")
print(f"2 hours ago: {result1}")
print(f"3 days ago: {result2}")
print(f"1 week ago: {result3}")
输出结果:
2 hours ago: 2026-01-06 12:09:34.584107
3 days ago: 2026-01-03 14:09:34.584504
1 week ago: 2025-12-30 14:09:34.584558
2. 从自然语言文本中提取日期
有时你需要从文本中提取隐藏的日期,比如“会议定于2026年1月15日举行”或“请于3月3日前回复”。此时无需手动解析整个句子,只需提取其中的日期部分即可。
以下函数可从自然语言文本中查找并提取日期:
import re
from datetime import datetime
def extract_date_from_text(text, current_year=None):
"""
从自然语言文本中提取日期。
支持格式:
- "January 15th, 2024"(2024年1月15日)
- "March 3rd"(3月3日)
- "Dec 25th, 2023"(2023年12月25日)
"""
if current_year is None:
current_year = datetime.now().year
# 月份映射字典(支持完整名称和缩写)
months = {
'january': 1, 'jan': 1,
'february': 2, 'feb': 2,
'march': 3, 'mar': 3,
'april': 4, 'apr': 4,
'may': 5,
'june': 6, 'jun': 6,
'july': 7, 'jul': 7,
'august': 8, 'aug': 8,
'september': 9, 'sep': 9, 'sept': 9,
'october': 10, 'oct': 10,
'november': 11, 'nov': 11,
'december': 12, 'dec': 12
}
# 正则表达式:匹配“月份 日期(序数后缀),年份”(年份可选)
pattern = r'(january|jan|february|feb|march|mar|april|apr|may|june|jun|july|jul|august|aug|september|sep|sept|october|oct|november|nov|december|dec)\s+(\d{1,2})(?:st|nd|rd|th)?(?:,?\s+(\d{4}))?'
matches = re.findall(pattern, text.lower())
if not matches:
return None
# 取第一个匹配结果(若有多个日期,可根据需求调整)
month_str, day_str, year_str = matches[0]
month = months[month_str] # 将月份字符串转为数字
day = int(day_str) # 日期转为整数
year = int(year_str) if year_str else current_year # 无年份时默认当前年份
return datetime(year, month, day)
该函数首先创建一个月份映射字典,将月份的完整名称(如January)和缩写(如Jan)对应到对应的数字(如1)。正则表达式则用于匹配“月份+日期+可选年份”的格式,其中序数后缀(st、nd、rd、th)和年份均为可选。
正则表达式中的(?:...)表示非捕获组,即匹配该模式但不单独保存结果,适用于序数后缀、年份分隔符等可选部分,避免捕获无用的冗余信息。
当文本中未指定年份时,函数默认使用当前年份——这符合日常逻辑:比如在1月份提到“3月3日”,通常指当年的3月3日,而非上一年。
测试不同文本格式:
text1 = "The meeting is scheduled for January 15th, 2026 at 3pm"
text2 = "Please respond by March 3rd"
text3 = "Deadline: Dec 25th, 2026"
date1 = extract_date_from_text(text1)
date2 = extract_date_from_text(text2)
date3 = extract_date_from_text(text3)
print(f"From '{text1}': {date1}")
print(f"From '{text2}': {date2}")
print(f"From '{text3}': {date3}")
输出结果:
From 'The meeting is scheduled for January 15th, 2026 at 3pm': 2026-01-15 00:00:00
From 'Please respond by March 3rd': 2026-03-03 00:00:00
From 'Deadline: Dec 25th, 2026': 2026-12-25 00:00:00
3. 智能检测并解析多种灵活日期格式
现实中的日期数据格式繁多,为每种格式单独编写解析器十分繁琐。因此,我们构建一个可自动尝试多种格式的智能日期解析函数。
以下函数可处理多种常见日期格式:
from datetime import datetime
def parse_flexible_date(date_string):
"""
解析多种常见格式的日期。
依次尝试多种格式,返回第一个匹配成功的结果。
"""
date_string = date_string.strip()
# 常见日期格式列表(可根据需求扩展)
formats = [
'%Y-%m-%d', # ISO标准格式:2026-01-15
'%Y/%m/%d', # 斜杠分隔格式:2026/01/15
'%d-%m-%Y', # 日-月-年格式:15-01-2026
'%d/%m/%Y', # 日/月/年格式:15/01/2026
'%m/%d/%Y', # 月/日/年格式:01/15/2026
'%d.%m.%Y', # 点分隔格式:15.01.2026
'%Y%m%d', # 无分隔符格式:20260115
'%B %d, %Y', # 完整月份+日+年:January 15, 2026
'%b %d, %Y', # 月份缩写+日+年:Jan 15, 2026
'%d %B %Y', # 日+完整月份+年:15 January 2026
'%d %b %Y', # 日+月份缩写+年:15 Jan 2026
]
# 依次尝试每种格式,匹配成功则返回解析结果
for fmt in formats:
try:
return datetime.strptime(date_string, fmt)
except ValueError:
continue
# 所有格式均匹配失败时,抛出异常
raise ValueError(f"无法解析该日期: {date_string}")
该函数采用“暴力匹配”思路:依次尝试列表中的每种日期格式,使用strptime函数进行解析。若解析失败(抛出ValueError异常),则跳过当前格式,尝试下一种,直到匹配成功。
格式列表的顺序很重要:我们将ISO标准格式(%Y-%m-%d)放在首位,因为它在技术场景中最常用;像%d/%m/%Y和%m/%d/%Y这类易混淆的格式,放在后面避免误判。若你明确知道数据的主流格式,可调整列表顺序,优先匹配常用格式,提升解析效率。
测试多种日期格式:
# 测试不同格式的日期
dates = [
"2026-01-15",
"15/01/2026",
"01/15/2026",
"15.01.2026",
"20260115",
"January 15, 2026",
"15 Jan 2026"
]
for date_str in dates:
parsed = parse_flexible_date(date_str)
print(f"{date_str:20} -> {parsed}")
输出结果:
2026-01-15 -> 2026-01-15 00:00:00
15/01/2026 -> 2026-01-15 00:00:00
01/15/2026 -> 2026-01-15 00:00:00
15.01.2026 -> 2026-01-15 00:00:00
20260115 -> 2026-01-15 00:00:00
January 15, 2026 -> 2026-01-15 00:00:00
15 Jan 2026 -> 2026-01-15 00:00:00
这种方法虽不是最高效的,但胜在简单易懂,且能覆盖绝大多数实际开发中遇到的日期格式。
4. 解析时间时长
视频播放器、运动追踪器、时间管理应用中,常会显示“1h 30m”“2:45:30”这类时长格式。当解析用户输入或爬取的数据时,需要将这些时长转换为timedelta对象,以便进行后续的计算(如时长累加、差值对比)。
以下函数可解析多种常见的时长格式:
from datetime import timedelta
import re
def parse_duration(duration_string):
"""
将时长字符串解析为timedelta对象。
支持格式:
- "1h 30m 45s"(1小时30分钟45秒)
- "2:45:30"(时:分:秒)
- "90 minutes"(90分钟)
- "1.5 hours"(1.5小时)
"""
duration_string = duration_string.strip().lower()
# 先尝试解析冒号分隔格式(时:分:秒 或 分:秒)
if ':' in duration_string:
parts = duration_string.split(':')
if len(parts) == 2:
# 分:秒格式
minutes, seconds = map(int, parts)
return timedelta(minutes=minutes, seconds=seconds)
elif len(parts) == 3:
# 时:分:秒格式
hours, minutes, seconds = map(int, parts)
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
# 再尝试解析单位标识格式(如1h 30m 45s)
total_seconds = 0
# 匹配小时(支持整数和小数,如1h、1.5hours)
hours_match = re.search(r'(\d+(?:\.\d+)?)\s*h(?:ours?)?', duration_string)
if hours_match:
total_seconds += float(hours_match.group(1)) * 3600 # 小时转为秒
# 匹配分钟(支持整数和小数,如30m、45.5minutes)
minutes_match = re.search(r'(\d+(?:\.\d+)?)\s*m(?:in(?:ute)?s?)?', duration_string)
if minutes_match:
total_seconds += float(minutes_match.group(1)) * 60 # 分钟转为秒
# 匹配秒(支持整数和小数,如45s、120.5seconds)
seconds_match = re.search(r'(\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?', duration_string)
if seconds_match:
total_seconds += float(seconds_match.group(1)) # 秒直接累加
if total_seconds > 0:
return timedelta(seconds=total_seconds)
# 所有格式均匹配失败时,抛出异常
raise ValueError(f"无法解析该时长: {duration_string}")
该函数主要处理两种主流时长格式:冒号分隔格式和单位标识格式。对于冒号分隔格式,根据分隔后的部分数量,分别解析为“时:分:秒”或“分:秒”,再转换为timedelta对象。
对于单位标识格式,使用三个独立的正则表达式分别匹配小时、分钟、秒:(\d+(?:.\d+)?)可匹配整数(如1)或小数(如1.5);\s*h(?:ours?)?可匹配“h”“hour”“hours”,且允许单位前有空格。
解析出的每个时长单位,都会先转换为秒并累加到总秒数中,最后通过总秒数创建timedelta对象。这种方式支持部分时长格式(如仅“45s”“2h 15m”),无需所有单位同时存在。
测试多种时长格式:
durations = [
"1h 30m 45s",
"2:45:30",
"90 minutes",
"1.5 hours",
"45s",
"2h 15m"
]
for duration in durations:
parsed = parse_duration(duration)
print(f"{duration:15} -> {parsed}")
输出结果:
1h 30m 45s -> 1:30:45
2:45:30 -> 2:45:30
90 minutes -> 1:30:00
1.5 hours -> 1:30:00
45s -> 0:00:45
2h 15m -> 2:15:00
5. 解析ISO周日期
部分系统会使用ISO周日期格式,而非常规的日历日期。例如“2026-W03-2”表示“2026年第3周,第2天(星期二)”。这种格式在商业场景中较为常见,因为这类场景的规划多以周为单位。
以下函数可解析ISO周日期:
from datetime import datetime, timedelta
def parse_iso_week_date(iso_week_string):
"""
解析ISO周日期格式:YYYY-Www-D
示例:"2024-W03-2" = 2024年第3周,星期二
ISO周编号规则:
- 第1周是包含该年第一个星期四的周
- 星期几编号:1(星期一)至7(星期日)
"""
# 拆分ISO周日期字符串(格式:YYYY-Www-D)
parts = iso_week_string.split('-')
# 校验格式合法性
if len(parts) != 3 or not parts[1].startswith('W'):
raise ValueError(f"无效的ISO周日期格式: {iso_week_string}")
year = int(parts[0]) # 年份
week = int(parts[1][1:]) # 周数(去除前缀W)
day = int(parts[2]) # 一周中的第几天
# 校验周数和天数的合法性
if not (1 <= week <= 53):
raise ValueError(f"周数必须在1-53之间: {week}")
if not (1 <= day <= 7):
raise ValueError(f"天数必须在1-7之间: {day}")
# 找到该年的1月4日(该日期一定在第1周内,ISO标准规定)
jan_4 = datetime(year, 1, 4)
# 找到第1周的星期一(jan_4.weekday()返回0=周一,6=周日)
week_1_monday = jan_4 - timedelta(days=jan_4.weekday())
# 计算目标日期:第1周周一 + (目标周数-1)周 + (目标天数-1)天
target_date = week_1_monday + timedelta(weeks=week - 1, days=day - 1)
return target_date
ISO周日期遵循严格的规则:第1周定义为包含该年第一个星期四的周,这意味着第1周可能始于上一年的12月;一周中的天数从1(星期一)到7(星期日)编号,与Python中weekday()方法的返回值(0=周一,6=周日)逻辑一致。
该函数采用一种可靠的计算方法:首先找到该年的1月4日(ISO标准规定,该日期一定在第1周内),再通过该日期推算出第1周的星期一;最后以第1周星期一为基准,加上目标周数和目标天数的偏移量,得到最终的日期。
测试ISO周日期:
# 测试ISO周日期解析
iso_dates = [
"2024-W01-1", # 2024年第1周,星期一
"2024-W03-2", # 2024年第3周,星期二
"2024-W10-5", # 2024年第10周,星期五
]
for iso_date in iso_dates:
parsed = parse_iso_week_date(iso_date)
print(f"{iso_date} -> {parsed.strftime('%Y-%m-%d (%A)')}")
输出结果:
2024-W01-1 -> 2024-01-01 (Monday)
2024-W03-2 -> 2024-01-16 (Tuesday)
2024-W10-5 -> 2024-03-08 (Friday)
这种格式虽不如常规日期常见,但在遇到时,提前准备好解析函数能节省大量开发时间。
总结
本文中的每个函数,都利用正则表达式和日期时间运算,解决了日期时间解析中的一种常见问题。这些技术思路可迁移到其他解析场景中,你可以根据自己项目中的自定义日期格式,调整正则表达式和解析逻辑。
自己编写解析器,能帮助你更深入理解日期时间解析的底层逻辑。当遇到标准库无法处理的非标准日期格式时,你将具备编写自定义解决方案的能力。
这些函数特别适用于小型脚本、原型开发和学习项目——在这些场景中,引入重量级的第三方依赖往往得不偿失。祝你编码愉快!
推荐学习书籍 《CDA一级教材》适合CDA一级考生备考,也适合业务及数据分析岗位的从业者提升自我。完整电子版已上线CDA网校,累计已有10万+在读~ !



雷达卡





京公网安备 11010802022788号







