start_run_ele.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. """
  2. 饿了么闪购 — 调度任务入口(模仿 D:\\drug\\pdd\\start_run_pdd.py)
  3. 从调度系统拉取任务执行,不再手动传参。
  4. 用法: python start_run_ele.py <ADB序列号>
  5. (adb devices 查看序列号;不传参数启动会直接报错退出)
  6. 流程: 心跳注册 → 循环领任务 → step1打开App → step2搜索(品牌+药品名合并)
  7. → step3遍历采集(说明书/快照/资质/入库) → 上报结果 → 等600秒 → 领下一个任务
  8. """
  9. import json
  10. import random
  11. import re
  12. import sys
  13. import time
  14. from datetime import datetime
  15. from pathlib import Path
  16. import requests
  17. import uiautomator2
  18. sys.path.insert(0, str(Path(__file__).parent))
  19. from steps.executor import SafeExecutor
  20. from main1 import step1_open_app, step2_search, step3_swipe_and_enter
  21. from commons.scheduler import CrawlerScheduler
  22. # ── 配置 ────────────────────────────────────────────────
  23. PLATFORM_ELE = 12 # 饿了么平台编号(与 db.py 一致)
  24. LOOP_INTERVAL_MIN = 480 # 任务/无任务等待间隔下限(秒)
  25. LOOP_INTERVAL_MAX = 720 # 上限;实际在两者间随机取(拟人,避免固定节奏被建模)
  26. START_HOUR = 6 # 采集开始时间(该点之前不领任务;心跳保持在线)
  27. API_HEADERS = {'X-Crawler-Token': 'zhijiayun_crawler_2026'}
  28. def loop_interval() -> int:
  29. """本轮等待秒数:480~720 随机"""
  30. return random.randint(LOOP_INTERVAL_MIN, LOOP_INTERVAL_MAX)
  31. def wait_until_start_hour():
  32. """当前时间早于 START_HOUR 点 → 等到整点再领任务(心跳线程不受影响,保持在线)"""
  33. now = datetime.now()
  34. if now.hour >= START_HOUR:
  35. return
  36. target = now.replace(hour=START_HOUR, minute=0, second=0, microsecond=0)
  37. wait_s = (target - now).total_seconds()
  38. print(f"[调度] 当前 {now.strftime('%H:%M')},未到 {START_HOUR} 点开始时间,等待 {wait_s/3600:.1f} 小时...")
  39. if wait_s > 0:
  40. time.sleep(wait_s)
  41. # 设备号通过命令行传入:python start_run_ele.py <ADB序列号>
  42. DEVICE_ID = sys.argv[1].strip() if len(sys.argv) > 1 else ""
  43. APP_PACKAGE = "me.ele"
  44. def close_app(device_id: str):
  45. """任务结束(验证码超限停止/封号/正常完成/异常)后关闭饿了么App。
  46. 下个任务 step1 会重新拉起;关闭失败不影响主流程。"""
  47. try:
  48. uiautomator2.connect(device_id).app_stop(APP_PACKAGE)
  49. print(f"[任务] 已关闭App({APP_PACKAGE})")
  50. except Exception as e:
  51. print(f"[任务] 关闭App失败(不影响流程): {e}")
  52. def build_task_payload_from_api(task_dict: dict, device_row: str) -> dict:
  53. """从调度 API 返回的 task dict 构建采集可消费的 payload(照搬 PDD 逻辑)"""
  54. username = device_row
  55. device_id = task_dict['_device_id'] or username
  56. search_key = (
  57. f"{task_dict.get('product_brand', '')}"
  58. f"{task_dict.get('product_name', '')} "
  59. f"{task_dict.get('product_specs', '') or ''}"
  60. ).strip()
  61. # 规格列表:按 | 、 , 分隔(用于规格过滤,照搬 PDD)
  62. spec_raw = task_dict.get('product_specs', '') or ''
  63. spec_list = [s.strip() for s in re.split(r'[|、,,\n\r]+', spec_raw) if s.strip()]
  64. collect_config_info = json.dumps({
  65. "sampling_cycle": task_dict.get('sampling_cycle', ''),
  66. "sampling_start_time": task_dict.get('sampling_start_time', 0),
  67. "sampling_end_time": task_dict.get('sampling_end_time', 0),
  68. }, ensure_ascii=False)
  69. return {
  70. "task_id": task_dict.get('id'),
  71. "search_key": search_key,
  72. "title_key": task_dict.get('product_name'),
  73. "brand": task_dict.get('product_brand', ''),
  74. "spec_list": spec_list,
  75. "product_specs": spec_raw,
  76. "enterprise_id": task_dict.get('company_id'),
  77. "collect_round": task_dict.get('collect_round'),
  78. "collect_equipment_account_id": task_dict.get('collect_equipment_account_id', 1),
  79. "collect_region_id": task_dict.get('collect_region_id', 1),
  80. "current_page": task_dict.get('current_page') or 0, # 调度重派时给的页码(跨设备接力恢复)
  81. "collect_config_info": collect_config_info,
  82. "username": username,
  83. "device_id": device_id,
  84. "device_row": device_row,
  85. }
  86. def run_task_worker(task_payload: dict, scheduler: CrawlerScheduler):
  87. """执行单个任务:打开App → 搜索 → 遍历采集 → 上报结果"""
  88. task_id = task_payload["task_id"]
  89. device_id = task_payload["device_id"]
  90. search_key = str(task_payload.get("search_key", "")).strip()
  91. keyword = str(task_payload.get("title_key") or search_key).strip()
  92. brand = str(task_payload.get("brand", "")).strip()
  93. report = {
  94. "task_id": task_id,
  95. "platform": scheduler.platform,
  96. "username": scheduler.username,
  97. }
  98. try:
  99. print(f"[任务 {task_id}] 开始执行,设备: {device_id},搜索: {search_key}")
  100. # 重置停止标志(上个任务的验证码/封号停止不能污染本任务)
  101. _m = sys.modules.get("main1")
  102. if _m is not None:
  103. for _flag, _val in (("CAPTCHA_ABORTED", False), ("CAPTCHA_ABORT_REASON", ""),
  104. ("ACCOUNT_ABORTED", False), ("CURRENT_PAGE", 0),
  105. ("WHITE_SCREEN_REASSIGN", False), ("WHITE_SCREEN_REASSIGN_REASON", ""),
  106. ("_LAST_SKIP_WHITE", False)):
  107. if hasattr(_m, _flag):
  108. setattr(_m, _flag, _val)
  109. scheduler.limit_reached = False # 上个任务的限额错误不污染本任务
  110. scheduler.limit_msg = ""
  111. ex = SafeExecutor(device_id)
  112. # step1: 打开 App
  113. if not step1_open_app(ex):
  114. print(f"[任务 {task_id}] step1 打开App失败")
  115. if getattr(sys.modules.get("main1"), "ACCOUNT_ABORTED", False):
  116. # 登录页xpath存在 = 账号被踢/封号
  117. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  118. "crawled_count": 0, "current_page": 0, "remark": "账号被踢/封号"})
  119. print(f"[任务 {task_id}] 账号被踢/封号(登录页),回告调度停止")
  120. else:
  121. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 1, "crawled_count": 0})
  122. scheduler.post_report(report)
  123. return
  124. # step2: 搜索(品牌+药品名合并后的搜索词)
  125. if not step2_search(ex, search_key):
  126. print(f"[任务 {task_id}] step2 搜索失败")
  127. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 2, "crawled_count": 0})
  128. scheduler.post_report(report)
  129. return
  130. # step3: 遍历采集(品牌+药品名过滤,任务字段随结果入库;逐页回告 current_page)
  131. visited = step3_swipe_and_enter(ex, keyword, brand, task_payload, scheduler)
  132. _main1_mod = sys.modules.get("main1")
  133. if getattr(_main1_mod, "ACCOUNT_ABORTED", False):
  134. # 账号被踢/封号:回告调度,需重新指派/换账号
  135. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  136. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0), "remark": "账号被踢/封号"})
  137. print(f"[任务 {task_id}] 账号被踢/封号,回告调度停止")
  138. elif getattr(_main1_mod, "CAPTCHA_ABORTED", False):
  139. # 验证码频率超限(一天≥8次等):回告调度
  140. _reason = getattr(_main1_mod, "CAPTCHA_ABORT_REASON", "") or "验证码频繁出现"
  141. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  142. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0), "remark": _reason})
  143. print(f"[任务 {task_id}] {_reason},回告调度停止")
  144. elif getattr(_main1_mod, "WHITE_SCREEN_REASSIGN", False):
  145. # 商品页反复白屏(重启恢复超限):回告调度重派给其他设备
  146. _reason = getattr(_main1_mod, "WHITE_SCREEN_REASSIGN_REASON", "") or "页面持续白屏"
  147. report.update({"is_finished": 0, "need_reassign": 1,
  148. "exception_type": getattr(_main1_mod, "WHITE_EXCEPTION_TYPE", 5),
  149. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0),
  150. "remark": _reason})
  151. print(f"[任务 {task_id}] {_reason},回告调度重派")
  152. else:
  153. report.update({"is_finished": 1, "need_reassign": 0, "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0)})
  154. if getattr(scheduler, "limit_reached", False):
  155. # 平台已释放任务(限额等)→ 不再回告(回告只会再次返回error)
  156. print(f"[任务 {task_id}] 平台已释放任务({getattr(scheduler, 'limit_msg', '')}),跳过最终回告")
  157. else:
  158. scheduler.post_report(report)
  159. print(f"[任务 {task_id}] 执行完成,采集 {len(visited)} 条")
  160. except Exception as e:
  161. print(f"[任务 {task_id}] 执行异常: {e}")
  162. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5, "crawled_count": 0, "current_page": getattr(sys.modules.get("main1"), "CURRENT_PAGE", 0)})
  163. scheduler.post_report(report)
  164. finally:
  165. # 无论哪种方式结束(验证码超限/封号/正常完成/异常),都关掉App再进入等待
  166. close_app(device_id)
  167. def main():
  168. if not DEVICE_ID:
  169. raise SystemExit("用法: python start_run_ele.py <ADB序列号> (adb devices 查看序列号)")
  170. scheduler = CrawlerScheduler(DEVICE_ID, str(PLATFORM_ELE))
  171. scheduler.start()
  172. time.sleep(2)
  173. print(f"饿了么闪购采集器启动,设备: {DEVICE_ID},等待间隔 {LOOP_INTERVAL_MIN}~{LOOP_INTERVAL_MAX} 秒随机")
  174. while True:
  175. try:
  176. wait_until_start_hour() # 早于6点不领任务(心跳线程保持在线)
  177. # 先带设备id发一次心跳(注册/激活账号),再领任务(PDD 同款做法)
  178. requests.post(
  179. scheduler.heartbeat_url,
  180. json={"platform": scheduler.platform, "username": scheduler.username},
  181. headers=API_HEADERS,
  182. timeout=5,
  183. verify=False,
  184. )
  185. task = scheduler.get_task()
  186. if task is None:
  187. _iv = loop_interval()
  188. print(f"当前没有可执行的任务,{_iv} 秒后重试...")
  189. time.sleep(_iv)
  190. continue
  191. print(f"获取到任务: task_id={task.get('id')}, product={task.get('product_name')}")
  192. task['_device_row'] = DEVICE_ID
  193. task['_device_id'] = DEVICE_ID
  194. run_task_worker(build_task_payload_from_api(task, DEVICE_ID), scheduler)
  195. _iv = loop_interval()
  196. print(f"任务完成,等待 {_iv} 秒后开始下一轮...")
  197. time.sleep(_iv)
  198. except Exception as e:
  199. print(f"本轮任务异常: {e}")
  200. time.sleep(loop_interval())
  201. if __name__ == '__main__':
  202. main()
  203. # d = uiautomator2.connect("RG5LFYT8UKK7BI95")
  204. # d.screenshot('screenshot.png')