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" ]