|
@@ -24,19 +24,19 @@ from selenium.webdriver.common.action_chains import ActionChains
|
|
|
from selenium_stealth import stealth
|
|
from selenium_stealth import stealth
|
|
|
|
|
|
|
|
class Scraper1688:
|
|
class Scraper1688:
|
|
|
- def __init__(self, headless=True, status_callback=None):
|
|
|
|
|
|
|
+ def __init__(self, headless=True, status_callback=None, log_callback=None):
|
|
|
self.headless = headless
|
|
self.headless = headless
|
|
|
self.status_callback = status_callback
|
|
self.status_callback = status_callback
|
|
|
|
|
+ self.log_callback = log_callback # 新增:用于向 GUI 发送普通日志
|
|
|
self.user_data_path = os.path.abspath(os.path.join(os.getcwd(), "1688_user_data"))
|
|
self.user_data_path = os.path.abspath(os.path.join(os.getcwd(), "1688_user_data"))
|
|
|
self.driver = None
|
|
self.driver = None
|
|
|
|
|
|
|
|
- # 1. 强力锁定长效 Session
|
|
|
|
|
edge_path = self._find_edge()
|
|
edge_path = self._find_edge()
|
|
|
if edge_path:
|
|
if edge_path:
|
|
|
- print(f"[*] 【长效稳定模式】正在启动 Edge 独立环境...")
|
|
|
|
|
|
|
+ print(f"[*] 【极致稳定模式】正在启动 Edge 深度伪装环境...")
|
|
|
self._cleanup_processes()
|
|
self._cleanup_processes()
|
|
|
- # 使用固定且持久的 Session 目录,确保 12 小时以上免登录
|
|
|
|
|
- edge_user_data = os.path.join(os.getcwd(), "1688_edge_stable_session")
|
|
|
|
|
|
|
+ # 使用固定且持久的 Session 目录,确保长效免登录
|
|
|
|
|
+ edge_user_data = os.path.join(os.getcwd(), "1688_edge_ultimate_session")
|
|
|
cmd = [
|
|
cmd = [
|
|
|
edge_path,
|
|
edge_path,
|
|
|
"--remote-debugging-port=9222",
|
|
"--remote-debugging-port=9222",
|
|
@@ -48,37 +48,32 @@ class Scraper1688:
|
|
|
if headless: cmd.append("--headless")
|
|
if headless: cmd.append("--headless")
|
|
|
try:
|
|
try:
|
|
|
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
- time.sleep(5) # 预留更充足的启动时间
|
|
|
|
|
|
|
+ time.sleep(6)
|
|
|
opts = EdgeOptions()
|
|
opts = EdgeOptions()
|
|
|
opts.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
|
|
opts.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
|
|
|
- try:
|
|
|
|
|
- self.driver = webdriver.Edge(options=opts)
|
|
|
|
|
- print("[+] Edge 长效环境连接成功!")
|
|
|
|
|
- except:
|
|
|
|
|
- from webdriver_manager.microsoft import EdgeChromiumDriverManager
|
|
|
|
|
- service = EdgeService(EdgeChromiumDriverManager().install())
|
|
|
|
|
- self.driver = webdriver.Edge(service=service, options=opts)
|
|
|
|
|
|
|
+ self.driver = webdriver.Edge(options=opts)
|
|
|
|
|
+ print("[+] Edge 极致稳定环境接管成功!")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
print(f"[!] Edge 启动失败: {e}")
|
|
print(f"[!] Edge 启动失败: {e}")
|
|
|
|
|
|
|
|
if not self.driver:
|
|
if not self.driver:
|
|
|
- print("[*] 正在启动备用 Chrome 模式...")
|
|
|
|
|
self._init_chrome(headless)
|
|
self._init_chrome(headless)
|
|
|
|
|
|
|
|
if self.driver:
|
|
if self.driver:
|
|
|
- # 基础特征隐藏补丁
|
|
|
|
|
|
|
+ # 深度擦除自动化指纹
|
|
|
try:
|
|
try:
|
|
|
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
|
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
|
|
|
- "source": "Object.defineProperty(navigator, 'webdriver', { get: () => undefined });"
|
|
|
|
|
|
|
+ "source": """
|
|
|
|
|
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
|
|
|
+ Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] });
|
|
|
|
|
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
|
|
|
+ """
|
|
|
})
|
|
})
|
|
|
except: pass
|
|
except: pass
|
|
|
|
|
|
|
|
def _find_edge(self):
|
|
def _find_edge(self):
|
|
|
import winreg
|
|
import winreg
|
|
|
- reg_paths = [
|
|
|
|
|
- (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe"),
|
|
|
|
|
- (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe")
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ reg_paths = [(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe")]
|
|
|
for hkey, subkey in reg_paths:
|
|
for hkey, subkey in reg_paths:
|
|
|
try:
|
|
try:
|
|
|
with winreg.OpenKey(hkey, subkey) as key:
|
|
with winreg.OpenKey(hkey, subkey) as key:
|
|
@@ -97,10 +92,7 @@ class Scraper1688:
|
|
|
opts = uc.ChromeOptions()
|
|
opts = uc.ChromeOptions()
|
|
|
opts.add_argument(f"--user-data-dir={self.user_data_path}")
|
|
opts.add_argument(f"--user-data-dir={self.user_data_path}")
|
|
|
return opts
|
|
return opts
|
|
|
- try:
|
|
|
|
|
- self.driver = uc.Chrome(options=create_options(), headless=headless)
|
|
|
|
|
- except:
|
|
|
|
|
- self.driver = uc.Chrome(options=create_options(), headless=headless)
|
|
|
|
|
|
|
+ self.driver = uc.Chrome(options=create_options(), headless=headless)
|
|
|
|
|
|
|
|
def check_for_captcha(self):
|
|
def check_for_captcha(self):
|
|
|
def is_blocked():
|
|
def is_blocked():
|
|
@@ -115,69 +107,76 @@ class Scraper1688:
|
|
|
if self.status_callback: self.status_callback(True, msg)
|
|
if self.status_callback: self.status_callback(True, msg)
|
|
|
while is_blocked(): time.sleep(3)
|
|
while is_blocked(): time.sleep(3)
|
|
|
if self.status_callback: self.status_callback(False, "验证通过")
|
|
if self.status_callback: self.status_callback(False, "验证通过")
|
|
|
- print("[*] 解封成功,强制进入 60 秒安全静默期...")
|
|
|
|
|
- time.sleep(60)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ cool_msg = "[*] 监测到干预完成,进入 120 秒深度冷却期以重置风控权重..."
|
|
|
|
|
+ print(cool_msg)
|
|
|
|
|
+ if self.log_callback: self.log_callback(f"<font color='orange'>{cool_msg}</font>")
|
|
|
|
|
+ time.sleep(120)
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
- def _human_interact(self):
|
|
|
|
|
- """ 深度模拟人类交互,降低被识别概率 """
|
|
|
|
|
- try:
|
|
|
|
|
- actions = ActionChains(self.driver)
|
|
|
|
|
- # 1. 随机小幅度鼠标移动
|
|
|
|
|
- for _ in range(random.randint(3, 6)):
|
|
|
|
|
- actions.move_by_offset(random.randint(-10, 10), random.randint(-10, 10)).perform()
|
|
|
|
|
- time.sleep(random.uniform(0.3, 0.8))
|
|
|
|
|
- # 2. 随机滚动一小段
|
|
|
|
|
- self.driver.execute_script(f"window.scrollBy(0, {random.randint(100, 400)});")
|
|
|
|
|
- time.sleep(random.uniform(1.0, 2.0))
|
|
|
|
|
- except: pass
|
|
|
|
|
|
|
+ def _human_behavior(self, duration=10):
|
|
|
|
|
+ """ 高级拟人化行为模拟 """
|
|
|
|
|
+ start_time = time.time()
|
|
|
|
|
+ while time.time() - start_time < duration:
|
|
|
|
|
+ try:
|
|
|
|
|
+ # 1. 随机滚动
|
|
|
|
|
+ scroll_y = random.randint(200, 600)
|
|
|
|
|
+ self.driver.execute_script(f"window.scrollBy(0, {scroll_y});")
|
|
|
|
|
+ # 2. 随机鼠标晃动
|
|
|
|
|
+ actions = ActionChains(self.driver)
|
|
|
|
|
+ actions.move_by_offset(random.randint(-5, 5), random.randint(-5, 5)).perform()
|
|
|
|
|
+ time.sleep(random.uniform(1.5, 4.0))
|
|
|
|
|
+ # 3. 概率性往回滚
|
|
|
|
|
+ if random.random() > 0.7:
|
|
|
|
|
+ self.driver.execute_script(f"window.scrollBy(0, -{random.randint(100, 300)});")
|
|
|
|
|
+ except: break
|
|
|
|
|
|
|
|
def search_products_yield(self, keyword, total_count=200, existing_links=None):
|
|
def search_products_yield(self, keyword, total_count=200, existing_links=None):
|
|
|
gbk_keyword = urllib.parse.quote(keyword, encoding='gbk')
|
|
gbk_keyword = urllib.parse.quote(keyword, encoding='gbk')
|
|
|
base_url = f"https://s.1688.com/selloffer/offer_search.htm?keywords={gbk_keyword}&n=y&netType=1%2C11%2C16"
|
|
base_url = f"https://s.1688.com/selloffer/offer_search.htm?keywords={gbk_keyword}&n=y&netType=1%2C11%2C16"
|
|
|
|
|
|
|
|
self.driver.get("https://www.1688.com")
|
|
self.driver.get("https://www.1688.com")
|
|
|
|
|
+ time.sleep(random.randint(3, 6))
|
|
|
self.check_for_captcha()
|
|
self.check_for_captcha()
|
|
|
|
|
|
|
|
all_links = existing_links if existing_links is not None else set()
|
|
all_links = existing_links if existing_links is not None else set()
|
|
|
page, initial_count = 1, len(all_links)
|
|
page, initial_count = 1, len(all_links)
|
|
|
|
|
+ # 随机设定下一次深度冷却的阈值 (5-12条之间)
|
|
|
|
|
+ next_cool_threshold = random.randint(5, 12)
|
|
|
|
|
|
|
|
while len(all_links) < total_count + initial_count:
|
|
while len(all_links) < total_count + initial_count:
|
|
|
- print(f"[*] 正在处理列表页: 第 {page} 页...")
|
|
|
|
|
|
|
+ print(f"[*] 正在模拟搜索: 第 {page} 页...")
|
|
|
self.driver.get(f"{base_url}&beginPage={page}&page={page}")
|
|
self.driver.get(f"{base_url}&beginPage={page}&page={page}")
|
|
|
self.check_for_captcha()
|
|
self.check_for_captcha()
|
|
|
|
|
|
|
|
- # 高级模拟滚动:滑下去,停顿,回滑一点
|
|
|
|
|
- steps = random.randint(6, 10)
|
|
|
|
|
- for i in range(1, steps + 1):
|
|
|
|
|
- self.driver.execute_script(f"window.scrollTo(0, document.body.scrollHeight * {i/steps});")
|
|
|
|
|
- time.sleep(random.uniform(1.2, 3.0))
|
|
|
|
|
- if i == random.randint(3, 5):
|
|
|
|
|
- self.driver.execute_script("window.scrollBy(0, -250);")
|
|
|
|
|
- time.sleep(1.0)
|
|
|
|
|
|
|
+ # 列表页模拟“翻找”行为
|
|
|
|
|
+ for _ in range(random.randint(5, 8)):
|
|
|
|
|
+ self.driver.execute_script(f"window.scrollBy(0, {random.randint(400, 800)});")
|
|
|
|
|
+ time.sleep(random.uniform(1.5, 3.5))
|
|
|
|
|
+ if random.random() > 0.8:
|
|
|
|
|
+ self.driver.execute_script("window.scrollBy(0, -300);")
|
|
|
|
|
|
|
|
page_results = self._extract_all_methods()
|
|
page_results = self._extract_all_methods()
|
|
|
- if not page_results:
|
|
|
|
|
- print(f"[!] 第 {page} 页无结果,尝试刷新...")
|
|
|
|
|
- self.driver.refresh()
|
|
|
|
|
- time.sleep(5)
|
|
|
|
|
- page_results = self._extract_all_methods()
|
|
|
|
|
-
|
|
|
|
|
page_batch = []
|
|
page_batch = []
|
|
|
for it in page_results:
|
|
for it in page_results:
|
|
|
clean_url = self.clean_url(it["link"])
|
|
clean_url = self.clean_url(it["link"])
|
|
|
if clean_url and clean_url not in all_links:
|
|
if clean_url and clean_url not in all_links:
|
|
|
all_links.add(clean_url)
|
|
all_links.add(clean_url)
|
|
|
|
|
|
|
|
- # --- 核心频率控制:深度保护机制 ---
|
|
|
|
|
- current_new = len(all_links) - initial_count
|
|
|
|
|
- if current_new > 0 and current_new % 8 == 0:
|
|
|
|
|
- rest = random.randint(90, 240)
|
|
|
|
|
- print(f"[*] 已连续作业8个详情,触发深度冷却 {rest} 秒以维持 12 小时 Session...")
|
|
|
|
|
|
|
+ # --- 核心订正:随机深度冷却 ---
|
|
|
|
|
+ new_processed = len(all_links) - initial_count
|
|
|
|
|
+ if new_processed >= next_cool_threshold:
|
|
|
|
|
+ rest = random.randint(120, 300)
|
|
|
|
|
+ cool_msg = f"[*] 随机触发深度保护 (已处理{new_processed}条),睡眠 {rest} 秒模拟休息..."
|
|
|
|
|
+ print(cool_msg)
|
|
|
|
|
+ if self.log_callback: self.log_callback(f"<font color='orange'><b>{cool_msg}</b></font>")
|
|
|
time.sleep(rest)
|
|
time.sleep(rest)
|
|
|
|
|
+ next_cool_threshold += random.randint(5, 12) # 设定下一个随机检查点
|
|
|
|
|
|
|
|
- print(f" [>] 详情解析: {clean_url}")
|
|
|
|
|
- time.sleep(random.uniform(4.0, 7.0)) # 访问前随机静默
|
|
|
|
|
|
|
+ print(f" [>] 详情仿真采集: {clean_url}")
|
|
|
|
|
+
|
|
|
|
|
+ # 访问前大幅随机停顿
|
|
|
|
|
+ time.sleep(random.uniform(5, 12))
|
|
|
|
|
|
|
|
detail_results = self.scrape_detail(clean_url)
|
|
detail_results = self.scrape_detail(clean_url)
|
|
|
if detail_results: page_batch.extend(detail_results)
|
|
if detail_results: page_batch.extend(detail_results)
|
|
@@ -187,22 +186,24 @@ class Scraper1688:
|
|
|
yield page_batch
|
|
yield page_batch
|
|
|
page_batch = []
|
|
page_batch = []
|
|
|
|
|
|
|
|
- # --- 极低频率抓取:详情页间超长等待 ---
|
|
|
|
|
- time.sleep(random.uniform(20, 45))
|
|
|
|
|
|
|
+ # 详情页之间的大跨度等待
|
|
|
|
|
+ time.sleep(random.uniform(30, 60))
|
|
|
|
|
|
|
|
if len(all_links) >= total_count + initial_count: break
|
|
if len(all_links) >= total_count + initial_count: break
|
|
|
|
|
|
|
|
if page_batch: yield page_batch
|
|
if page_batch: yield page_batch
|
|
|
page += 1
|
|
page += 1
|
|
|
- if page > 100: break
|
|
|
|
|
|
|
+ # 每翻 3 页随机回一次 1688 首页,消除路径单一性
|
|
|
|
|
+ if page % 3 == 0:
|
|
|
|
|
+ self.driver.get("https://www.1688.com")
|
|
|
|
|
+ time.sleep(random.randint(10, 20))
|
|
|
return list(all_links)
|
|
return list(all_links)
|
|
|
|
|
|
|
|
def scrape_detail(self, url):
|
|
def scrape_detail(self, url):
|
|
|
- """ 拟人化的详情数据提取 """
|
|
|
|
|
try:
|
|
try:
|
|
|
self.driver.get(url)
|
|
self.driver.get(url)
|
|
|
- time.sleep(random.uniform(5.0, 10.0)) # 充分加载
|
|
|
|
|
- self._human_interact() # 执行随机交互
|
|
|
|
|
|
|
+ # --- 核心改进:详情页留存仿真 ---
|
|
|
|
|
+ self._human_behavior(duration=random.randint(12, 25))
|
|
|
self.check_for_captcha()
|
|
self.check_for_captcha()
|
|
|
|
|
|
|
|
model = self.driver.execute_script(
|
|
model = self.driver.execute_script(
|
|
@@ -264,7 +265,6 @@ class Scraper1688:
|
|
|
return url
|
|
return url
|
|
|
|
|
|
|
|
def _extract_all_methods(self):
|
|
def _extract_all_methods(self):
|
|
|
- """ 全力提取列表页链接 """
|
|
|
|
|
results = []
|
|
results = []
|
|
|
try:
|
|
try:
|
|
|
res = self.driver.execute_script("return JSON.stringify(window.data || window.__INITIAL_DATA__)")
|
|
res = self.driver.execute_script("return JSON.stringify(window.data || window.__INITIAL_DATA__)")
|