vlm.md
← 所有食谱 · UI 操控 · 进阶

网页 UI 元素定位与点击

让 agent 截图后通过 VLM 识别目标按钮/链接/输入框的坐标,再用 pyautogui 或 Playwright 自动点击,适用于 CSS 选择器不可靠的动态页面。

2026/4/30 · vlm.md · 推荐模型: GPT-4oClaude 3.5 Sonnet

场景

你的 web automation agent 需要操作一个动态页面——按钮由 React 渲染、class 名称每次构建都会变、iframe 嵌套导致 CSS 选择器极不稳定。常规的 document.querySelector 方案随时失效。

解决思路:截图 → VLM 定位 → 坐标点击。让模型直接”看”页面,返回目标元素的归一化坐标(0–1 区间),再换算成像素后驱动鼠标点击。

典型用例:

  • 自动化测试中识别”提交”按钮的真实位置
  • 浏览器 agent 点击动态生成的分页链接
  • RPA 流程中定位表单里的”下一步”按钮

推荐模型

模型适用场景
GPT-4o坐标精度最高,UI 元素识别能力强
Claude 3.5 Sonnet对复杂布局的理解更准确,少误判

两者均支持传入 base64 图片;优先用 GPT-4o,如遇到密集 UI 布局可切换 Claude。

Prompt 模板

你是一个网页 UI 分析专家。请在截图中找到以下目标元素,并返回其边界框的归一化坐标。

目标元素描述:{target_description}

返回格式(严格 JSON,不要有任何其他文字):
{
  "found": true,
  "element_description": "对找到元素的简短描述",
  "bbox": {
    "x_min": 0.0,
    "y_min": 0.0,
    "x_max": 0.0,
    "y_max": 0.0
  },
  "center": {
    "x": 0.0,
    "y": 0.0
  }
}

说明:
- 所有坐标均为归一化值(0.0 到 1.0),相对于图片宽高
- center 是边界框中心点
- 如果找不到目标元素,返回 {"found": false, "reason": "..."}

代码示例

import base64
import json
import time
from pathlib import Path

import mss
import pyautogui
from openai import OpenAI
from PIL import Image

client = OpenAI()


def take_screenshot(save_path: str = "/tmp/screen.png") -> tuple[str, int, int]:
    """截取全屏并保存,返回 (路径, 宽, 高)"""
    with mss.mss() as sct:
        monitor = sct.monitors[1]  # 主显示器
        screenshot = sct.grab(monitor)
        img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
        img.save(save_path)
        return save_path, screenshot.width, screenshot.height


def locate_element(image_path: str, target_description: str) -> dict:
    """调用 VLM 定位目标元素,返回归一化坐标"""
    image_data = base64.b64encode(Path(image_path).read_bytes()).decode()

    prompt = f"""你是一个网页 UI 分析专家。请在截图中找到以下目标元素,并返回其边界框的归一化坐标。

目标元素描述:{target_description}

返回格式(严格 JSON,不要有任何其他文字):
{{
  "found": true,
  "element_description": "对找到元素的简短描述",
  "bbox": {{
    "x_min": 0.0,
    "y_min": 0.0,
    "x_max": 0.0,
    "y_max": 0.0
  }},
  "center": {{
    "x": 0.0,
    "y": 0.0
  }}
}}

如果找不到目标元素,返回 {{"found": false, "reason": "..."}}"""

    response = client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": "你是 UI 元素定位助手,只输出 JSON。"},
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:image/png;base64,{image_data}"},
                    },
                    {"type": "text", "text": prompt},
                ],
            },
        ],
        max_tokens=256,
    )

    return json.loads(response.choices[0].message.content)


def click_element(target_description: str, confidence_check: bool = True) -> bool:
    """
    截图 → VLM 定位 → 坐标换算 → 点击
    返回 True 表示成功点击
    """
    # 1. 截图
    img_path, screen_w, screen_h = take_screenshot()
    print(f"截图尺寸: {screen_w}x{screen_h}")

    # 2. VLM 定位
    result = locate_element(img_path, target_description)
    print(f"VLM 返回: {json.dumps(result, ensure_ascii=False)}")

    if not result.get("found"):
        print(f"未找到元素: {result.get('reason', '未知原因')}")
        return False

    # 3. 归一化坐标 → 像素坐标
    cx_norm = result["center"]["x"]
    cy_norm = result["center"]["y"]
    pixel_x = int(cx_norm * screen_w)
    pixel_y = int(cy_norm * screen_h)
    print(f"点击坐标: ({pixel_x}, {pixel_y})")

    # 4. 执行点击(加入轻微随机偏移,模拟真实用户行为)
    pyautogui.moveTo(pixel_x, pixel_y, duration=0.3)
    time.sleep(0.1)
    pyautogui.click()

    return True


# --- Playwright 版本(推荐用于无头浏览器场景)---

def click_element_playwright(page, target_description: str) -> bool:
    """
    使用 Playwright 截图后定位并点击
    page: playwright 的 Page 对象
    """
    import io

    # 截取页面截图(字节流)
    screenshot_bytes = page.screenshot()
    img = Image.open(io.BytesIO(screenshot_bytes))
    screen_w, screen_h = img.size

    # 保存临时文件
    tmp_path = "/tmp/playwright_screen.png"
    img.save(tmp_path)

    result = locate_element(tmp_path, target_description)
    if not result.get("found"):
        return False

    pixel_x = int(result["center"]["x"] * screen_w)
    pixel_y = int(result["center"]["y"] * screen_h)

    page.mouse.click(pixel_x, pixel_y)
    return True


if __name__ == "__main__":
    # 示例:点击页面上的"登录"按钮
    success = click_element("蓝色的'登录'按钮,通常在页面右上角")
    print("点击成功" if success else "点击失败")

安装依赖:

pip install openai mss pyautogui pillow
# 如需 Playwright 版
pip install playwright && playwright install chromium

踩坑记录

坑 1:模型返回的是归一化坐标,不是像素——必须乘以屏幕尺寸

最常见的新手错误:模型说 "center": {"x": 0.75, "y": 0.12},直接把 0.75 当成像素传给 pyautogui,结果点到屏幕左上角角落。

正确做法:pixel_x = int(0.75 * screen_width)。截图时务必记录屏幕分辨率,Retina 屏(2x DPI)需要额外注意——mss 返回的是物理像素,而 pyautogui 使用逻辑像素,需要除以设备像素比。

import subprocess, json

def get_dpi_scale() -> float:
    """macOS 获取设备像素比"""
    result = subprocess.run(
        ["system_profiler", "SPDisplaysDataType", "-json"],
        capture_output=True, text=True
    )
    # 简化处理:Retina 屏返回 2.0,普通屏返回 1.0
    return 2.0 if "Retina" in result.stdout else 1.0

坑 2:元素被遮挡或 z-index 问题

弹出的 cookie 提示、悬浮的客服按钮可能覆盖目标元素。模型会”看到”最上层的元素。建议在截图前先检查并关闭常见遮挡层:

# Playwright 示例:关闭 cookie 横幅
try:
    page.locator("[class*='cookie']").click(timeout=2000)
except:
    pass  # 没有就跳过

坑 3:目标元素滚动到屏幕外

截图时元素可能不在可视区域内。如果模型返回 "found": false 且原因涉及”不可见”,需要先滚动:

# 先让 VLM 判断元素是否在当前视口
# 如果不在,尝试向下滚动后重新截图
def scroll_and_locate(page, target_description: str, max_scrolls: int = 5):
    for i in range(max_scrolls):
        result = locate_element_playwright(page, target_description)
        if result.get("found"):
            return result
        page.mouse.wheel(0, 500)  # 向下滚动 500px
        page.wait_for_timeout(300)
    return {"found": false, "reason": "滚动后仍未找到"}