|
@@ -13,6 +13,7 @@ import re
|
|
|
import threading
|
|
import threading
|
|
|
import time
|
|
import time
|
|
|
import traceback
|
|
import traceback
|
|
|
|
|
+import urllib.parse
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
from io import BytesIO
|
|
from io import BytesIO
|
|
|
|
|
|
|
@@ -42,7 +43,7 @@ PLATFORM_ID = 9
|
|
|
YBM_DEVICE_ID = "18008650300"
|
|
YBM_DEVICE_ID = "18008650300"
|
|
|
|
|
|
|
|
# API(内部调度系统)
|
|
# API(内部调度系统)
|
|
|
-TASK_API_BASE = "http://120.24.26.108:8082/api/collect_task"
|
|
|
|
|
|
|
+TASK_API_BASE = "http://pricesys2.kailin.com.cn:8083/api/collect_task"
|
|
|
TASK_API_TOKEN = "zhijiayun_crawler_2026"
|
|
TASK_API_TOKEN = "zhijiayun_crawler_2026"
|
|
|
|
|
|
|
|
TASK_PULL_URL = TASK_API_BASE + "/pull"
|
|
TASK_PULL_URL = TASK_API_BASE + "/pull"
|
|
@@ -89,6 +90,7 @@ LOGIN_VALIDATE_URL = "https://www.ybm100.com/new/"
|
|
|
USERNAME = "18008650300"
|
|
USERNAME = "18008650300"
|
|
|
PASSWORD = "12345678"
|
|
PASSWORD = "12345678"
|
|
|
TARGET_LOGIN_URL = "https://www.ybm100.com/new/login"
|
|
TARGET_LOGIN_URL = "https://www.ybm100.com/new/login"
|
|
|
|
|
+HOME_URL = "https://www.ybm100.com/new/"
|
|
|
|
|
|
|
|
# 选择器
|
|
# 选择器
|
|
|
USERNAME_SELECTOR = "input[placeholder*=请输入账号]"
|
|
USERNAME_SELECTOR = "input[placeholder*=请输入账号]"
|
|
@@ -377,7 +379,7 @@ def get_current_time():
|
|
|
|
|
|
|
|
def random_delay(min_seconds, max_seconds):
|
|
def random_delay(min_seconds, max_seconds):
|
|
|
delay = random.uniform(min_seconds, max_seconds)
|
|
delay = random.uniform(min_seconds, max_seconds)
|
|
|
- time.sleep(delay)
|
|
|
|
|
|
|
+ time.sleep(1)
|
|
|
return delay
|
|
return delay
|
|
|
|
|
|
|
|
|
|
|
|
@@ -431,11 +433,6 @@ def slow_scroll_400px(page, scroll_distance1=400):
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
-def type_slow(locator, text: str, min_delay=0.06, max_delay=0.18):
|
|
|
|
|
- for ch in text:
|
|
|
|
|
- locator.type(ch, delay=int(random.uniform(min_delay, max_delay) * 1000))
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def kill_masks(page):
|
|
def kill_masks(page):
|
|
|
page.evaluate(r"""
|
|
page.evaluate(r"""
|
|
|
() => {
|
|
() => {
|
|
@@ -909,68 +906,21 @@ class YbmCrawler:
|
|
|
|
|
|
|
|
# ---------- search ----------
|
|
# ---------- search ----------
|
|
|
|
|
|
|
|
- @staticmethod
|
|
|
|
|
- def _pick_search_input(page):
|
|
|
|
|
- inputs = page.locator(SEARCH_INPUT_SELECTOR)
|
|
|
|
|
- cnt = inputs.count()
|
|
|
|
|
- for i in range(min(cnt, 2)):
|
|
|
|
|
- candidate = inputs.nth(i)
|
|
|
|
|
- try:
|
|
|
|
|
- candidate.wait_for(state="visible", timeout=1500)
|
|
|
|
|
- if candidate.is_enabled():
|
|
|
|
|
- return candidate
|
|
|
|
|
- except PlaywrightTimeoutError:
|
|
|
|
|
- continue
|
|
|
|
|
- candidate = page.locator(f"{SEARCH_INPUT_SELECTOR}:visible").first
|
|
|
|
|
- candidate.wait_for(state="visible", timeout=ELEMENT_TIMEOUT)
|
|
|
|
|
- return candidate
|
|
|
|
|
-
|
|
|
|
|
- def _search_operation(self, page, keyword, is_first_search=True):
|
|
|
|
|
|
|
+ def _search_operation(self, page, keyword):
|
|
|
|
|
+ """直接拼搜索结果页 URL 跳转,不再操作输入框"""
|
|
|
try:
|
|
try:
|
|
|
- search_locator = self._pick_search_input(page)
|
|
|
|
|
- search_locator.wait_for(timeout=ELEMENT_TIMEOUT)
|
|
|
|
|
- search_locator.click(force=True)
|
|
|
|
|
- search_locator.fill("")
|
|
|
|
|
- page.keyboard.down("Control")
|
|
|
|
|
- page.keyboard.press("a")
|
|
|
|
|
- page.keyboard.up("Control")
|
|
|
|
|
- page.keyboard.press("Backspace")
|
|
|
|
|
- type_slow(search_locator, keyword, min_delay=0.06, max_delay=0.18)
|
|
|
|
|
- logger.info(f"📝 已输入搜索关键词:{keyword}")
|
|
|
|
|
- btn = page.locator(SEARCH_BTN_SELECTOR)
|
|
|
|
|
- btn.wait_for(state="visible", timeout=SEARCH_BTN_TIMEOUT)
|
|
|
|
|
- page.wait_for_timeout(3000)
|
|
|
|
|
- detail_page = page
|
|
|
|
|
- if is_first_search:
|
|
|
|
|
- try:
|
|
|
|
|
- with page.context.expect_page(timeout=60000) as new_page_info:
|
|
|
|
|
- btn.click()
|
|
|
|
|
- detail_page = new_page_info.value
|
|
|
|
|
- detail_page.wait_for_load_state("networkidle", timeout=20000)
|
|
|
|
|
- except PlaywrightTimeoutError:
|
|
|
|
|
- logger.warning(f"{get_current_time()} 未检测到新标签页")
|
|
|
|
|
- return None, False
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.warning(f"{get_current_time()} 等待新标签页异常:{e}")
|
|
|
|
|
- return None, False
|
|
|
|
|
- else:
|
|
|
|
|
- btn.click()
|
|
|
|
|
- page.wait_for_load_state("networkidle", timeout=20000)
|
|
|
|
|
- detail_page = page
|
|
|
|
|
- logger.info("✅ 后续搜索:已在原页面完成跳转加载")
|
|
|
|
|
- test_btn = detail_page.locator("div[data-v-c65c36bc].first-time-highlight-message-btn button")
|
|
|
|
|
- if test_btn.count() > 0:
|
|
|
|
|
- test_btn.wait_for(state="attached", timeout=5000)
|
|
|
|
|
- test_btn.click()
|
|
|
|
|
- force_close_popup(detail_page)
|
|
|
|
|
- kill_masks(detail_page)
|
|
|
|
|
|
|
+ encoded = urllib.parse.quote(keyword)
|
|
|
|
|
+ search_url = f"https://www.ybm100.com/new/base/search?keyword={encoded}#/bigSearch/index"
|
|
|
|
|
+ logger.info(f"跳转搜索页: {search_url}")
|
|
|
|
|
+ page.goto(search_url, timeout=20000)
|
|
|
|
|
+ page.wait_for_load_state("networkidle")
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+ force_close_popup(page)
|
|
|
|
|
+ kill_masks(page)
|
|
|
logger.info("✅ 已触发搜索")
|
|
logger.info("✅ 已触发搜索")
|
|
|
- return detail_page, True
|
|
|
|
|
- except PlaywrightTimeoutError as e:
|
|
|
|
|
- logger.error(f" 搜索失败:元素定位超时 - {str(e)}")
|
|
|
|
|
- return None, False
|
|
|
|
|
|
|
+ return page, True
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f" 搜索异常:{str(e)}")
|
|
|
|
|
|
|
+ logger.error(f"搜索失败: {e}")
|
|
|
return None, False
|
|
return None, False
|
|
|
|
|
|
|
|
# ---------- pagination ----------
|
|
# ---------- pagination ----------
|
|
@@ -1570,12 +1520,20 @@ class YbmCrawler:
|
|
|
save_cookies(context)
|
|
save_cookies(context)
|
|
|
logger.info(" 登录并保存Cookie成功!")
|
|
logger.info(" 登录并保存Cookie成功!")
|
|
|
|
|
|
|
|
|
|
+ # 确保回到首页(登录后可能跳到了其他页面)
|
|
|
|
|
+ page.goto(HOME_URL, timeout=15000)
|
|
|
|
|
+ page.wait_for_load_state("networkidle")
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+ popup_guard(page, "after_login")
|
|
|
|
|
+ force_close_popup(page)
|
|
|
|
|
+ kill_masks(page)
|
|
|
|
|
+
|
|
|
# 启动心跳守护线程(CrawlerScheduler)
|
|
# 启动心跳守护线程(CrawlerScheduler)
|
|
|
self.scheduler.start()
|
|
self.scheduler.start()
|
|
|
|
|
|
|
|
# 搜索
|
|
# 搜索
|
|
|
popup_guard(page, "before_search")
|
|
popup_guard(page, "before_search")
|
|
|
- store_page, search_success = self._search_operation(page, self.keyword, is_first_search=True)
|
|
|
|
|
|
|
+ store_page, search_success = self._search_operation(page, self.keyword)
|
|
|
if store_page:
|
|
if store_page:
|
|
|
popup_guard(store_page, "after_search")
|
|
popup_guard(store_page, "after_search")
|
|
|
|
|
|