time.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. # coding=utf-8
  2. """
  3. 时间工具模块 - 统一时间处理函数
  4. """
  5. from datetime import datetime
  6. from typing import Optional
  7. import pytz
  8. # 默认时区
  9. DEFAULT_TIMEZONE = "Asia/Shanghai"
  10. def get_configured_time(timezone: str = DEFAULT_TIMEZONE) -> datetime:
  11. """
  12. 获取配置时区的当前时间
  13. Args:
  14. timezone: 时区名称,如 'Asia/Shanghai', 'America/Los_Angeles'
  15. Returns:
  16. 带时区信息的当前时间
  17. """
  18. try:
  19. tz = pytz.timezone(timezone)
  20. except pytz.UnknownTimeZoneError:
  21. print(f"[警告] 未知时区 '{timezone}',使用默认时区 {DEFAULT_TIMEZONE}")
  22. tz = pytz.timezone(DEFAULT_TIMEZONE)
  23. return datetime.now(tz)
  24. def format_date_folder(
  25. date: Optional[str] = None, timezone: str = DEFAULT_TIMEZONE
  26. ) -> str:
  27. """
  28. 格式化日期文件夹名 (ISO 格式: YYYY-MM-DD)
  29. Args:
  30. date: 指定日期字符串,为 None 则使用当前日期
  31. timezone: 时区名称
  32. Returns:
  33. 格式化后的日期字符串,如 '2025-12-09'
  34. """
  35. if date:
  36. return date
  37. return get_configured_time(timezone).strftime("%Y-%m-%d")
  38. def format_time_filename(timezone: str = DEFAULT_TIMEZONE) -> str:
  39. """
  40. 格式化时间文件名 (格式: HH-MM,用于文件名)
  41. Windows 系统不支持冒号作为文件名,因此使用连字符
  42. Args:
  43. timezone: 时区名称
  44. Returns:
  45. 格式化后的时间字符串,如 '15-30'
  46. """
  47. return get_configured_time(timezone).strftime("%H-%M")
  48. def get_current_time_display(timezone: str = DEFAULT_TIMEZONE) -> str:
  49. """
  50. 获取当前时间显示 (格式: HH:MM,用于显示)
  51. Args:
  52. timezone: 时区名称
  53. Returns:
  54. 格式化后的时间字符串,如 '15:30'
  55. """
  56. return get_configured_time(timezone).strftime("%H:%M")
  57. def convert_time_for_display(time_str: str) -> str:
  58. """
  59. 将 HH-MM 格式转换为 HH:MM 格式用于显示
  60. Args:
  61. time_str: 输入时间字符串,如 '15-30'
  62. Returns:
  63. 转换后的时间字符串,如 '15:30'
  64. """
  65. if time_str and "-" in time_str and len(time_str) == 5:
  66. return time_str.replace("-", ":")
  67. return time_str
  68. def format_iso_time_friendly(
  69. iso_time: str,
  70. timezone: str = DEFAULT_TIMEZONE,
  71. include_date: bool = True,
  72. ) -> str:
  73. """
  74. 将 ISO 格式时间转换为用户时区的友好显示格式
  75. Args:
  76. iso_time: ISO 格式时间字符串,如 '2025-12-29T00:20:00' 或 '2025-12-29T00:20:00+00:00'
  77. timezone: 目标时区名称
  78. include_date: 是否包含日期部分
  79. Returns:
  80. 友好格式的时间字符串,如 '12-29 08:20' 或 '08:20'
  81. """
  82. if not iso_time:
  83. return ""
  84. try:
  85. # 尝试解析各种 ISO 格式
  86. dt = None
  87. # 尝试解析带时区的格式
  88. if "+" in iso_time or iso_time.endswith("Z"):
  89. iso_time = iso_time.replace("Z", "+00:00")
  90. try:
  91. dt = datetime.fromisoformat(iso_time)
  92. except ValueError:
  93. pass
  94. # 尝试解析不带时区的格式(假设为 UTC)
  95. if dt is None:
  96. try:
  97. # 处理 T 分隔符
  98. if "T" in iso_time:
  99. dt = datetime.fromisoformat(iso_time.replace("T", " ").split(".")[0])
  100. else:
  101. dt = datetime.fromisoformat(iso_time.split(".")[0])
  102. # 假设为 UTC 时间
  103. dt = pytz.UTC.localize(dt)
  104. except ValueError:
  105. pass
  106. if dt is None:
  107. # 无法解析,返回原始字符串的简化版本
  108. if "T" in iso_time:
  109. parts = iso_time.split("T")
  110. if len(parts) == 2:
  111. date_part = parts[0][5:] # MM-DD
  112. time_part = parts[1][:5] # HH:MM
  113. return f"{date_part} {time_part}" if include_date else time_part
  114. return iso_time
  115. # 转换到目标时区
  116. try:
  117. target_tz = pytz.timezone(timezone)
  118. except pytz.UnknownTimeZoneError:
  119. target_tz = pytz.timezone(DEFAULT_TIMEZONE)
  120. dt_local = dt.astimezone(target_tz)
  121. # 格式化输出
  122. if include_date:
  123. return dt_local.strftime("%m-%d %H:%M")
  124. else:
  125. return dt_local.strftime("%H:%M")
  126. except Exception:
  127. # 出错时返回原始字符串的简化版本
  128. if "T" in iso_time:
  129. parts = iso_time.split("T")
  130. if len(parts) == 2:
  131. date_part = parts[0][5:] # MM-DD
  132. time_part = parts[1][:5] # HH:MM
  133. return f"{date_part} {time_part}" if include_date else time_part
  134. return iso_time
  135. def is_within_days(
  136. iso_time: str,
  137. max_days: int,
  138. timezone: str = DEFAULT_TIMEZONE,
  139. ) -> bool:
  140. """
  141. 检查 ISO 格式时间是否在指定天数内
  142. 用于 RSS 文章新鲜度过滤,判断文章发布时间是否超过指定天数。
  143. Args:
  144. iso_time: ISO 格式时间字符串(如 '2025-12-29T00:20:00' 或带时区)
  145. max_days: 最大天数(文章发布时间距今不超过此天数则返回 True)
  146. - max_days > 0: 正常过滤,保留 N 天内的文章
  147. - max_days <= 0: 禁用过滤,保留所有文章
  148. timezone: 时区名称(用于获取当前时间)
  149. Returns:
  150. True 如果时间在指定天数内(应保留),False 如果超过指定天数(应过滤)
  151. 如果无法解析时间,返回 True(保留文章)
  152. """
  153. # 无时间戳或禁用过滤时,保留文章
  154. if not iso_time:
  155. return True
  156. if max_days <= 0:
  157. return True # max_days=0 表示禁用过滤
  158. try:
  159. dt = None
  160. # 尝试解析带时区的格式
  161. if "+" in iso_time or iso_time.endswith("Z"):
  162. iso_time_normalized = iso_time.replace("Z", "+00:00")
  163. try:
  164. dt = datetime.fromisoformat(iso_time_normalized)
  165. except ValueError:
  166. pass
  167. # 尝试解析不带时区的格式(假设为 UTC)
  168. if dt is None:
  169. try:
  170. if "T" in iso_time:
  171. dt = datetime.fromisoformat(iso_time.replace("T", " ").split(".")[0])
  172. else:
  173. dt = datetime.fromisoformat(iso_time.split(".")[0])
  174. dt = pytz.UTC.localize(dt)
  175. except ValueError:
  176. pass
  177. if dt is None:
  178. # 无法解析时间,保留文章
  179. return True
  180. # 获取当前时间(配置的时区,带时区信息)
  181. now = get_configured_time(timezone)
  182. # 计算时间差(两个带时区的 datetime 相减会自动处理时区差异)
  183. diff = now - dt
  184. days_diff = diff.total_seconds() / (24 * 60 * 60)
  185. return days_diff <= max_days
  186. except Exception:
  187. # 出错时保留文章
  188. return True