start_run_yaoshibang_v2.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """药师帮统一入口 — 根据任务 snapshot_collect_status 字段自动切换普通/快照模式。
  2. 支持账号池轮换:一个账号被锁自动换下一个。
  3. """
  4. import random
  5. import time
  6. import json
  7. from spiders.yaoshibang.ysbang_crawl import YsbSpider
  8. from spiders.yaoshibang.ysb_snapshot_crawl import YaoShiBangSnapshot
  9. from commons.scheduler import CrawlerScheduler
  10. from commons.conn_mysql import MySQLPoolOn2
  11. from commons.Logger import logger
  12. from commons.feishu_webhook import send_text
  13. PLATFORM_NAME = "药师帮"
  14. PLATFORM_ID = 5
  15. # 账号池
  16. _account_pool = []
  17. _account_index = 0
  18. def _load_account_pool():
  19. """从 DB 加载所有可用药师帮账号"""
  20. global _account_pool, _account_index
  21. db = MySQLPoolOn2()
  22. sql = """SELECT `name`, `phone`, `password` FROM `accounts_platform`
  23. WHERE `platform`=%s AND `status`=1 AND `equipment_id`=3
  24. ORDER BY `cookie_timestamp` ASC"""
  25. rows = db.select_data(sql, (PLATFORM_ID,))
  26. if not rows:
  27. logger.error("无可用药师帮账号(platform=%s, status=1)", PLATFORM_ID)
  28. return False
  29. _account_pool = [(r["phone"], r["password"], r["name"]) for r in rows]
  30. _account_index = 0
  31. logger.info("从 DB 加载 %s 个药师帮账号", len(_account_pool))
  32. return True
  33. def _get_current_account():
  34. """获取当前账号,池空则重新加载"""
  35. global _account_pool, _account_index
  36. if not _account_pool:
  37. if not _load_account_pool():
  38. return None
  39. return _account_pool[_account_index]
  40. def _rotate_account():
  41. """换下一个账号,返回 (phone, password, name)"""
  42. global _account_index
  43. if not _account_pool:
  44. if not _load_account_pool():
  45. return None
  46. _account_index = (_account_index + 1) % len(_account_pool)
  47. new_acc = _account_pool[_account_index]
  48. logger.warning("账号被锁,切换: %s → %s (第%s/%s个)",
  49. _account_pool[(_account_index - 1) % len(_account_pool)][2],
  50. new_acc[2], _account_index + 1, len(_account_pool))
  51. return new_acc
  52. def _build_task_dict(api_task, device_id, account_password, account_name, scheduler):
  53. """将调度 API 返回的原始任务字段组装为 spider 需要的 task_dict。"""
  54. current_page = api_task.get("current_page", 0)
  55. start_page = current_page if current_page > 0 else 1
  56. task_dict = {
  57. # 新 API 字段
  58. "id": api_task.get("id"),
  59. "company_id": api_task.get("company_id"),
  60. "product_name": api_task.get("product_name", ""),
  61. "product_specs": api_task.get("product_specs", ""),
  62. "product_brand": api_task.get("product_brand", ""),
  63. "product_keyword": api_task.get("product_keyword", ""),
  64. "current_page": current_page,
  65. "start_offset": api_task.get("start_offset"),
  66. # 兼容旧字段
  67. "start_page": start_page,
  68. "end_page": 0,
  69. # spider 逐页回告用
  70. "scheduler": scheduler,
  71. "_device_id": device_id,
  72. "_platform": str(PLATFORM_ID),
  73. "collect_task_id": str(api_task.get("id", "")),
  74. "snapshot_collect_status": api_task.get("snapshot_collect_status"),
  75. "sampling_cycle": "",
  76. "sampling_start_time": "",
  77. "sampling_end_time": "",
  78. "collect_equipment_account_id": "",
  79. "collect_region_id": "",
  80. "collect_equipment_id": "",
  81. "collect_round": 1,
  82. # 入口传入的账号信息,spider 可复用避免重复查 DB
  83. "account_phone": device_id,
  84. "account_password": account_password,
  85. "account_name": account_name,
  86. }
  87. return task_dict
  88. def _report_progress(scheduler, task_id, is_finished, crawled_count=0,
  89. current_page=0, total_pages=0,
  90. exception_type=None, remark=None):
  91. """通过调度器 API 上报任务进度。"""
  92. data = {
  93. "task_id": task_id,
  94. "platform": str(PLATFORM_ID),
  95. "username": scheduler.username,
  96. "current_page": current_page,
  97. "is_finished": is_finished,
  98. "total_pages": total_pages,
  99. "crawled_count": crawled_count,
  100. }
  101. if exception_type is not None:
  102. data["exception_type"] = exception_type
  103. if remark:
  104. data["remark"] = remark
  105. try:
  106. scheduler.post_report(data)
  107. logger.info("任务状态上报成功: is_finished=%s, crawled_count=%s",
  108. is_finished, crawled_count)
  109. except Exception as e:
  110. logger.warning("任务状态上报失败: %s", e)
  111. if __name__ == "__main__":
  112. # 1. 加载账号池
  113. if not _load_account_pool():
  114. logger.error("无可用药师帮账号,退出")
  115. exit(1)
  116. idle_seconds = random.randint(180, 300)
  117. empty_count = 0 # 连续拉不到任务的次数
  118. max_empty_before_rotate = 2 # 连续 2 次拉不到就换号
  119. # 2. 初始化调度器
  120. phone, password, name = _get_current_account()
  121. scheduler = CrawlerScheduler(DEVICE_ID=phone, platform=str(PLATFORM_ID))
  122. scheduler.start()
  123. time.sleep(2)
  124. logger.info("调度器已启动,当前账号: %s (phone=%s)", name, phone)
  125. logger.info("循环任务已启动,平台=%s,每轮间隔 %s 秒", PLATFORM_NAME, idle_seconds)
  126. while True:
  127. try:
  128. logger.info("开始执行%s爬虫任务", PLATFORM_NAME)
  129. api_task = scheduler.get_task()
  130. if not api_task:
  131. empty_count += 1
  132. logger.info("%s暂无任务 (连续%s次),等待 %s 秒后重试",
  133. PLATFORM_NAME, empty_count, idle_seconds)
  134. if empty_count >= max_empty_before_rotate:
  135. # 换个号试试
  136. new_acc = _rotate_account()
  137. if new_acc:
  138. scheduler.stop()
  139. phone, password, name = new_acc
  140. scheduler = CrawlerScheduler(DEVICE_ID=phone, platform=str(PLATFORM_ID))
  141. scheduler.start()
  142. empty_count = 0
  143. logger.info("已切换到账号: %s", name)
  144. time.sleep(idle_seconds)
  145. continue
  146. empty_count = 0 # 拉到任务了,重置
  147. task_id = api_task.get("id", "")
  148. task_dict = _build_task_dict(api_task, phone, password, name, scheduler)
  149. log_dict = {k: v for k, v in task_dict.items() if k != "scheduler"}
  150. logger.info("拉取到任务: %s", json.dumps(log_dict, ensure_ascii=False))
  151. # 根据 snapshot_collect_status 选爬虫
  152. snapshot_status = task_dict.get("snapshot_collect_status", 0)
  153. if not snapshot_status:
  154. spider_cls = YaoShiBangSnapshot
  155. logger.info("任务 %s 启用快照模式", task_id)
  156. else:
  157. spider_cls = YsbSpider
  158. logger.info("任务 %s 使用普通模式", task_id)
  159. result = spider_cls(task_dict).run()
  160. if len(result) == 3:
  161. crawl_count, is_success, total_pages = result
  162. else:
  163. crawl_count, is_success = result
  164. total_pages = 0
  165. send_text(
  166. f"{time.strftime('%Y-%m-%d %H:%M:%S')} 通知:\n"
  167. f"平台: {PLATFORM_NAME}, 药品: {task_dict.get('product_name')}, "
  168. f"爬取数据: {crawl_count}条"
  169. )
  170. if is_success:
  171. _report_progress(scheduler, task_id, is_finished=1,
  172. crawled_count=crawl_count,
  173. current_page=total_pages, total_pages=total_pages)
  174. else:
  175. _report_progress(scheduler, task_id, is_finished=0,
  176. crawled_count=crawl_count,
  177. current_page=total_pages, total_pages=total_pages)
  178. except Exception as e:
  179. logger.error("%s爬虫任务执行失败: %s", PLATFORM_NAME, e, exc_info=True)
  180. time.sleep(idle_seconds)