start_run_ele.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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. from commons import err_log
  23. # ── 配置 ────────────────────────────────────────────────
  24. PLATFORM_ELE = 12 # 饿了么平台编号(与 db.py 一致)
  25. LOOP_INTERVAL_MIN = 480 # 任务/无任务等待间隔下限(秒)
  26. LOOP_INTERVAL_MAX = 720 # 上限;实际在两者间随机取(拟人,避免固定节奏被建模)
  27. START_HOUR = 6 # 采集开始时间(该点之前不领任务;心跳保持在线)
  28. API_HEADERS = {'X-Crawler-Token': 'zhijiayun_crawler_2026'}
  29. def loop_interval() -> int:
  30. """本轮等待秒数:480~720 随机"""
  31. return random.randint(LOOP_INTERVAL_MIN, LOOP_INTERVAL_MAX)
  32. def wait_until_start_hour():
  33. """当前时间早于 START_HOUR 点 → 等到整点再领任务(心跳线程不受影响,保持在线)"""
  34. now = datetime.now()
  35. if now.hour >= START_HOUR:
  36. return
  37. target = now.replace(hour=START_HOUR, minute=0, second=0, microsecond=0)
  38. wait_s = (target - now).total_seconds()
  39. print(f"[调度] 当前 {now.strftime('%H:%M')},未到 {START_HOUR} 点开始时间,等待 {wait_s/3600:.1f} 小时...")
  40. if wait_s > 0:
  41. time.sleep(wait_s)
  42. # 设备号通过命令行传入:python start_run_ele.py <ADB序列号>
  43. DEVICE_ID = sys.argv[1].strip() if len(sys.argv) > 1 else ""
  44. APP_PACKAGE = "me.ele"
  45. # ── 账号压力统计(跨任务累计, 按天清零; 用于分析风控封号阈值) ──
  46. _PRESSURE = {"day": None, "tasks": 0, "crawled": 0}
  47. def _count_captcha_today(device_id: str) -> int:
  48. """当日验证码次数(main1 的验证码日志按日期开头计数)"""
  49. try:
  50. m = sys.modules.get("main1")
  51. if m is None or not hasattr(m, "_captcha_log_path"):
  52. return 0
  53. path = m._captcha_log_path(device_id)
  54. today = time.strftime("%Y-%m-%d")
  55. with open(path, encoding="utf-8") as f:
  56. return sum(1 for line in f if line.startswith(today))
  57. except Exception:
  58. return 0
  59. def close_app(device_id: str):
  60. """任务结束(验证码超限停止/封号/正常完成/异常)后关闭饿了么App。
  61. 下个任务 step1 会重新拉起;关闭失败不影响主流程。"""
  62. try:
  63. uiautomator2.connect(device_id).app_stop(APP_PACKAGE)
  64. print(f"[任务] 已关闭App({APP_PACKAGE})")
  65. except Exception as e:
  66. print(f"[任务] 关闭App失败(不影响流程): {e}")
  67. def build_task_payload_from_api(task_dict: dict, device_row: str) -> dict:
  68. """从调度 API 返回的 task dict 构建采集可消费的 payload(照搬 PDD 逻辑)"""
  69. username = device_row
  70. device_id = task_dict['_device_id'] or username
  71. search_key = (
  72. f"{task_dict.get('product_brand', '')}"
  73. f"{task_dict.get('product_name', '')} "
  74. f"{task_dict.get('product_specs', '') or ''}"
  75. ).strip()
  76. # 规格列表:按 | 、 , 分隔(用于规格过滤,照搬 PDD)
  77. spec_raw = task_dict.get('product_specs', '') or ''
  78. spec_list = [s.strip() for s in re.split(r'[|、,,\n\r]+', spec_raw) if s.strip()]
  79. collect_config_info = json.dumps({
  80. "sampling_cycle": task_dict.get('sampling_cycle', ''),
  81. "sampling_start_time": task_dict.get('sampling_start_time', 0),
  82. "sampling_end_time": task_dict.get('sampling_end_time', 0),
  83. }, ensure_ascii=False)
  84. return {
  85. "task_id": task_dict.get('id'),
  86. "search_key": search_key,
  87. "title_key": task_dict.get('product_name'),
  88. "brand": task_dict.get('product_brand', ''),
  89. "spec_list": spec_list,
  90. "product_specs": spec_raw,
  91. "enterprise_id": task_dict.get('company_id'),
  92. "collect_round": task_dict.get('collect_round'),
  93. "collect_equipment_account_id": task_dict.get('collect_equipment_account_id', 1),
  94. "collect_region_id": task_dict.get('collect_region_id', 1),
  95. "current_page": task_dict.get('current_page') or 0, # 调度重派时给的页码(跨设备接力恢复)
  96. "collect_config_info": collect_config_info,
  97. "username": username,
  98. "device_id": device_id,
  99. "device_row": device_row,
  100. }
  101. def run_task_worker(task_payload: dict, scheduler: CrawlerScheduler):
  102. """执行单个任务:打开App → 搜索 → 遍历采集 → 上报结果"""
  103. task_id = task_payload["task_id"]
  104. device_id = task_payload["device_id"]
  105. search_key = str(task_payload.get("search_key", "")).strip()
  106. keyword = str(task_payload.get("title_key") or search_key).strip()
  107. brand = str(task_payload.get("brand", "")).strip()
  108. t0 = time.time()
  109. report = {
  110. "task_id": task_id,
  111. "platform": scheduler.platform,
  112. "username": scheduler.username,
  113. }
  114. try:
  115. print(f"[任务 {task_id}] 开始执行,设备: {device_id},搜索: {search_key}")
  116. # 重置停止标志(上个任务的验证码/封号停止不能污染本任务)
  117. _m = sys.modules.get("main1")
  118. if _m is not None:
  119. for _flag, _val in (("CAPTCHA_ABORTED", False), ("CAPTCHA_ABORT_REASON", ""),
  120. ("ACCOUNT_ABORTED", False), ("CURRENT_PAGE", 0),
  121. ("WHITE_SCREEN_REASSIGN", False), ("WHITE_SCREEN_REASSIGN_REASON", ""),
  122. ("_LAST_SKIP_WHITE", False)):
  123. if hasattr(_m, _flag):
  124. setattr(_m, _flag, _val)
  125. scheduler.limit_reached = False # 上个任务的限额错误不污染本任务
  126. scheduler.limit_msg = ""
  127. ex = SafeExecutor(device_id)
  128. # step1: 打开 App
  129. if not step1_open_app(ex):
  130. print(f"[任务 {task_id}] step1 打开App失败")
  131. _kicked = bool(getattr(sys.modules.get("main1"), "ACCOUNT_ABORTED", False))
  132. err_log.log_error(device_id, "step1_open_app_failed",
  133. message="打开App失败" + ("(账号被踢/封号)" if _kicked else ""),
  134. extra={"task_id": task_id, "search_key": search_key, "account_aborted": _kicked})
  135. if getattr(sys.modules.get("main1"), "ACCOUNT_ABORTED", False):
  136. # 登录页xpath存在 = 账号被踢/封号
  137. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  138. "crawled_count": 0, "current_page": 0, "remark": "账号被踢/封号"})
  139. print(f"[任务 {task_id}] 账号被踢/封号(登录页),回告调度停止")
  140. else:
  141. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 1, "crawled_count": 0})
  142. scheduler.post_report(report)
  143. return
  144. # step2: 搜索(品牌+药品名合并后的搜索词)
  145. if not step2_search(ex, search_key):
  146. print(f"[任务 {task_id}] step2 搜索失败")
  147. err_log.log_error(device_id, "step2_search_failed", message="搜索失败",
  148. extra={"task_id": task_id, "search_key": search_key})
  149. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 2, "crawled_count": 0})
  150. scheduler.post_report(report)
  151. return
  152. # step3: 遍历采集(品牌+药品名过滤,任务字段随结果入库;逐页回告 current_page)
  153. visited = step3_swipe_and_enter(ex, keyword, brand, task_payload, scheduler)
  154. _main1_mod = sys.modules.get("main1")
  155. if getattr(_main1_mod, "ACCOUNT_ABORTED", False):
  156. # 账号被踢/封号:回告调度,需重新指派/换账号
  157. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  158. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0), "remark": "账号被踢/封号"})
  159. print(f"[任务 {task_id}] 账号被踢/封号,回告调度停止")
  160. elif getattr(_main1_mod, "CAPTCHA_ABORTED", False):
  161. # 验证码频率超限(一天≥8次等):回告调度
  162. _reason = getattr(_main1_mod, "CAPTCHA_ABORT_REASON", "") or "验证码频繁出现"
  163. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5,
  164. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0), "remark": _reason})
  165. print(f"[任务 {task_id}] {_reason},回告调度停止")
  166. elif getattr(_main1_mod, "WHITE_SCREEN_REASSIGN", False):
  167. # 商品页反复白屏(重启恢复超限):回告调度重派给其他设备
  168. _reason = getattr(_main1_mod, "WHITE_SCREEN_REASSIGN_REASON", "") or "页面持续白屏"
  169. report.update({"is_finished": 0, "need_reassign": 1,
  170. "exception_type": getattr(_main1_mod, "WHITE_EXCEPTION_TYPE", 5),
  171. "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0),
  172. "remark": _reason})
  173. print(f"[任务 {task_id}] {_reason},回告调度重派")
  174. else:
  175. report.update({"is_finished": 1, "need_reassign": 0, "crawled_count": len(visited), "current_page": getattr(_main1_mod, "CURRENT_PAGE", 0)})
  176. if getattr(scheduler, "limit_reached", False):
  177. # 平台已释放任务(限额等)→ 不再回告(回告只会再次返回error)
  178. print(f"[任务 {task_id}] 平台已释放任务({getattr(scheduler, 'limit_msg', '')}),跳过最终回告")
  179. else:
  180. scheduler.post_report(report)
  181. print(f"[任务 {task_id}] 执行完成,采集 {len(visited)} 条")
  182. except Exception as e:
  183. print(f"[任务 {task_id}] 执行异常: {e}")
  184. err_log.log_error(device_id, "task_exception", exc=e,
  185. extra={"task_id": task_id, "search_key": search_key, "keyword": keyword})
  186. report.update({"is_finished": 0, "need_reassign": 1, "exception_type": 5, "crawled_count": 0, "current_page": getattr(sys.modules.get("main1"), "CURRENT_PAGE", 0)})
  187. scheduler.post_report(report)
  188. finally:
  189. # ── 账号压力日志: 每任务一条累计统计, 与 account_kicked 事件对照分析风控阈值 ──
  190. try:
  191. today = time.strftime("%Y-%m-%d")
  192. if _PRESSURE["day"] != today:
  193. _PRESSURE["day"] = today
  194. _PRESSURE["tasks"] = 0
  195. _PRESSURE["crawled"] = 0
  196. _PRESSURE["tasks"] += 1
  197. _PRESSURE["crawled"] += report.get("crawled_count", 0) or 0
  198. if report.get("is_finished") == 1:
  199. tag = "finished"
  200. elif "封号" in str(report.get("remark", "")):
  201. tag = "account_kicked"
  202. elif "验证码" in str(report.get("remark", "")):
  203. tag = "captcha_limit"
  204. elif getattr(scheduler, "limit_reached", False):
  205. tag = "limit_reached"
  206. else:
  207. tag = "failed"
  208. err_log.log_pressure(device_id, {
  209. "task_id": task_id, "result": tag,
  210. "crawled_today": _PRESSURE["crawled"], "tasks_today": _PRESSURE["tasks"],
  211. "captcha_today": _count_captcha_today(device_id),
  212. "runtime_min": round((time.time() - t0) / 60, 1),
  213. "search": search_key[:20],
  214. })
  215. except Exception:
  216. pass
  217. # 无论哪种方式结束(验证码超限/封号/正常完成/异常),都关掉App再进入等待
  218. close_app(device_id)
  219. def main():
  220. if not DEVICE_ID:
  221. raise SystemExit("用法: python start_run_ele.py <ADB序列号> (adb devices 查看序列号)")
  222. scheduler = CrawlerScheduler(DEVICE_ID, str(PLATFORM_ELE))
  223. scheduler.start()
  224. time.sleep(2)
  225. print(f"饿了么闪购采集器启动,设备: {DEVICE_ID},等待间隔 {LOOP_INTERVAL_MIN}~{LOOP_INTERVAL_MAX} 秒随机")
  226. while True:
  227. try:
  228. wait_until_start_hour() # 早于6点不领任务(心跳线程保持在线)
  229. # 先带设备id发一次心跳(注册/激活账号),再领任务(PDD 同款做法)
  230. requests.post(
  231. scheduler.heartbeat_url,
  232. json={"platform": scheduler.platform, "username": scheduler.username},
  233. headers=API_HEADERS,
  234. timeout=5,
  235. verify=False,
  236. )
  237. task = scheduler.get_task()
  238. if task is None:
  239. _iv = loop_interval()
  240. print(f"当前没有可执行的任务,{_iv} 秒后重试...")
  241. time.sleep(_iv)
  242. continue
  243. print(f"获取到任务: task_id={task.get('id')}, product={task.get('product_name')}")
  244. task['_device_row'] = DEVICE_ID
  245. task['_device_id'] = DEVICE_ID
  246. # 限额探针:执行前先发一次进度回告(页码=调度给的接力页,crawled=0)。
  247. # 平台返回 error(如今日限额已到/任务已释放)→ 直接跳过本轮,
  248. # 省掉开App/搜索/跨设备接力滑页的无用功;返回 success 则照常执行。
  249. scheduler.limit_reached = False
  250. scheduler.limit_msg = ""
  251. scheduler.post_report({
  252. "task_id": task.get('id'),
  253. "platform": scheduler.platform,
  254. "username": scheduler.username,
  255. "is_finished": 0,
  256. "need_reassign": 0,
  257. "current_page": task.get('current_page') or 0,
  258. "crawled_count": 0,
  259. })
  260. if scheduler.limit_reached:
  261. print(f"[探针] 平台仍限额({scheduler.limit_msg}),本轮跳过执行")
  262. time.sleep(loop_interval())
  263. continue
  264. run_task_worker(build_task_payload_from_api(task, DEVICE_ID), scheduler)
  265. _iv = loop_interval()
  266. print(f"任务完成,等待 {_iv} 秒后开始下一轮...")
  267. time.sleep(_iv)
  268. except Exception as e:
  269. print(f"本轮任务异常: {e}")
  270. err_log.log_error(DEVICE_ID, "loop_exception", exc=e)
  271. time.sleep(loop_interval())
  272. if __name__ == '__main__':
  273. main()
  274. # d = uiautomator2.connect("RG5LFYT8UKK7BI95")
  275. # d.screenshot('screenshot.png')