270 lines
9.0 KiB
Python
270 lines
9.0 KiB
Python
from datetime import datetime, timedelta, timezone, date
|
||
from typing import Optional, Union, Tuple
|
||
from dateutil import parser
|
||
|
||
# ===================== 时区与格式化常量(PHP风格映射) =====================
|
||
# 北京时间 东八区 UTC+8
|
||
BEIJING_TZ = timezone(timedelta(hours=8))
|
||
UTC_TZ = timezone.utc
|
||
|
||
# Python strftime 标准格式
|
||
FORMAT_DATETIME = "%Y-%m-%d %H:%M:%S"
|
||
FORMAT_DATE = "%Y-%m-%d"
|
||
FORMAT_TIME = "%H:%M:%S"
|
||
|
||
# PHP格式 -> Python格式 映射,贴近PHP使用习惯
|
||
PHP_FMT_MAP = {
|
||
"Y-m-d": FORMAT_DATE,
|
||
"Y-m-d H:i:s": FORMAT_DATETIME,
|
||
"H:i:s": FORMAT_TIME
|
||
}
|
||
|
||
# ===================== 私有公共工具函数(内部复用) =====================
|
||
def _normalize_date_str(date_str: str) -> str:
|
||
"""
|
||
预处理中文格式日期字符串,统一转为横杠分隔标准格式
|
||
支持:2025年01月01日 12时30分00秒 / 2025:01:01 等格式
|
||
"""
|
||
if not isinstance(date_str, str):
|
||
return ""
|
||
s = date_str.strip()
|
||
# 中文、全角符号替换清洗
|
||
s = s.replace(":", ":") \
|
||
.replace("年", "-") \
|
||
.replace("月", "-") \
|
||
.replace("日", " ") \
|
||
.replace("时", ":") \
|
||
.replace("分", ":") \
|
||
.replace("秒", "")
|
||
return s
|
||
|
||
|
||
def _get_python_fmt(fmt: str) -> str:
|
||
"""兼容PHP格式字符串,自动转为Python strftime格式"""
|
||
return PHP_FMT_MAP.get(fmt, fmt)
|
||
|
||
|
||
def _convert_ts_to_datetime(timestamp: Union[int, float], tz: timezone = BEIJING_TZ) -> datetime:
|
||
"""
|
||
统一处理 10位秒 / 13位毫秒 时间戳 -> 带时区datetime
|
||
"""
|
||
ts = float(timestamp)
|
||
# 毫秒时间戳兼容
|
||
if ts > 10 ** 12:
|
||
ts /= 1000
|
||
return datetime.fromtimestamp(ts, tz=tz)
|
||
|
||
|
||
def _parse_datetime_with_tz(dt_str: str, fmt: str, tz: timezone = BEIJING_TZ) -> Optional[datetime]:
|
||
"""
|
||
通用:固定格式日期字符串解析+绑定时区,支持中文日期预处理
|
||
仅支持指定fmt格式,不支持带时区后缀字符串
|
||
"""
|
||
try:
|
||
fmt = _get_python_fmt(fmt)
|
||
normalize_str = _normalize_date_str(dt_str)
|
||
dt = datetime.strptime(normalize_str, fmt)
|
||
return dt.replace(tzinfo=tz)
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
def _smart_parse_datetime(dt_str: str, base_dt: datetime, is_timezone: bool) -> Optional[datetime]:
|
||
"""
|
||
私有:dateutil智能解析日期(用于带时区、相对时间、中文复杂日期)
|
||
"""
|
||
clean_str = _normalize_date_str(dt_str)
|
||
try:
|
||
parsed_dt = parser.parse(clean_str, default=base_dt, tzinfos=None)
|
||
if is_timezone:
|
||
# 保留原始时区,无时区则兜底北京时间
|
||
if parsed_dt.tzinfo is None:
|
||
parsed_dt = parsed_dt.replace(tzinfo=BEIJING_TZ)
|
||
else:
|
||
# 普通日期强制绑定北京时间
|
||
parsed_dt = parsed_dt.replace(tzinfo=BEIJING_TZ)
|
||
return parsed_dt
|
||
except (parser.ParserError, ValueError, TypeError):
|
||
return None
|
||
|
||
# ===================== 对外工具方法(对标PHP + dateutil增强) =====================
|
||
def time() -> int:
|
||
"""
|
||
对标 PHP time()
|
||
获取当前北京时间 10位 秒级时间戳
|
||
"""
|
||
return int(datetime.now(tz=BEIJING_TZ).timestamp())
|
||
|
||
|
||
def microtime(get_as_float: bool = False) -> Union[float, str]:
|
||
"""
|
||
对标 PHP microtime()
|
||
:param get_as_float: True 返回浮点时间戳,False 返回 "微秒 秒" 字符串
|
||
"""
|
||
now = datetime.now(tz=BEIJING_TZ)
|
||
ts = now.timestamp()
|
||
if get_as_float:
|
||
return ts
|
||
sec = int(ts)
|
||
usec = int((ts - sec) * 1000000)
|
||
return f"{usec:06d} {sec}"
|
||
|
||
|
||
def strtotime(
|
||
time_str: str,
|
||
now: Optional[int] = None,
|
||
is_timezone: bool = False
|
||
) -> Optional[int]:
|
||
"""
|
||
【增强版 对标 PHP strtotime】依托 dateutil 支持相对时间、带时区时间解析
|
||
:param time_str: 待解析日期/相对时间字符串
|
||
:param now: 基准时间戳,默认当前北京时间
|
||
:param is_timezone: 是否为带时区格式的时间字符串(如 2026-07-03 10:01:29.621351+08)
|
||
- True:优先使用字符串自带时区,无时区则兜底北京时间
|
||
- False:默认按北京时间解析普通日期字符串
|
||
:return: 秒级时间戳,解析失败返回 None
|
||
"""
|
||
if not isinstance(time_str, str) or not time_str.strip():
|
||
return None
|
||
|
||
# 基准时间:默认当前北京时间
|
||
base_dt = datetime.fromtimestamp(now, tz=BEIJING_TZ) if now else datetime.now(tz=BEIJING_TZ)
|
||
parsed_dt = _smart_parse_datetime(time_str, base_dt, is_timezone)
|
||
if not parsed_dt:
|
||
return None
|
||
return int(parsed_dt.timestamp())
|
||
|
||
|
||
def date(fmt: str, timestamp: Optional[int] = None) -> str:
|
||
"""
|
||
对标 PHP date()
|
||
时间戳格式化,默认北京时间
|
||
:param fmt: 支持 Y-m-d / Y-m-d H:i:s 或 %Y-%m-%d 原生格式
|
||
:param timestamp: 秒时间戳,None则取当前时间
|
||
"""
|
||
if timestamp is None:
|
||
dt = datetime.now(tz=BEIJING_TZ)
|
||
else:
|
||
dt = _convert_ts_to_datetime(timestamp)
|
||
|
||
python_fmt = _get_python_fmt(fmt)
|
||
return dt.strftime(python_fmt)
|
||
|
||
|
||
def timestamp_to_datetime(timestamp: Union[int, float], fmt: str = "Y-m-d H:i:s") -> str:
|
||
"""
|
||
时间戳(秒/毫秒)→ 北京时间格式化字符串
|
||
:param timestamp: 10位秒 /13位毫秒
|
||
:param fmt: PHP风格格式 Y-m-d / Y-m-d H:i:s
|
||
"""
|
||
dt = _convert_ts_to_datetime(timestamp)
|
||
python_fmt = _get_python_fmt(fmt)
|
||
return dt.strftime(python_fmt)
|
||
|
||
|
||
def datetime_to_timestamp(dt_str: str, fmt: str = "Y-m-d H:i:s") -> Optional[int]:
|
||
"""
|
||
日期字符串(含中文日期) → 北京时间秒时间戳
|
||
:param dt_str: 日期字符串
|
||
:param fmt: PHP风格格式化模板
|
||
:return: 秒时间戳,解析失败返回None
|
||
"""
|
||
dt = _parse_datetime_with_tz(dt_str, fmt)
|
||
if not dt:
|
||
return None
|
||
return int(dt.timestamp())
|
||
|
||
|
||
def get_today_start_timestamp() -> int:
|
||
"""获取今日 00:00:00 北京时间 秒时间戳"""
|
||
today: date = datetime.now(tz=BEIJING_TZ).date()
|
||
start_dt = datetime.combine(today, datetime.min.time(), tzinfo=BEIJING_TZ)
|
||
return int(start_dt.timestamp())
|
||
|
||
|
||
def get_today_end_timestamp() -> int:
|
||
"""获取今日 23:59:59.999999 北京时间 秒时间戳"""
|
||
today: date = datetime.now(tz=BEIJING_TZ).date()
|
||
end_dt = datetime.combine(today, datetime.max.time(), tzinfo=BEIJING_TZ)
|
||
return int(end_dt.timestamp())
|
||
|
||
|
||
def parse_date_range(date_list: list, fmt: str = "Y-m-d", is_timezone: bool = False) -> Optional[Tuple[datetime, datetime]]:
|
||
"""
|
||
时间范围解析:["2025-01-01","2026-01-01"] → (开始0点,结束23:59:59.999999) 带北京时间
|
||
:param date_list: 长度为2的日期字符串数组
|
||
:param fmt: 日期格式,默认Y-m-d
|
||
:param is_timezone: 是否为带时区格式的时间字符串(如 2026-07-03 10:01:29.621351+08)
|
||
- True:使用dateutil智能解析,优先保留字符串自带时区,无时区兜底北京时间
|
||
- False:按指定fmt格式精准解析,强制北京时间
|
||
:return: (start_dt, end_dt) 格式非法返回None
|
||
"""
|
||
# 强参数校验
|
||
if not isinstance(date_list, list) or len(date_list) != 2:
|
||
return None
|
||
start_str, end_str = date_list[0].strip(), date_list[1].strip()
|
||
if not start_str or not end_str:
|
||
return None
|
||
|
||
base_now = datetime.now(tz=BEIJING_TZ)
|
||
|
||
if is_timezone:
|
||
# 带时区场景:智能解析,支持 2026-07-03 08:16:26.88303+08
|
||
start_dt = _smart_parse_datetime(start_str, base_now, is_timezone=True)
|
||
end_dt = _smart_parse_datetime(end_str, base_now, is_timezone=True)
|
||
else:
|
||
# 普通前端日期:固定格式解析
|
||
start_dt = _parse_datetime_with_tz(start_str, fmt, BEIJING_TZ)
|
||
end_dt = _parse_datetime_with_tz(end_str, fmt, BEIJING_TZ)
|
||
|
||
if not start_dt or not end_dt:
|
||
return None
|
||
|
||
# 结束时间补全到当日最后一毫秒
|
||
end_dt = end_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||
return start_dt, end_dt
|
||
|
||
|
||
def db_tz_str_to_datetime(db_datetime_str: str) -> Optional[datetime]:
|
||
"""
|
||
数据库timestamptz格式字符串(2026-07-03 08:16:26.88303+08)转为带时区datetime
|
||
"""
|
||
try:
|
||
dt = datetime.fromisoformat(db_datetime_str)
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=BEIJING_TZ)
|
||
return dt
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
def datetime_to_db_tz_str(dt: datetime) -> str:
|
||
"""
|
||
带时区datetime转为数据库timestamptz标准字符串,统一北京时间存储
|
||
"""
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=BEIJING_TZ)
|
||
else:
|
||
dt = dt.astimezone(BEIJING_TZ)
|
||
return dt.isoformat()
|
||
|
||
|
||
# 导出列表
|
||
__all__ = [
|
||
"BEIJING_TZ",
|
||
"UTC_TZ",
|
||
"FORMAT_DATETIME",
|
||
"FORMAT_DATE",
|
||
"FORMAT_TIME",
|
||
"time",
|
||
"microtime",
|
||
"strtotime",
|
||
"date",
|
||
"timestamp_to_datetime",
|
||
"datetime_to_timestamp",
|
||
"get_today_start_timestamp",
|
||
"get_today_end_timestamp",
|
||
"parse_date_range",
|
||
"db_tz_str_to_datetime",
|
||
"datetime_to_db_tz_str"
|
||
] |