scheduler.py 12 KB

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