
1. 项目概述当自动化脚本遇上视觉搜索巨头最近在做一个挺有意思的项目叫 FindQC。简单来说它的核心目标就是挑战 Google Lens 这个视觉搜索领域的“巨无霸”。你可能用过 Google Lens拍张照片就能搜到同款商品、识别植物、翻译文字非常方便。但作为开发者我们想的是能不能用自动化的方式去模拟、甚至超越这种交互体验这就是 FindQC 的出发点。不过事情远没有“写个脚本去访问 Google Lens 网页版”那么简单。Google 的反爬虫机制是出了名的严密尤其是对于 Lens 这种核心服务。直接用一个普通的自动化脚本去访问大概率会在几秒钟内看到验证码或者直接被限制访问。所以这个实战系列的第二部分核心就聚焦在两个关键词上“隐匿模式”和“反爬虫机制构建”。我们得让我们的自动化脚本这里选用 Playwright看起来尽可能像一个真实的、来自不同地区、不同设备的普通用户同时还要能智能地应对 Google 设下的各种“路障”。这不仅仅是技术对抗更像是一场精心设计的“伪装游戏”。我们需要考虑浏览器指纹、网络环境、行为模式等方方面面。本文将详细拆解我们如何利用 Playwright 构建一个足够“隐匿”的自动化环境并设计一套健壮的反爬虫策略让 FindQC 能够稳定、持续地与 Google Lens 进行交互为后续的图像上传、结果解析等核心功能铺平道路。无论你是对爬虫与反爬虫攻防感兴趣还是想深入了解现代浏览器自动化工具的高级玩法相信接下来的内容都会对你有所启发。2. 核心思路与架构设计为何是 Playwright以及我们如何“隐身”在开始敲代码之前我们必须想清楚两件事第一为什么选择 Playwright 而不是 Selenium 或 Puppeteer第二一个理想的“隐匿模式”应该包含哪些要素2.1 工具选型Playwright 的压倒性优势几年前Selenium 几乎是浏览器自动化的代名词。但时至今日Playwright 在应对现代复杂 Web 应用和反爬虫场景时展现出了更显著的优势这正是我们项目所需要的。原生多浏览器支持与一致性Playwright 由微软开发为 Chromium、Firefox 和 WebKitSafari 内核提供了一致的 API。这意味着我们可以用同一套脚本轻松测试在不同浏览器引擎下的表现。对于反爬虫而言这太重要了——我们可以随机或按策略切换浏览器类型避免因单一浏览器指纹而被轻易标记。Selenium 虽然也支持多浏览器但需要单独下载和配置不同浏览器的驱动WebDriver管理和版本同步是个麻烦事。自动等待与强大的选择器Playwright 内置了智能等待机制它会自动等待元素可操作如可点击、可见这大大减少了编写显式等待time.sleep的需要让脚本更稳定、更像真人操作真人不会在页面加载一半时精确地每秒点击一次。它的选择器也非常强大支持文本选择、CSS、XPath 以及 Playwright 特有的如:has-text()等定位元素更加灵活精准。网络拦截与模拟这是隐匿和反爬的核心功能。Playwright 允许我们轻松地拦截和修改网络请求与响应。我们可以修改请求头这是伪装的关键。我们可以为每个请求添加或覆盖User-Agent、Accept-Language、Referer等头部信息模拟来自不同设备和地区的访问。注入 JavaScript在页面加载前或加载后执行自定义 JS例如修改navigator对象属性来伪装浏览器指纹或者移除某些可能暴露自动化特征的属性如webdriver标志。模拟地理位置和语言直接通过 API 设置浏览器上下文的地理位置和语言偏好让网站“相信”我们来自特定区域。处理资源加载可以阻止加载图片、样式表、字体等非必要资源大幅提升脚本运行速度这在批量任务中非常有用。当然为了更像真人我们有时需要选择性加载。浏览器上下文隔离Playwright 的BrowserContext概念类似于一个独立的隐身会话。每个上下文都有独立的缓存、Cookie、本地存储。我们可以为每次任务创建一个新的上下文任务结束后销毁从而实现完美的会话隔离避免 Cookie 和痕迹残留导致关联追踪。基于以上几点Playwright 在控制力、灵活性和对现代 Web 特性的支持上都更适合我们构建一个高隐匿性的自动化客户端。2.2 “隐匿模式”架构蓝图我们的目标不是“隐身”那是不可能的而是“融入”。我们要让脚本产生的流量和行为模式淹没在 Google 海量的正常用户流量中。为此我们设计了一个分层架构基础层浏览器指纹伪装。包括User-Agent、屏幕分辨率、时区、语言、WebGL 报告、Canvas 指纹等。Playwright 提供了部分直接设置如视口大小、UA更复杂的则需要通过注入 JS 来修改。网络层请求特征伪装。标准化并随机化请求头管理 Cookie 策略如使用持久化 Cookie 模拟登录态或每次使用全新会话以及通过代理池轮换 IP 地址。这是避免 IP 和请求模式被封禁的关键。行为层人类交互模拟。引入随机延迟、非直线的鼠标移动轨迹、不规律的滚动行为、甚至“误点击”等。Playwright 可以录制鼠标移动轨迹并回放使其看起来更自然。感知与应对层反爬虫检测与响应。这是智能部分。脚本需要能够检测是否触发了反爬机制如出现验证码、访问被重定向到异常页面、返回特定状态码并触发相应的应对策略如自动解析简单验证码谨慎使用、切换代理、更换浏览器上下文甚至进入“冷却”模式等待一段时间。这个架构将指导我们后续的所有实现。它不是一蹴而就的而是一个需要不断迭代和调整的过程因为对手的防御策略也在进化。3. 核心细节解析与实操要点从启动到伪装的每一步让我们深入到代码层面看看如何用 Playwright 一步步实现上述架构。这里我会分享大量实操中的细节和“坑点”。3.1 环境准备与 Playwright 初始化首先确保安装了 Playwright。推荐使用 pip 安装并同步安装浏览器二进制文件。pip install playwright playwright install chromium firefox webkit # 建议安装所有增加多样性初始化浏览器时我们就要开始考虑隐匿性。不要使用playwright.chromium.launch(headlessFalse)这种简单方式。在无头模式下一些指纹特征会与普通浏览器不同尽管 Playwright 做了很多隐藏工作但对于 Google 这样的对手我们需要更谨慎。我们的启动策略是优先使用非无头模式但以高度伪装的方式运行。我们可以通过args参数传递大量命令行开关来修改浏览器行为。import asyncio from playwright.async_api import async_playwright import random async def create_stealth_context(browser_typechromium, use_proxyNone): playwright await async_playwright().start() # 准备启动参数 launch_options { headless: False, # 为了更好的兼容性使用非无头 args: [ --disable-blink-featuresAutomationControlled, # 禁用自动化控制特征 --disable-infobars, # 禁用“Chrome正受到自动测试软件控制”的信息栏 --no-sandbox, --disable-setuid-sandbox, --disable-dev-shm-usage, # 可以添加更多用于指纹混淆的 args例如随机化窗口大小 f--window-size{random.randint(1200,1920)},{random.randint(800,1080)}, ], } # 如果有代理加入代理参数 if use_proxy: launch_options[args].append(f--proxy-server{use_proxy}) if browser_type chromium: browser await playwright.chromium.launch(**launch_options) elif browser_type firefox: browser await playwright.firefox.launch(**launch_options) else: browser await playwright.webkit.launch(**launch_options) # 创建上下文是关键一步 context await browser.new_context( viewport{width: 1366, height: 768}, # 设置一个常见视口 localeen-US, # 设置语言环境 timezone_idAmerica/Los_Angeles, # 设置时区 user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, # 一个常见的UA # 可以在这里注入初始脚本修改navigator等属性 # ignore_https_errorsTrue, # 谨慎使用处理某些证书错误 ) # 进一步通过add_init_script注入JS进行深度伪装 await context.add_init_script( // 覆盖webdriver属性 Object.defineProperty(navigator, webdriver, { get: () undefined, }); // 修改plugins长度一种常见的指纹点 Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5], }); // 修改languages Object.defineProperty(navigator, languages, { get: () [en-US, en], }); ) return browser, context, playwright注意add_init_script是在每个新页面加载之前执行的这是修改全局对象如navigator,window的最佳时机。但要注意有些高级指纹检测会通过多次访问和对比来发现这些属性的不一致性因此伪装需要尽可能全面和一致。3.2 请求头管理与代理集成一个真实的浏览器请求会携带数十个头部信息。我们需要在上下文层面和页面层面进行管理。上下文层面我们在创建new_context时已经设置了user_agent和locale这会影响相关请求头的基础值。页面/请求层面我们可以使用route功能拦截所有请求并统一修改请求头。这是更精细的控制方式。async def setup_headers_and_proxy(page, proxy_urlNone): # 定义一个常见的请求头模板 common_headers { Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Encoding: gzip, deflate, br, Accept-Language: en-US,en;q0.9, Cache-Control: no-cache, Connection: keep-alive, Pragma: no-cache, Upgrade-Insecure-Requests: 1, } # 可以准备多个UA轮换 user_agents [...] await page.route(**/*, lambda route: route.continue_(headerscommon_headers)) # 如果需要动态代理可以在route里处理但更常见的做法是在创建context时设置代理 # 通过 context await browser.new_context(proxy{server: proxy_url})关于代理对于 Google 这类目标高质量的住宅代理Residential Proxy或移动代理几乎是必须的。数据中心代理Datacenter Proxy的 IP 段很容易被识别和封禁。在 Playwright 中代理可以在浏览器启动参数或浏览器上下文中设置。建议为每个独立的上下文即每次“会话”使用不同的代理 IP实现完全隔离。3.3 人类行为模拟让脚本“活”起来这是区分初级爬虫和高级爬虫的关键。Playwright 提供了一些原生支持。随机延迟在关键操作点击、输入、跳转前后加入随机等待时间。不要用固定的page.wait_for_timeout(2000)而要用random.uniform(1.5, 4.0)这样的随机值。鼠标移动轨迹page.mouse.move(x, y)是瞬间完成的。我们可以写一个函数让鼠标沿着一条带有随机偏移的贝塞尔曲线或折线移动。import random async def human_like_move(page, selector): element await page.wait_for_selector(selector) box await element.bounding_box() target_x box[x] box[width] / 2 random.randint(-5, 5) target_y box[y] box[height] / 2 random.randint(-5, 5) # 模拟移动分多步带随机路径 current_x, current_y random.randint(100, 300), random.randint(100, 300) # 从随机位置开始 steps random.randint(20, 40) for i in range(steps): t i / steps # 简单的线性插值加上随机扰动 next_x current_x (target_x - current_x) * 0.1 random.uniform(-3, 3) next_y current_y (target_y - current_y) * 0.1 random.uniform(-3, 3) await page.mouse.move(next_x, next_y) await page.wait_for_timeout(random.randint(10, 30)) # 每步短暂停顿 current_x, current_y next_x, next_y await page.wait_for_timeout(random.uniform(0.1, 0.3)) await page.mouse.click(target_x, target_y)滚动行为人不一定一次性滚到底。可以模拟先快速滚动一部分停顿阅读再缓慢滚动。async def human_like_scroll(page): viewport_height page.viewport_size[height] total_height await page.evaluate(document.body.scrollHeight) scrolled 0 while scrolled total_height: # 每次滚动随机距离有时向上回滚一点 scroll_amount random.randint(int(viewport_height*0.7), int(viewport_height*1.5)) scrolled scroll_amount await page.evaluate(fwindow.scrollBy(0, {scroll_amount})) await page.wait_for_timeout(random.uniform(0.5, 2.5)) # 滚动后停顿 # 小概率向上回滚 if random.random() 0.1: back_scroll random.randint(50, 200) await page.evaluate(fwindow.scrollBy(0, -{back_scroll})) await page.wait_for_timeout(random.uniform(0.3, 1.0))这些行为模拟的代码会显著增加脚本运行时间但为了隐匿性和稳定性这是必要的代价。你可以根据任务紧急程度调整这些参数的“人性化”强度。4. 反爬虫检测与响应机制的构建即使做了完美伪装我们仍可能触发反爬。一个健壮的系统必须能“感知”危险并“反应”。4.1 检测机制如何知道被“抓”了我们需要在脚本的关键节点插入检查点页面内容检测访问目标页面如lens.google.com后立即检查是否存在特定元素。验证码检查是否有#captcha-form,img[alt*CAPTCHA]等元素。异常提示检查页面标题或大字体文字是否包含 “Sorry...”, “We have detected unusual traffic”, “Access Denied”, “Rate Limited” 等关键词。重定向检查当前 URL 是否被重定向到了非预期的域名如google.com/sorry或一个纯验证码页面。网络响应检测监听网络响应检查状态码。403 Forbidden,429 Too Many Requests,503 Service Unavailable都是明确的警告信号。Playwright 的page.on(‘response’)事件可以帮我们做到这一点。行为异常检测例如点击搜索按钮后预期的新结果区域没有加载或者页面长时间处于加载状态。我们可以将这些检测逻辑封装成一个函数async def check_anti_bot(page): 检查当前页面是否触发了反爬机制 warnings [] # 1. 检查URL current_url page.url if sorry in current_url or denied in current_url or challenge in current_url: warnings.append(f疑似被重定向到验证页面: {current_url}) # 2. 检查页面内容 captcha await page.query_selector(div#captcha, img[alt*CAPTCHA], iframe[src*recaptcha]) if captcha: warnings.append(检测到验证码(CAPTCHA)元素) unusual_traffic await page.query_selector(text/We have detected unusual traffic) if unusual_traffic: warnings.append(检测到“异常流量”提示) # 3. 检查特定标题或文本 title await page.title() if any(word in title.lower() for word in [sorry, denied, blocked, unusual]): warnings.append(f页面标题包含异常关键词: {title}) return warnings4.2 响应策略触发后怎么办当检测到警告时我们不能让脚本傻傻地继续执行。需要有一套分级响应策略轻度警告如首次出现异常流量提示策略可以是“减速并观察”。立即大幅增加后续所有操作的随机延迟并可能随机模拟一些浏览其他非目标页面的行为如在 Google 首页搜索一些无关词条然后再回到主任务。中度警告如出现验证码但非复杂类型对于简单的图像点选验证码理论上可以集成 OCR 服务如pytesseract配合图像预处理进行尝试但成功率有限且违反 Google 服务条款风险极高。更稳妥的策略是放弃当前上下文。记录下这个代理 IP 和浏览器指纹组合可能已被标记然后关闭当前页面和浏览器上下文。从代理池中获取一个新 IP。更换一套浏览器指纹User-Agent, Viewport 等。使用新配置创建一个全新的浏览器上下文重试任务。重度警告如 IP 直接被封返回 403/429立即将当前代理 IP 加入“冷却黑名单”在接下来的数小时甚至数天内不再使用。同时触发“全局冷却”机制可能意味着整个脚本暂停运行一段时间例如 10-30 分钟模拟真人遇到问题后离开的行为。class AntiBotManager: def __init__(self, proxy_pool): self.proxy_pool proxy_pool self.bad_proxies set() self.cool_down_until None async def handle_detection(self, page, context, warning_levelmedium): print(f[反爬处理] 触发警告级别: {warning_level}) if warning_level high: # 重度警告记录代理全局冷却 proxy_in_use context._options.get(proxy) if proxy_in_use: self.bad_proxies.add(proxy_in_use[server]) print(f[反爬处理] 代理 {proxy_in_use[server]} 被加入黑名单) # 设置全局冷却例如30分钟 self.cool_down_until time.time() 1800 await context.close() return global_cool_down elif warning_level medium: # 中度警告丢弃当前会话更换代理重试 await context.close() new_proxy self.proxy_pool.get_proxy(excludeself.bad_proxies) if new_proxy: print(f[反爬处理] 更换新代理: {new_proxy} 进行重试) return retry_with_new_proxy, new_proxy else: print([反爬处理] 无可用代理进入冷却) return no_proxy_cool_down elif warning_level low: # 轻度警告模拟人类“困惑”行为然后继续 await page.wait_for_timeout(random.uniform(5000, 10000)) # 长等待 # 可能随机浏览一些其他链接 await page.evaluate(window.scrollTo(0, 0)) await page.wait_for_timeout(1000) # 尝试点击页面上的一个随机安全链接如“首页” links await page.query_selector_all(a) if links: await random.choice(links).click() await page.wait_for_timeout(3000) await page.go_back() return continue_with_caution这个管理器需要与你的主任务循环紧密结合根据返回的状态决定下一步动作。5. 实战整合构建稳健的 FindQC 访问流程现在我们把所有模块组合起来形成一个完整的、可重试的访问流程。这个流程是 FindQC 与 Google Lens 交互的基础。import asyncio import random import time from your_proxy_pool_module import ProxyPool from your_fingerprint_lib import generate_fingerprint async def robust_google_lens_access(search_image_path, max_retries3): 稳健地访问Google Lens并上传图片进行搜索 proxy_pool ProxyPool() anti_bot_mgr AntiBotManager(proxy_pool) for attempt in range(max_retries): print(f[尝试 {attempt1}/{max_retries}]) # 0. 检查全局冷却 if anti_bot_mgr.cool_down_until and time.time() anti_bot_mgr.cool_down_until: wait_time int(anti_bot_mgr.cool_down_until - time.time()) print(f全局冷却中还需等待 {wait_time} 秒) await asyncio.sleep(wait_time 5) anti_bot_mgr.cool_down_until None # 1. 准备资源获取代理和生成指纹 proxy proxy_pool.get_proxy(excludeanti_bot_mgr.bad_proxies) if not proxy: print(无可用代理任务终止) break fp generate_fingerprint() # 生成一套随机的浏览器指纹UA 视口等 # 2. 创建隐匿的浏览器上下文 browser, context, playwright await create_stealth_context( browser_typerandom.choice([chromium, firefox]), use_proxyproxy[server], fingerprintfp ) try: page await context.new_page() # 设置请求头拦截 await setup_headers_and_proxy(page) # 3. 导航至 Google Lens print(正在导航至 Google Lens...) # 使用 human_like_move 和 human_like_scroll 的变体来模拟输入网址并回车的过程略 await page.goto(https://lens.google.com, wait_untilnetworkidle, timeout60000) await page.wait_for_timeout(random.uniform(2000, 5000)) # 4. 执行反爬检测 warnings await check_anti_bot(page) if warnings: print(f检测到反爬警告: {warnings}) level medium if any(验证码 in w or sorry in w for w in warnings) else low if 403 in str(warnings) or 429 in str(warnings): level high action await anti_bot_mgr.handle_detection(page, context, level) if action global_cool_down: await browser.close() await playwright.stop() print(触发全局冷却任务暂停) return None elif action[0] retry_with_new_proxy: await browser.close() await playwright.stop() continue # 进入下一次循环重试 elif action continue_with_caution: pass # 谨慎继续 # 5. 核心操作上传图片这里简化实际需处理文件选择对话框 print(准备上传图片...) # Playwright 处理文件上传非常方便 upload_button await page.wait_for_selector(div[aria-label*搜索] input[typefile], timeout10000) await upload_button.set_input_files(search_image_path) print(图片已上传等待结果...) # 6. 等待结果加载并再次检测 await page.wait_for_selector(div[data-encoded], timeout30000) # 假设这是结果容器 await human_like_scroll(page) # 模拟浏览结果 final_warnings await check_anti_bot(page) if final_warnings: print(f结果页仍有警告: {final_warnings}可能影响数据质量) # 7. 提取数据这部分是FindQC后续功能 # results await extract_results(page) # 8. 任务成功清理资源 await page.close() await context.close() await browser.close() await playwright.stop() print(任务成功完成) # return results return 模拟成功 except Exception as e: print(f尝试过程中发生异常: {e}) # 发生未知异常也关闭当前会话使用新代理重试 try: await context.close() await browser.close() await playwright.stop() except: pass continue # 进入下一次重试循环 print(f经过 {max_retries} 次尝试仍未成功) return None # 运行示例 # asyncio.run(robust_google_lens_access(path/to/your/image.jpg))这个流程体现了我们构建的隐匿与反爬体系的运作方式资源准备 - 环境构建 - 访问与检测 - 响应与恢复 - 任务执行。它是一个带有自我修复和适应能力的循环。6. 常见问题、排查技巧与进阶优化在实际运行中你一定会遇到各种各样的问题。这里记录了一些我们踩过的坑和解决方案。6.1 常见问题速查表问题现象可能原因排查步骤与解决方案页面无法加载超时1. 代理IP失效或速度慢。2. 浏览器启动参数有误导致崩溃。3. 目标网站屏蔽了该指纹。1. 测试代理IP的连通性和速度。2. 尝试去掉部分args启动参数或使用headlessTrue测试。3. 更换一套浏览器指纹特别是User-Agent和Viewport。立刻弹出验证码1. IP 质量太差数据中心IP。2. 浏览器指纹暴露如navigator.webdriver未覆盖。3. 行为模式过于规律。1.必须使用高质量住宅代理。2. 检查add_init_script注入的JS是否生效在页面控制台测试。3. 加强行为模拟的随机性在访问前先模拟一些“预热”行为如搜索其他词。鼠标点击无效1. 元素定位不准动态加载。2. 元素被遮挡。3. 点击坐标不对。1. 使用更稳定的选择器并配合page.wait_for_selector和state: attached等选项。2. 使用page.click(selector, forceTrue)强制点击谨慎。3. 使用element.click()而非坐标点击。add_init_script修改的属性被还原某些网站在页面加载后或特定交互后会重新检测并覆盖这些属性。1. 尝试在page.evaluate_on_new_document中注入确保在文档创建之初执行。2. 设置事件监听在可能触发检测的交互如点击、输入后再次执行修改脚本。内存泄漏运行久了崩溃1. 上下文Context或页面Page未正确关闭。2. 代理连接未释放。1.确保异常处理中也有资源清理逻辑如上面的try-exatch所示。2. 定期重启浏览器实例或者为每个任务使用独立进程。6.2 进阶优化技巧指纹库与行为模式库不要硬编码几套指纹和行为。可以建立一个小型数据库或 JSON 文件存储数十套经过验证的、看似真实的浏览器指纹从真实浏览器中提取和鼠标移动轨迹模板每次任务随机选取一套。异步并发控制如果需要大规模任务你会想并发运行多个浏览器实例。务必做好并发控制避免对单一代理IP或目标服务器造成过大压力那等于自我暴露。使用asyncio.Semaphore限制同时活跃的任务数。结果验证与反馈循环并非所有成功加载的页面都有有效数据。建立一套验证机制检查返回的页面是否包含预期的数据区域。如果没有将其视为一次“软失败”触发与反爬类似的恢复流程更换会话重试。这能有效应对那些不返回错误码但返回空白或错误内容的反爬策略。日志与监控详细的日志至关重要。记录每次任务的使用的代理IP、浏览器指纹、开始时间、结束时间、触发的警告、采取的行动、最终结果成功/失败及原因。这些日志是优化你的隐匿策略和代理池质量的最佳数据来源。构建这样一个系统更像是在进行一场持续的“军备竞赛”。Google 的工程师们也在不断更新他们的检测模型。今天有效的方法明天可能就会失效。因此核心思路不是找到一个一劳永逸的“银弹”而是建立一个能够快速感知变化、灵活调整策略的自动化框架。FindQC 项目的这一部分正是这个框架的基石。它让我们能够以尽可能接近真实用户的姿态去触碰和利用 Google Lens 提供的服务为后续的图像识别结果比对、质量分析等核心功能打下了坚实可靠的基础。记住稳健性和可维护性比极致的单次成功率更重要。