scheduler.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. """统一定时任务调度入口。"""
  2. from __future__ import annotations
  3. import argparse
  4. import json
  5. import sys
  6. from datetime import datetime, timedelta
  7. from pathlib import Path
  8. from typing import Any
  9. PROJECT_ROOT = Path(__file__).resolve().parents[1]
  10. if str(PROJECT_ROOT) not in sys.path:
  11. sys.path.insert(0, str(PROJECT_ROOT))
  12. from app.hot_content.client import JsonApiClient
  13. from app.hot_content.decode_result_service import run_once as run_decode_result_once
  14. from app.hot_content.config import load_flow_config
  15. from app.hot_content.postprocess_service import run_once as run_postprocess_once
  16. from app.hot_content.repository import HotContentRepository
  17. from app.hot_content.service import run_once
  18. from app.hot_content.timezone import SHANGHAI_TZ
  19. from app.hot_content.types import FlowConfig
  20. from app.hot_content.wxindex_words import run_wxindex_words_daily_job
  21. from app.hot_content.wxindex_heat_pattern import run_wxindex_heat_pattern_daily_job
  22. from app.festival_demand.config import load_festival_demand_config
  23. from app.festival_demand.service import run_festival_demand_daily_job, print_festival_demand_summary
  24. from app.festival_demand.types import FestivalDemandConfig
  25. def _import_blocking_scheduler() -> Any:
  26. try:
  27. from apscheduler.schedulers.blocking import BlockingScheduler
  28. except ImportError as exc:
  29. raise RuntimeError("缺少依赖:请先执行 pip install -r requirements.txt") from exc
  30. return BlockingScheduler
  31. def run_hot_content_job(config: FlowConfig) -> None:
  32. try:
  33. summary = run_once(config)
  34. print(json.dumps(summary, ensure_ascii=False, indent=2))
  35. except Exception as exc:
  36. print(f"hot content flow failed: {exc}", file=sys.stderr)
  37. def run_decode_result_job(config: FlowConfig) -> None:
  38. """解构结果拉取 -> 后处理 -> 写入 ODPS 需求表。"""
  39. summary: dict[str, Any] = {}
  40. try:
  41. summary["decode_result"] = run_decode_result_once(config)
  42. except Exception as exc:
  43. summary["decode_result_error"] = str(exc)
  44. print(f"decode result flow failed: {exc}", file=sys.stderr)
  45. try:
  46. summary["postprocess"] = run_postprocess_once(config)
  47. except Exception as exc:
  48. summary["postprocess_error"] = str(exc)
  49. print(f"postprocess flow failed: {exc}", file=sys.stderr)
  50. print(json.dumps(summary, ensure_ascii=False, indent=2))
  51. def run_postprocess_job(config: FlowConfig) -> None:
  52. try:
  53. summary = run_postprocess_once(config)
  54. print(json.dumps({"job": "postprocess", "summary": summary}, ensure_ascii=False, indent=2))
  55. except Exception as exc:
  56. print(f"postprocess flow failed: {exc}", file=sys.stderr)
  57. def run_wxindex_words_refresh_job(config: FlowConfig) -> None:
  58. repository = HotContentRepository(config.mysql)
  59. api_client = JsonApiClient(
  60. timeout_seconds=config.request_timeout_seconds,
  61. verify_ssl=config.https_verify_ssl,
  62. )
  63. try:
  64. summary = run_wxindex_words_daily_job(
  65. repository,
  66. api_client,
  67. config.wxindex_api_url,
  68. )
  69. print(
  70. json.dumps(
  71. {"job": "wxindex_words_refresh", "summary": summary},
  72. ensure_ascii=False,
  73. indent=2,
  74. )
  75. )
  76. except Exception as exc:
  77. print(f"wxindex words refresh failed: {exc}", file=sys.stderr)
  78. finally:
  79. repository.close()
  80. def run_wxindex_heat_pattern_job(config: FlowConfig) -> None:
  81. repository = HotContentRepository(config.mysql)
  82. api_client = JsonApiClient(
  83. timeout_seconds=config.request_timeout_seconds,
  84. verify_ssl=config.https_verify_ssl,
  85. )
  86. try:
  87. summary = run_wxindex_heat_pattern_daily_job(
  88. repository,
  89. config=config,
  90. api_client=api_client,
  91. )
  92. print(
  93. json.dumps(
  94. {"job": "wxindex_heat_pattern", "summary": summary},
  95. ensure_ascii=False,
  96. indent=2,
  97. )
  98. )
  99. except Exception as exc:
  100. print(f"wxindex heat pattern failed: {exc}", file=sys.stderr)
  101. finally:
  102. repository.close()
  103. def run_festival_demand_job(config: FestivalDemandConfig) -> None:
  104. try:
  105. summary = run_festival_demand_daily_job(config)
  106. print_festival_demand_summary(summary)
  107. print(
  108. json.dumps(
  109. {"job": "festival_demand", "summary": summary},
  110. ensure_ascii=False,
  111. indent=2,
  112. )
  113. )
  114. except Exception as exc:
  115. print(f"festival demand failed: {exc}", file=sys.stderr)
  116. def register_hot_content_job(scheduler: Any, config: FlowConfig) -> None:
  117. scheduler.add_job(
  118. run_hot_content_job,
  119. trigger="cron",
  120. hour=config.hot_flow_cron_hours,
  121. minute=config.hot_flow_cron_minute,
  122. timezone=SHANGHAI_TZ,
  123. args=[config],
  124. id="hot_content_flow",
  125. name="热点内容抓取搜索解构流程",
  126. replace_existing=True,
  127. coalesce=True,
  128. max_instances=1,
  129. )
  130. def register_decode_result_job(scheduler: Any, config: FlowConfig) -> None:
  131. interval_seconds = max(config.decode_result_interval_seconds, 60)
  132. scheduler.add_job(
  133. run_decode_result_job,
  134. trigger="interval",
  135. seconds=interval_seconds,
  136. args=[config],
  137. id="decode_result_flow",
  138. name="解构结果拉取、后处理与 ODPS 需求表写入",
  139. replace_existing=True,
  140. coalesce=True,
  141. max_instances=1,
  142. next_run_time=datetime.now(SHANGHAI_TZ) + timedelta(seconds=interval_seconds),
  143. )
  144. def register_wxindex_words_refresh_job(scheduler: Any, config: FlowConfig) -> None:
  145. scheduler.add_job(
  146. run_wxindex_words_refresh_job,
  147. trigger="cron",
  148. hour=config.wxindex_words_cron_hours,
  149. minute=config.wxindex_words_cron_minute,
  150. timezone=SHANGHAI_TZ,
  151. args=[config],
  152. id="wxindex_words_refresh",
  153. name="微信指数词汇总表补全缺失日期并清理低均值词",
  154. replace_existing=True,
  155. coalesce=True,
  156. max_instances=1,
  157. )
  158. def register_wxindex_heat_pattern_job(scheduler: Any, config: FlowConfig) -> None:
  159. scheduler.add_job(
  160. run_wxindex_heat_pattern_job,
  161. trigger="cron",
  162. hour=config.wxindex_heat_pattern_cron_hours,
  163. minute=config.wxindex_heat_pattern_cron_minute,
  164. timezone=SHANGHAI_TZ,
  165. args=[config],
  166. id="wxindex_heat_pattern",
  167. name="微信指数热度模式分析(持续高热/上涨/暴涨)",
  168. replace_existing=True,
  169. coalesce=True,
  170. max_instances=1,
  171. )
  172. def register_festival_demand_job(scheduler: Any, config: FestivalDemandConfig) -> None:
  173. scheduler.add_job(
  174. run_festival_demand_job,
  175. trigger="cron",
  176. hour=config.cron_hours,
  177. minute=config.cron_minute,
  178. timezone=SHANGHAI_TZ,
  179. args=[config],
  180. id="festival_demand",
  181. name="节日需求:活跃节日检测 + ODPS 需求词匹配",
  182. replace_existing=True,
  183. coalesce=True,
  184. max_instances=1,
  185. )
  186. def start_scheduler() -> None:
  187. BlockingScheduler = _import_blocking_scheduler()
  188. scheduler = BlockingScheduler(timezone=SHANGHAI_TZ)
  189. config = load_flow_config()
  190. festival_config = load_festival_demand_config()
  191. register_hot_content_job(scheduler, config)
  192. register_decode_result_job(scheduler, config)
  193. register_wxindex_words_refresh_job(scheduler, config)
  194. register_wxindex_heat_pattern_job(scheduler, config)
  195. register_festival_demand_job(scheduler, festival_config)
  196. print(
  197. "scheduler started, timezone=Asia/Shanghai, "
  198. "jobs=['hot_content_flow', 'decode_result_flow', 'wxindex_words_refresh', "
  199. "'wxindex_heat_pattern', 'festival_demand'], "
  200. f"hot_cron={config.hot_flow_cron_hours}:{config.hot_flow_cron_minute:02d}, "
  201. f"decode_result_interval={config.decode_result_interval_seconds}s, "
  202. f"wxindex_words_cron={config.wxindex_words_cron_hours}:{config.wxindex_words_cron_minute:02d}, "
  203. f"wxindex_heat_pattern_cron="
  204. f"{config.wxindex_heat_pattern_cron_hours}:{config.wxindex_heat_pattern_cron_minute:02d}, "
  205. f"festival_demand_cron={festival_config.cron_hours}:{festival_config.cron_minute:02d}"
  206. )
  207. scheduler.start()
  208. def parse_args() -> argparse.Namespace:
  209. parser = argparse.ArgumentParser(description="统一定时任务调度入口")
  210. parser.add_argument("--once", action="store_true", help="执行一次,不启动调度器")
  211. parser.add_argument(
  212. "--job",
  213. choices=(
  214. "all",
  215. "hot-content",
  216. "decode-result",
  217. "postprocess",
  218. "wxindex-refresh",
  219. "wxindex-heat-pattern",
  220. "festival-demand",
  221. ),
  222. default="all",
  223. help="--once 时选择执行哪个任务",
  224. )
  225. return parser.parse_args()
  226. def main() -> None:
  227. args = parse_args()
  228. if args.once:
  229. config = load_flow_config()
  230. if args.job in {"all", "hot-content"}:
  231. summary = run_once(config)
  232. print(
  233. json.dumps(
  234. {"job": "hot_content_flow", "summary": summary},
  235. ensure_ascii=False,
  236. indent=2,
  237. )
  238. )
  239. if args.job in {"all", "decode-result"}:
  240. summary: dict[str, Any] = {}
  241. try:
  242. summary["decode_result"] = run_decode_result_once(config)
  243. except Exception as exc:
  244. summary["decode_result_error"] = str(exc)
  245. try:
  246. summary["postprocess"] = run_postprocess_once(config)
  247. except Exception as exc:
  248. summary["postprocess_error"] = str(exc)
  249. print(
  250. json.dumps(
  251. {"job": "decode_result_flow", "summary": summary},
  252. ensure_ascii=False,
  253. indent=2,
  254. )
  255. )
  256. if args.job in {"postprocess"}:
  257. summary = run_postprocess_once(config)
  258. print(
  259. json.dumps(
  260. {"job": "postprocess", "summary": summary},
  261. ensure_ascii=False,
  262. indent=2,
  263. )
  264. )
  265. if args.job in {"wxindex-refresh"}:
  266. run_wxindex_words_refresh_job(config)
  267. if args.job in {"wxindex-heat-pattern"}:
  268. run_wxindex_heat_pattern_job(config)
  269. if args.job in {"festival-demand"}:
  270. festival_config = load_festival_demand_config()
  271. run_festival_demand_job(festival_config)
  272. return
  273. start_scheduler()
  274. if __name__ == "__main__":
  275. main()