Files

270 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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秒 / 20250101 等格式
"""
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"
]