start_run_yaoshibang_v2.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """药师帮统一入口 — 根据任务 snapshot_collect_status 字段自动切换普通/快照模式。
  2. 与公共类 collect_schedule_runner 不同:本入口直接对接 CrawlerScheduler API
  3. (拉取任务/心跳/进度上报),并支持快照模式切换、账号注入等药师帮定制逻辑,
  4. 不再依赖 CollectScheduleRunner。
  5. """
  6. import random
  7. import time
  8. import json
  9. from spiders.yaoshibang.ysbang_crawl import YsbSpider
  10. from spiders.yaoshibang.ysb_snapshot_crawl import YaoShiBangSnapshot
  11. from commons.scheduler import CrawlerScheduler
  12. from commons.conn_mysql import MySQLPoolOn2
  13. from commons.Logger import logger
  14. from commons.feishu_webhook import send_text
  15. PLATFORM_NAME = "药师帮"
  16. PLATFORM_ID = 5
  17. def get_device_id_from_db():
  18. """从 accounts_platform 表获取一个可用的药师帮账号(platform=5, status=1, equipment_id=3),
  19. 用 phone 作为 DEVICE_ID(回告接口的 username 也用它),同时返回密码供 spider 登录。"""
  20. db = MySQLPoolOn2()
  21. sql = """SELECT `name`, `phone`, `password` FROM `accounts_platform`
  22. WHERE `platform`=5 AND `status`=1 AND `equipment_id`=3
  23. ORDER BY `cookie_timestamp` ASC LIMIT 1"""
  24. rows = db.select_data(sql)
  25. if not rows:
  26. logger.error("无可用药师帮账号(platform=5, status=1, equipment_id=3)")
  27. return None
  28. account = rows[0]
  29. logger.info("获取到药师帮账号: name=%s, phone=%s", account["name"], account["phone"])
  30. return account["phone"], account["password"], account["name"]
  31. def _build_task_dict(api_task, device_id, account_password, account_name, scheduler):
  32. """将调度 API 返回的原始任务字段组装为 spider 需要的 task_dict。"""
  33. current_page = api_task.get("current_page", 0)
  34. start_page = current_page if current_page > 0 else 1
  35. task_dict = {
  36. # 新 API 字段
  37. "id": api_task.get("id"),
  38. "company_id": api_task.get("company_id"),
  39. "product_name": api_task.get("product_name", ""),
  40. "product_specs": api_task.get("product_specs", ""),
  41. "product_brand": api_task.get("product_brand", ""),
  42. "product_keyword": api_task.get("product_keyword", ""),
  43. "current_page": current_page,
  44. "start_offset": api_task.get("start_offset", 0),
  45. # 兼容旧字段
  46. "start_page": start_page,
  47. "end_page": 0,
  48. # spider 逐页回告用
  49. "scheduler": scheduler,
  50. "_device_id": device_id,
  51. "_platform": str(PLATFORM_ID),
  52. "collect_task_id": str(api_task.get("id", "")),
  53. "snapshot_collect_status": api_task.get("snapshot_collect_status", 1),
  54. "sampling_cycle": "",
  55. "sampling_start_time": "",
  56. "sampling_end_time": "",
  57. "collect_equipment_account_id": "",
  58. "collect_region_id": "",
  59. "collect_equipment_id": "",
  60. "collect_round": 1,
  61. # 入口传入的账号信息,spider 可复用避免重复查 DB
  62. "account_phone": device_id,
  63. "account_password": account_password,
  64. "account_name": account_name,
  65. }
  66. return task_dict
  67. def _report_progress(scheduler, task_id, is_finished, crawled_count=0,
  68. current_page=0, total_pages=0,
  69. exception_type=None, remark=None):
  70. """通过调度器 API 上报任务进度。"""
  71. data = {
  72. "task_id": task_id,
  73. "platform": str(PLATFORM_ID),
  74. "username": scheduler.username,
  75. "current_page": current_page,
  76. "is_finished": is_finished,
  77. "total_pages": total_pages,
  78. }
  79. if crawled_count:
  80. data["crawled_count"] = crawled_count
  81. if exception_type is not None:
  82. data["exception_type"] = exception_type
  83. if remark:
  84. data["remark"] = remark
  85. try:
  86. scheduler.post_report(data)
  87. logger.info("任务状态上报成功: is_finished=%s, crawled_count=%s",
  88. is_finished, crawled_count)
  89. except Exception as e:
  90. logger.warning("任务状态上报失败: %s", e)
  91. def run_one_task(scheduler, device_id, account_password, account_name):
  92. """拉取一个任务,根据 snapshot_collect_status 选 spider 执行并上报。"""
  93. # 拉取任务
  94. api_task = scheduler.get_task()
  95. if not api_task:
  96. logger.info("%s暂无任务", PLATFORM_NAME)
  97. return
  98. task_id = api_task.get("id", "")
  99. task_dict = _build_task_dict(api_task, device_id, account_password,
  100. account_name, scheduler)
  101. # 打印任务信息(隐藏不可序列化的 scheduler)
  102. log_dict = {k: v for k, v in task_dict.items() if k != "scheduler"}
  103. logger.info("拉取到任务: %s", json.dumps(log_dict, ensure_ascii=False))
  104. # 根据 snapshot_collect_status 决定走普通爬虫还是快照爬虫
  105. snapshot_status = task_dict.get("snapshot_collect_status", 1)
  106. if snapshot_status == 0:
  107. spider_cls = YaoShiBangSnapshot
  108. logger.info("任务 %s 启用快照模式", task_id)
  109. else:
  110. spider_cls = YsbSpider
  111. logger.info("任务 %s 使用普通模式", task_id)
  112. # 执行爬虫
  113. result = spider_cls(task_dict).run()
  114. if len(result) == 3:
  115. crawl_count, is_success, total_pages = result
  116. else:
  117. crawl_count, is_success = result
  118. total_pages = 0
  119. # 飞书通知
  120. send_text(
  121. f"{time.strftime('%Y-%m-%d %H:%M:%S')} 通知:\n"
  122. f"平台: {PLATFORM_NAME}, 药品: {task_dict.get('product_name')}, "
  123. f"爬取数据: {crawl_count}条"
  124. )
  125. # 上报任务结果
  126. if is_success:
  127. _report_progress(scheduler, task_id, is_finished=1,
  128. crawled_count=crawl_count,
  129. current_page=total_pages, total_pages=total_pages)
  130. else:
  131. _report_progress(scheduler, task_id, is_finished=0,
  132. crawled_count=crawl_count,
  133. current_page=total_pages, total_pages=total_pages)
  134. if __name__ == "__main__":
  135. # 1. 获取账号
  136. result = get_device_id_from_db()
  137. if result:
  138. device_id, account_password, account_name = result
  139. else:
  140. device_id, account_password, account_name = "YSB", None, None
  141. # 2. 初始化调度器(心跳 + 拉任务 + 上报)
  142. scheduler = CrawlerScheduler(
  143. DEVICE_ID=device_id,
  144. platform=str(PLATFORM_ID),
  145. )
  146. scheduler.start()
  147. time.sleep(2) # 等待首次心跳完成
  148. logger.info("调度器已启动,device_id=%s,心跳已就绪", device_id)
  149. # 3. 循环执行
  150. idle_seconds = random.randint(180, 300)
  151. logger.info("循环任务已启动,平台=%s,每轮间隔 %s 秒", PLATFORM_NAME, idle_seconds)
  152. while True:
  153. try:
  154. logger.info("开始执行%s爬虫任务", PLATFORM_NAME)
  155. run_one_task(scheduler, device_id, account_password, account_name)
  156. logger.info("%s爬虫任务执行完成", PLATFORM_NAME)
  157. except Exception as e:
  158. logger.error("%s爬虫任务执行失败: %s", PLATFORM_NAME, e, exc_info=True)
  159. time.sleep(idle_seconds)