tools.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """通用工具函数 —— 只包含与业务无关的基础设施工具"""
  2. import hashlib
  3. import json
  4. import random
  5. import re
  6. import string
  7. from datetime import datetime, timezone, date, timedelta
  8. from typing import Dict, List, Optional
  9. def safe_json_parse(text: str) -> Optional[Dict | List]:
  10. """多层降级解析 JSON:直接解析 → 提取代码块 → 提取 JSON 对象/数组
  11. LLM 可能返回 ```json ... ``` 包裹的文本,或文本中夹杂 markdown 前缀/后缀。
  12. 先尝试直接解析(最常见路径),失败后逐层降级提取。
  13. """
  14. if not text:
  15. return None
  16. # 降级 1:直接解析
  17. try:
  18. return json.loads(text)
  19. except (json.JSONDecodeError, TypeError):
  20. pass
  21. clean = text.strip()
  22. # 降级 2:提取最外层 json 代码块
  23. m = re.search(r"```json\s*(.*?)\s*```", clean, re.DOTALL)
  24. if m:
  25. try:
  26. return json.loads(m.group(1))
  27. except (json.JSONDecodeError, TypeError):
  28. pass
  29. else:
  30. m = re.search(r"```\s*(.*?)\s*```", clean, re.DOTALL)
  31. if m:
  32. try:
  33. return json.loads(m.group(1))
  34. except (json.JSONDecodeError, TypeError):
  35. pass
  36. # 降级 3:在文本中查找第一个完整 JSON 对象 { ... } 或数组 [ ... ]
  37. for bracket_pair in [("{}", "{", "}"), ("[]", "[", "]")]:
  38. opener, closer = bracket_pair[1], bracket_pair[2]
  39. start = clean.find(opener)
  40. if start == -1:
  41. continue
  42. depth = 0
  43. in_string = False
  44. escape_next = False
  45. for i in range(start, len(clean)):
  46. ch = clean[i]
  47. if escape_next:
  48. escape_next = False
  49. continue
  50. if ch == "\\":
  51. escape_next = True
  52. continue
  53. if ch == '"' and not escape_next:
  54. in_string = not in_string
  55. continue
  56. if in_string:
  57. continue
  58. if ch == opener:
  59. depth += 1
  60. elif ch == closer:
  61. depth -= 1
  62. if depth == 0:
  63. try:
  64. return json.loads(clean[start : i + 1])
  65. except (json.JSONDecodeError, TypeError):
  66. return None
  67. try:
  68. return json.loads(clean[start:])
  69. except (json.JSONDecodeError, TypeError):
  70. pass
  71. return None
  72. def str_to_md5(strings: str) -> str:
  73. """字符串转 MD5"""
  74. original_bytes = strings.encode("utf-8")
  75. md5_hash = hashlib.md5()
  76. md5_hash.update(original_bytes)
  77. return md5_hash.hexdigest()
  78. def generate_task_trace_id() -> str:
  79. """生成任务追踪 ID,格式: Task-YYYYMMDDHHmmss-16位随机"""
  80. random_str = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
  81. return f"Task-{datetime.now().strftime('%Y%m%d%H%M%S')}-{random_str}"
  82. def yield_batch(data: list, batch_size: int):
  83. """生成批次数据"""
  84. for i in range(0, len(data), batch_size):
  85. yield data[i : i + batch_size]
  86. def timestamp_to_str(timestamp: int, string_format: str = "%Y-%m-%d %H:%M:%S") -> str:
  87. """时间戳转字符串"""
  88. dt_object = (
  89. datetime.utcfromtimestamp(timestamp).replace(tzinfo=timezone.utc).astimezone()
  90. )
  91. return dt_object.strftime(string_format)
  92. def days_remaining_in_month() -> int:
  93. """计算本月剩余天数"""
  94. today = date.today()
  95. if today.month == 12:
  96. next_month = today.replace(year=today.year + 1, month=1, day=1)
  97. else:
  98. next_month = today.replace(month=today.month + 1, day=1)
  99. last_day_of_month = next_month - timedelta(days=1)
  100. return (last_day_of_month - today).days