input-keyword.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env python3
  2. """关键词搜索流程:先读 ``save/project-config``;若有已保存坐标则跳过对应 OCR/模板匹配,否则现算并写回。
  3. 若输入框已有文案则 ``clear-input.start``;再 ``find_search_input.start``(未命中抛错);
  4. 未清空时 ``move-mouse-to-pos.start`` 到占位文案;再 ``input-word-by-word.start`` → ``press-search-btn.start`` → ``then``。
  5. 新算出的坐标会合并写入配置(见 ``workplace/config-save-read.py``)。"""
  6. from __future__ import annotations
  7. import importlib.util
  8. import sys
  9. from collections.abc import Callable
  10. from pathlib import Path
  11. from typing import Any
  12. _ROOT = Path(__file__).resolve().parent.parent.parent
  13. if str(_ROOT) not in sys.path:
  14. sys.path.insert(0, str(_ROOT))
  15. from workplace import pyautogui as workplace_pyautogui # noqa: E402
  16. from workplace.screenshot import PillowImageGrabFullScreenScreenshotCaptureSaver # noqa: E402
  17. def _load_find_search_input_module():
  18. path = Path(__file__).resolve().parent / "find_search_input.py"
  19. spec = importlib.util.spec_from_file_location("workplace_find_search_input", path)
  20. if spec is None or spec.loader is None:
  21. raise ImportError(f"Cannot load {path}")
  22. mod = importlib.util.module_from_spec(spec)
  23. spec.loader.exec_module(mod)
  24. return mod
  25. def _load_move_mouse_to_pos_module():
  26. path = Path(__file__).resolve().parent.parent / "move-mouse-to-pos.py"
  27. spec = importlib.util.spec_from_file_location("workplace_move_mouse_to_pos", path)
  28. if spec is None or spec.loader is None:
  29. raise ImportError(f"Cannot load {path}")
  30. mod = importlib.util.module_from_spec(spec)
  31. spec.loader.exec_module(mod)
  32. return mod
  33. def _load_clear_input_module():
  34. path = Path(__file__).resolve().parent / "clear-input.py"
  35. spec = importlib.util.spec_from_file_location("workplace_clear_input", path)
  36. if spec is None or spec.loader is None:
  37. raise ImportError(f"Cannot load {path}")
  38. mod = importlib.util.module_from_spec(spec)
  39. spec.loader.exec_module(mod)
  40. return mod
  41. def _load_input_word_by_word_module():
  42. path = Path(__file__).resolve().parent / "input-word-by-word.py"
  43. spec = importlib.util.spec_from_file_location("workplace_input_word_by_word", path)
  44. if spec is None or spec.loader is None:
  45. raise ImportError(f"Cannot load {path}")
  46. mod = importlib.util.module_from_spec(spec)
  47. spec.loader.exec_module(mod)
  48. return mod
  49. def _load_press_search_btn_module():
  50. path = Path(__file__).resolve().parent / "press-search-btn.py"
  51. spec = importlib.util.spec_from_file_location("workplace_press_search_btn", path)
  52. if spec is None or spec.loader is None:
  53. raise ImportError(f"Cannot load {path}")
  54. mod = importlib.util.module_from_spec(spec)
  55. spec.loader.exec_module(mod)
  56. return mod
  57. def _load_config_save_read_module():
  58. path = Path(__file__).resolve().parent.parent / "config-save-read.py"
  59. spec = importlib.util.spec_from_file_location("workplace_config_save_read", path)
  60. if spec is None or spec.loader is None:
  61. raise ImportError(f"Cannot load {path}")
  62. mod = importlib.util.module_from_spec(spec)
  63. spec.loader.exec_module(mod)
  64. return mod
  65. def _wait_pointer_at(
  66. target_screen_x: int,
  67. target_screen_y: int,
  68. *,
  69. tolerance_pixels: int = 3,
  70. ) -> None:
  71. while True:
  72. current_pointer_x, current_pointer_y = workplace_pyautogui.raw.position()
  73. if (
  74. abs(int(current_pointer_x) - target_screen_x) <= tolerance_pixels
  75. and abs(int(current_pointer_y) - target_screen_y) <= tolerance_pixels
  76. ):
  77. return
  78. workplace_pyautogui.sleep_human_pointer_poll_step()
  79. def _pointer_settled_callback(
  80. target_screen_x: int,
  81. target_screen_y: int,
  82. on_settled: Callable[[tuple[int, int]], Any],
  83. ) -> Any:
  84. """等指针落到目标附近后 ``on_settled((x, y))``,类似 Node 里异步完成后的 callback。"""
  85. _wait_pointer_at(target_screen_x, target_screen_y)
  86. workplace_pyautogui.sleep_human_after_pointer_settled_before_typing()
  87. return on_settled((target_screen_x, target_screen_y))
  88. def init(
  89. page,
  90. keyword: str = "",
  91. *,
  92. project_config: dict[str, Any] | None = None,
  93. then: Callable[[tuple[int, int]], Any] | None = None,
  94. poll_interval_sec: float = 0.35,
  95. ) -> Any:
  96. """
  97. 步骤概览:若已有文案则先清空;再 ``find_search_input`` 模块 ``start`` 一次;未命中则抛错。
  98. 若未清空则移鼠到「搜索小红书」;若刚清空则指针留在清除钮附近;再输入 → 点搜索 → ``then``。
  99. """
  100. find_search_input = _load_find_search_input_module()
  101. move_mouse_to_pos = _load_move_mouse_to_pos_module()
  102. clear_input = _load_clear_input_module()
  103. input_word_by_word = _load_input_word_by_word_module()
  104. press_search_btn = _load_press_search_btn_module()
  105. config_save_read = _load_config_save_read_module()
  106. if project_config is None:
  107. from workplace.singleton import REPOSITORY_CONFIG_INI_SNAPSHOT
  108. project_config = REPOSITORY_CONFIG_INI_SNAPSHOT.project_config
  109. cfg = project_config
  110. clear_xy_preset = config_save_read.input_keyword_xy_pair_from_config(
  111. cfg,
  112. "clear_x_button_xy",
  113. )
  114. search_xy_preset = config_save_read.input_keyword_xy_pair_from_config(
  115. cfg,
  116. "ocr_search_xiaohongshu_xy",
  117. )
  118. full_screen_screenshot_capture_saver = PillowImageGrabFullScreenScreenshotCaptureSaver()
  119. # 步骤 1:若有非空文案则先清空;配置里有坐标则跳过 OCR
  120. clear_x_button_xy = clear_input.start(
  121. page,
  122. selector_search_input=find_search_input.selector_search_input,
  123. full_screen_screenshot_capture_saver=full_screen_screenshot_capture_saver,
  124. move_mouse_to_pos=move_mouse_to_pos,
  125. poll_interval_sec=poll_interval_sec,
  126. preset_clear_button_xy=clear_xy_preset,
  127. )
  128. cleared_search_input_this_iteration = clear_x_button_xy is not None
  129. if clear_x_button_xy is not None and clear_xy_preset is None:
  130. config_save_read.merge_input_keyword_config(clear_x_button_xy=clear_x_button_xy)
  131. # 步骤 2:「搜索小红书」中心;配置里有则跳过 OCR
  132. search_input_flow_result = find_search_input.start(
  133. page,
  134. keyword=keyword,
  135. preset_center_xy=search_xy_preset,
  136. )
  137. if search_input_flow_result.center_xy is None:
  138. raise RuntimeError(
  139. (search_input_flow_result.message or "").strip()
  140. or "OCR 未在全屏截图中找到「搜索小红书」。",
  141. )
  142. if search_xy_preset is None:
  143. config_save_read.merge_input_keyword_config(
  144. ocr_search_xiaohongshu_xy=search_input_flow_result.center_xy,
  145. )
  146. ocr_text_center_screen_x, ocr_text_center_screen_y = (
  147. search_input_flow_result.center_xy
  148. )
  149. if not cleared_search_input_this_iteration:
  150. move_mouse_to_pos.start(
  151. ocr_text_center_screen_x,
  152. ocr_text_center_screen_y,
  153. )
  154. pointer_settle_screen_x = ocr_text_center_screen_x
  155. pointer_settle_screen_y = ocr_text_center_screen_y
  156. else:
  157. cur_x, cur_y = workplace_pyautogui.raw.position()
  158. pointer_settle_screen_x = int(cur_x)
  159. pointer_settle_screen_y = int(cur_y)
  160. def on_pointer_settled(pointer_final_screen_xy: tuple[int, int]) -> Any:
  161. # 步骤 3:指针已在 OCR 目标点附近;聚焦用 PyAutoGUI 当前位置左键,见 input-word-by-word.type_keyword_human
  162. # 步骤 4:input-word-by-word.start → click_here + 逐字 insert_text + 校验
  163. input_word_by_word.start(page, keyword=keyword)
  164. # 步骤 5:press-search-btn.start(读配置坐标或 LoFTR 匹配后移鼠并点击顶栏搜索)
  165. _press_ok, _press_msg, press_search_button_xy = press_search_btn.start(
  166. page,
  167. project_config=project_config,
  168. )
  169. # 步骤 6:把最终屏幕坐标交给上层 ``then``;无 ``then`` 时返回占位码与 OCR 结果对象
  170. if then is not None:
  171. return then(pointer_final_screen_xy)
  172. return 0, search_input_flow_result
  173. return _pointer_settled_callback(
  174. pointer_settle_screen_x,
  175. pointer_settle_screen_y,
  176. on_pointer_settled,
  177. )
  178. start = init
  179. __all__ = ["init", "start"]