tools.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. """
  2. @author: luojunhui
  3. """
  4. import json
  5. import re
  6. import oss2
  7. import random
  8. import string
  9. import hashlib
  10. import math
  11. import statistics
  12. from scipy.stats import t
  13. from odps import ODPS
  14. from datetime import datetime, timezone, date, timedelta
  15. from typing import Dict, List, Optional
  16. from requests import RequestException
  17. from urllib.parse import urlparse, parse_qs
  18. from tenacity import (
  19. stop_after_attempt,
  20. wait_exponential,
  21. retry_if_exception_type,
  22. )
  23. def safe_json_parse(text: str) -> Optional[Dict | List]:
  24. """多层降级解析 JSON:直接解析 → 提取代码块 → 提取 JSON 对象/数组
  25. 模型有时返回 ```json ... ``` 包裹的文本,或文本中夹杂 markdown 前缀/后缀。
  26. 先尝试直接解析(最常见路径),失败后逐层降级提取。
  27. """
  28. if not text:
  29. return None
  30. # 降级 1:直接解析
  31. try:
  32. return json.loads(text)
  33. except (json.JSONDecodeError, TypeError):
  34. pass
  35. clean = text.strip()
  36. # 降级 2:提取最外层 json 代码块 ```json ... ```
  37. # 优先匹配带语言标注的,再退到任意 code fence
  38. m = re.search(r"```json\s*(.*?)\s*```", clean, re.DOTALL)
  39. if m:
  40. try:
  41. return json.loads(m.group(1))
  42. except (json.JSONDecodeError, TypeError):
  43. pass
  44. else:
  45. m = re.search(r"```\s*(.*?)\s*```", clean, re.DOTALL)
  46. if m:
  47. try:
  48. return json.loads(m.group(1))
  49. except (json.JSONDecodeError, TypeError):
  50. pass
  51. # 降级 3:在文本中查找第一个完整 JSON 对象 { ... } 或数组 [ ... ]
  52. # 逐字符扫描,维护字符串状态机,正确处理内嵌括号和转义引号
  53. for bracket_pair in [("{}", "{", "}"), ("[]", "[", "]")]:
  54. opener, closer = bracket_pair[1], bracket_pair[2]
  55. start = clean.find(opener)
  56. if start == -1:
  57. continue
  58. depth = 0
  59. in_string = False
  60. escape_next = False
  61. for i in range(start, len(clean)):
  62. ch = clean[i]
  63. if escape_next:
  64. escape_next = False
  65. continue
  66. if ch == "\\":
  67. escape_next = True
  68. continue
  69. if ch == '"' and not escape_next:
  70. in_string = not in_string
  71. continue
  72. if in_string:
  73. continue
  74. if ch == opener:
  75. depth += 1
  76. elif ch == closer:
  77. depth -= 1
  78. if depth == 0:
  79. try:
  80. return json.loads(clean[start : i + 1])
  81. except (json.JSONDecodeError, TypeError):
  82. return None
  83. # 数组或对象未闭合时也尝试下
  84. try:
  85. return json.loads(clean[start:])
  86. except (json.JSONDecodeError, TypeError):
  87. pass
  88. return None
  89. def str_to_md5(strings):
  90. """
  91. 字符串转化为 md5 值
  92. :param strings:
  93. :return:
  94. """
  95. # 将字符串转换为字节
  96. original_bytes = strings.encode("utf-8")
  97. # 创建一个md5 hash对象
  98. md5_hash = hashlib.md5()
  99. # 更新hash对象,传入原始字节
  100. md5_hash.update(original_bytes)
  101. # 获取16进制形式的MD5哈希值
  102. md5_value = md5_hash.hexdigest()
  103. return md5_value
  104. def proxy():
  105. """
  106. 快代理
  107. """
  108. # 隧道域名:端口号
  109. tunnel = "j685.kdltps.com:15818"
  110. # 用户名密码方式
  111. username = "t16899444538299"
  112. password = "5w5ersso"
  113. proxies = {
  114. "http": "http://%(user)s:%(pwd)s@%(proxy)s/"
  115. % {"user": username, "pwd": password, "proxy": tunnel},
  116. "https": "http://%(user)s:%(pwd)s@%(proxy)s/"
  117. % {"user": username, "pwd": password, "proxy": tunnel},
  118. }
  119. return proxies
  120. def async_proxy():
  121. return {
  122. "url": "http://j685.kdltps.com:15818",
  123. "username": "t16899444538299",
  124. "password": "5w5ersso",
  125. }
  126. def request_retry(retry_times, min_retry_delay, max_retry_delay):
  127. """
  128. :param retry_times:
  129. :param min_retry_delay:
  130. :param max_retry_delay:
  131. """
  132. common_retry = dict(
  133. stop=stop_after_attempt(retry_times),
  134. wait=wait_exponential(min=min_retry_delay, max=max_retry_delay),
  135. retry=retry_if_exception_type((RequestException, TimeoutError)),
  136. reraise=True, # 重试耗尽后重新抛出异常
  137. )
  138. return common_retry
  139. def yield_batch(data, batch_size):
  140. """
  141. 生成批次数据
  142. :param data:
  143. :param batch_size:
  144. :return:
  145. """
  146. for i in range(0, len(data), batch_size):
  147. yield data[i : i + batch_size]
  148. def extract_root_source_id(path: str) -> dict:
  149. """
  150. 提取path参数
  151. :param path:
  152. :return:
  153. """
  154. params = parse_qs(urlparse(path).query)
  155. jump_page = params.get("jumpPage", [None])[0]
  156. if jump_page:
  157. params2 = parse_qs(jump_page)
  158. res = {
  159. "video_id": params2["pages/user-videos?id"][0],
  160. "root_source_id": params2["rootSourceId"][0],
  161. }
  162. return res
  163. else:
  164. return {}
  165. def show_desc_to_sta(show_desc: str):
  166. def decode_show_v(show_v: str) -> int:
  167. """
  168. 解析数值(全球通用):
  169. 支持:
  170. - 中文:1.3万 / 2千 / 5亿
  171. - 英文:13k / 2.5m / 1.2b
  172. - 混合:1.2万阅读 / 13k views
  173. """
  174. if not show_v:
  175. return 0
  176. show_v = show_v.strip().lower()
  177. # 防止欧洲小数格式:1,3k
  178. show_v = show_v.replace(",", ".")
  179. # 提取 数字 + 单位
  180. match = re.search(r"(\d+(?:\.\d+)?)([a-z一-龥]*)", show_v)
  181. if not match:
  182. return 0
  183. num = float(match.group(1))
  184. unit = match.group(2)
  185. # 中文单位
  186. if "亿" in unit:
  187. num *= 1e8
  188. elif "万" in unit:
  189. num *= 1e4
  190. elif "千" in unit:
  191. num *= 1e3
  192. # 英文单位
  193. elif unit.startswith("k"):
  194. num *= 1e3
  195. elif unit.startswith("m"):
  196. num *= 1e6
  197. elif unit.startswith("b"):
  198. num *= 1e9
  199. return int(num)
  200. def decode_show_k(show_k: str) -> str:
  201. """
  202. 统一 key(中英文)
  203. """
  204. if not show_k:
  205. return "show_unknown"
  206. show_k = show_k.strip().lower()
  207. mapping = {
  208. # 中文
  209. "阅读": "show_view_count",
  210. "看过": "show_view_count",
  211. "观看": "show_view_count",
  212. "赞": "show_like_count",
  213. "点赞": "show_like_count",
  214. "付费": "show_pay_count",
  215. "赞赏": "show_zs_count",
  216. # 英文
  217. "reads": "show_view_count",
  218. "views": "show_view_count",
  219. "view": "show_view_count",
  220. "likes": "show_like_count",
  221. "like": "show_like_count",
  222. "payments": "show_pay_count",
  223. "paid": "show_pay_count",
  224. }
  225. return mapping.get(show_k, "show_unknown")
  226. # ===== 主逻辑 =====
  227. if not show_desc:
  228. return {
  229. "show_view_count": 0,
  230. "show_like_count": 0,
  231. "show_pay_count": 0,
  232. "show_zs_count": 0,
  233. }
  234. # 去掉 +
  235. show_desc = show_desc.replace("+", "")
  236. sta = {}
  237. # 按"组"切分(兼容各种奇怪空格)
  238. groups = re.split(r"[  ]+", show_desc)
  239. for group in groups:
  240. group = group.strip()
  241. if not group:
  242. continue
  243. # 按 key-value 分隔符拆
  244. parts = group.split(" ")
  245. if len(parts) != 2:
  246. continue
  247. a, b = parts
  248. # 自动判断哪个是数字
  249. if re.search(r"\d", a):
  250. show_v, show_k = a, b
  251. else:
  252. show_k, show_v = a, b
  253. k = decode_show_k(show_k)
  254. v = decode_show_v(show_v)
  255. if k != "show_unknown":
  256. sta[k] = v
  257. return {
  258. "show_view_count": sta.get("show_view_count", 0),
  259. "show_like_count": sta.get("show_like_count", 0),
  260. "show_pay_count": sta.get("show_pay_count", 0),
  261. "show_zs_count": sta.get("show_zs_count", 0),
  262. }
  263. def generate_gzh_id(url):
  264. biz = url.split("biz=")[1].split("&")[0]
  265. idx = url.split("&idx=")[1].split("&")[0]
  266. sn = url.split("&sn=")[1].split("&")[0]
  267. url_bit = "{}-{}-{}".format(biz, idx, sn).encode()
  268. md5_hash = hashlib.md5()
  269. md5_hash.update(url_bit)
  270. md5_value = md5_hash.hexdigest()
  271. return md5_value
  272. def timestamp_to_str(timestamp, string_format="%Y-%m-%d %H:%M:%S") -> str:
  273. """
  274. :param string_format:
  275. :param timestamp:
  276. """
  277. dt_object = (
  278. datetime.utcfromtimestamp(timestamp).replace(tzinfo=timezone.utc).astimezone()
  279. )
  280. date_string = dt_object.strftime(string_format)
  281. return date_string
  282. def days_remaining_in_month():
  283. # 获取当前日期
  284. today = date.today()
  285. # 获取下个月的第一天
  286. if today.month == 12:
  287. next_month = today.replace(year=today.year + 1, month=1, day=1)
  288. else:
  289. next_month = today.replace(month=today.month + 1, day=1)
  290. # 计算本月最后一天(下个月第一天减去1天)
  291. last_day_of_month = next_month - timedelta(days=1)
  292. # 计算剩余天数
  293. remaining_days = (last_day_of_month - today).days
  294. return remaining_days
  295. def generate_task_trace_id():
  296. random_str = "".join(random.choices(string.ascii_lowercase + string.digits, k=16))
  297. return f"Task-{datetime.now().strftime('%Y%m%d%H%M%S')}-{random_str}"
  298. def ci_lower(data: List[int], conf: float = 0.95) -> float:
  299. """
  300. 计算data的置信区间下限
  301. """
  302. if len(data) < 2:
  303. raise ValueError("Sample length less than 2")
  304. n = len(data)
  305. mean = statistics.mean(data)
  306. std = statistics.stdev(data) / math.sqrt(n)
  307. # t 分位点(左侧):ppf 返回负值
  308. t_left = t.ppf((1 - conf) / 2, df=n - 1)
  309. return mean + t_left * std
  310. def fetch_from_odps(query):
  311. client = ODPS(
  312. access_id="LTAIWYUujJAm7CbH",
  313. secret_access_key="RfSjdiWwED1sGFlsjXv0DlfTnZTG1P",
  314. endpoint="http://service.cn.maxcompute.aliyun.com/api",
  315. project="loghubods",
  316. )
  317. with client.execute_sql(query).open_reader() as reader:
  318. if reader:
  319. return [item for item in reader]
  320. else:
  321. return []
  322. def init_odps_client():
  323. return ODPS(
  324. access_id="LTAIWYUujJAm7CbH",
  325. secret_access_key="RfSjdiWwED1sGFlsjXv0DlfTnZTG1P",
  326. endpoint="http://service.cn.maxcompute.aliyun.com/api",
  327. project="loghubods",
  328. )
  329. def upload_to_oss(local_video_path, oss_key):
  330. """
  331. 把视频上传到 oss
  332. :return:
  333. """
  334. access_key_id = "LTAIP6x1l3DXfSxm"
  335. access_key_secret = "KbTaM9ars4OX3PMS6Xm7rtxGr1FLon"
  336. endpoint = "oss-cn-hangzhou.aliyuncs.com"
  337. bucket_name = "art-pubbucket"
  338. bucket = oss2.Bucket(
  339. oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name
  340. )
  341. bucket.put_object_from_file(key=oss_key, filename=local_video_path)
  342. return oss_key