# import os # import re # import json # import argparse # from pathlib import Path # from typing import Dict, Any, List, Optional # from urllib.parse import urljoin, urlparse # from selenium import webdriver # from selenium.webdriver.edge.options import Options # from selenium.webdriver.edge.service import Service # from selenium.webdriver.support.ui import WebDriverWait # from webdriver_manager.microsoft import EdgeChromiumDriverManager # # -------------------- 工具函数 -------------------- # def get_absolute_url(base_url: str, href: str) -> Optional[str]: # if not href: # return None # href = href.strip() # if href.startswith(('javascript:', 'mailto:', 'tel:', '#')): # return None # absolute = urljoin(base_url, href) # parsed = urlparse(absolute) # if parsed.scheme in ('http', 'https', 'file'): # return absolute # return None # # -------------------- 语义上下文推断 -------------------- # def infer_context_from_attrs(tag_name: str, attrs: Dict[str, str], text: str = "") -> str: # class_list = attrs.get("class", "").split() # id_ = attrs.get("id", "") # href = attrs.get("href", "") # features = " ".join(class_list) + " " + id_ # if tag_name == "a": # if any(w in features for w in ["btn", "button", "tab", "nav-btn", "menu-item", "action-btn"]): # return "按钮" # if "cm.bilibili.com" in href or "ad" in features: # return "广告链接" # return "普通链接" # if "search" in features or "nav-search" in features: # return "搜索框" # if any(w in features for w in ["header", "top-bar", "masthead", "nav"]): # return "顶部导航" # if any(w in features for w in ["sidebar", "aside", "right-entry"]): # return "侧边栏" # if "footer" in features: # return "页脚" # if any(w in features for w in ["channel", "partition", "channel-icons"]): # return "频道/分区" # if any(w in features for w in ["carousel", "banner", "slider"]): # return "轮播/横幅" # if any(w in features for w in ["video-card", "feed-card", "card-item", "card"]): # return "视频卡片" # if "main" in features or "content" in features: # return "主要内容区" # if tag_name in ("input", "textarea"): # return "输入控件" # if tag_name == "button": # return "按钮" # if tag_name == "select": # return "下拉选择" # if tag_name in ("div", "li", "span", "img", "td", "label"): # if any(w in features for w in ["btn", "button", "action", "clickable", "tab", "menu-item"]): # return "按钮" # if any(w in features for w in ["card", "item", "cell", "list-item"]): # return "卡片/列表项" # if "dropdown" in features or "menu" in features: # return "菜单" # if "dialog" in features or "modal" in features: # return "弹窗" # if "icon" in features: # return "图标" # if tag_name == "img": # return "图片" # return "交互区域" # return "其他" # # -------------------- 控件提取(去重 + 仅保留元素大小) -------------------- # def extract_controls(driver) -> List[Dict[str, Any]]: # """ # 无头模式下提取所有可交互元素(去重), # 仅返回元素的尺寸(CSS 像素),不包含坐标。 # """ # raw = driver.execute_script(""" # const tags = ['a', 'input', 'button', 'select', 'textarea', # 'div', 'li', 'span', 'img', 'td', 'label']; # let allElements = []; # // 1. 收集候选元素 # for (let tag of tags) { # let elems = document.querySelectorAll(tag); # for (let elem of elems) { # if (tag === 'input' && elem.type === 'hidden') continue; # if (tag === 'a' && !elem.href && !elem.onclick) continue; # let isInteractive = false; # if (['div', 'li', 'span', 'img', 'td', 'label'].includes(tag)) { # if (elem.onclick) isInteractive = true; # let role = elem.getAttribute('role'); # if (role && ['button', 'link', 'tab', 'menuitem'].includes(role)) isInteractive = true; # if (elem.hasAttribute('tabindex') && elem.getAttribute('tabindex') !== '-1') isInteractive = true; # if (elem.hasAttribute('data-action') || elem.hasAttribute('data-href')) isInteractive = true; # let cls = elem.className || ''; # if (/(btn|button|clickable|action|link|tab|menu-item|card|cell)/i.test(cls)) { # isInteractive = true; # } # if (!isInteractive) continue; # } # allElements.push(elem); # } # } # // 2. 去重:只保留不被其他任何元素包含的最外层元素 # let uniqueElements = []; # for (let elem of allElements) { # let isChild = false; # for (let other of allElements) { # if (other !== elem && other.contains(elem)) { # isChild = true; # break; # } # } # if (!isChild) { # uniqueElements.push(elem); # } # } # // 3. 获取每个元素的属性、文本和尺寸 # return uniqueElements.map(elem => { # let tag = elem.tagName.toLowerCase(); # let attrs = {}; # for (let attr of elem.attributes) { # attrs[attr.name] = attr.value; # } # let formAction = null; # if (tag === 'button' || (tag === 'input' && ['submit','button','image'].includes(elem.type))) { # let form = elem.closest('form'); # if (form) formAction = form.getAttribute('action') || ''; # } # let text = elem.innerText ? elem.innerText.trim().substring(0, 50) : ''; # let rect = elem.getBoundingClientRect(); # return { # tag: tag, # attributes: attrs, # text: text, # form_action: formAction, # is_interactive: true, # size: { # width: rect.width, # height: rect.height # } # }; # }); # """) # controls = [] # for item in raw: # ctrl = { # "type": item["tag"], # "attributes": item["attributes"], # "text": item.get("text", ""), # "context": infer_context_from_attrs(item["tag"], item["attributes"], item.get("text", "")), # "form_action": item.get("form_action"), # "is_interactive": item.get("is_interactive", False), # "size": item.get("size", {}) # } # controls.append(ctrl) # return controls # # -------------------- 人性化报告生成 -------------------- # def short_text_for_control(ctrl: Dict[str, Any]) -> str: # tp = ctrl["type"] # attrs = ctrl["attributes"] # is_interactive = ctrl.get("is_interactive", False) # size = ctrl.get("size", {}) # size_str = "" # if size: # w, h = size.get("width", 0), size.get("height", 0) # size_str = f" [大小:{w:.0f}x{h:.0f}]" # base = "" # if tp == "a": # href = attrs.get("href", "") # text = ctrl.get("text") or attrs.get("title") or attrs.get("aria-label") or href[:60] # base = f"链接 →{text}" # if href: # base += f" : {href}" # elif tp == "button": # text = ctrl.get("text") or attrs.get("value") or attrs.get("aria-label") or "按钮" # base = f"按钮: {text}" # elif tp == "input": # input_type = attrs.get("type", "text") # text = ctrl.get("text") or attrs.get("value") or attrs.get("placeholder") or "" # base = f"输入框 ({input_type})" # if text: # base += f" [{text}]" # elif tp == "textarea": # text = ctrl.get("text") or attrs.get("placeholder") or "" # base = "多行文本框" # if text: # base += f" [{text}]" # elif tp == "select": # text = ctrl.get("text") or "" # base = "下拉选择框" # if text: # base += f" [{text}]" # elif is_interactive: # text = ctrl.get("text") or "" # role = attrs.get("role", "") # tag_desc = f"{tp}控件" # if role == "button": # tag_desc = "按钮(ARIA)" # elif role == "link": # tag_desc = "链接(ARIA)" # elif "card" in (attrs.get("class", "") + attrs.get("id", "")): # tag_desc = "卡片" # base = f"可交互{tag_desc}" # if text: # base += f" [{text}]" # if attrs.get("data-href"): # base += f" (目标: {attrs['data-href']})" # else: # text = ctrl.get("text") or "" # base = f"{tp} 控件" # if text: # base += f" [{text}]" # return base + size_str # def generate_report(url: str, controls: List[Dict[str, Any]]) -> str: # lines = [f"🌐 页面交互控件分析报告(静态单页·无头模式)\n页面: {url}\n" + "=" * 50] # groups: Dict[str, List[Dict]] = {} # for ctrl in controls: # ctx = ctrl["context"] # groups.setdefault(ctx, []).append(ctrl) # sorted_groups = sorted(groups.items(), key=lambda x: x[0]) # for context, items in sorted_groups: # lines.append(f"├── 【{context}】({len(items)}个)") # for item in items: # desc = short_text_for_control(item) # lines.append(f"│ ├── {desc}") # return "\n".join(lines) # # -------------------- 主程序 -------------------- # def main(): # parser = argparse.ArgumentParser(description="静态单页交互控件分析(Edge·无头模式·仅元素大小)") # parser.add_argument("source", help="起始 URL 或本地 HTML 文件路径") # parser.add_argument("-o", "--output", help="输出报告文件路径,默认打印到控制台") # parser.add_argument("--json", action="store_true", help="同时保存机器可读的 JSON 文件(与 -o 同名 .json)") # args = parser.parse_args() # # 构造 URL # if args.source.startswith(('http://', 'https://')): # url = args.source # else: # abs_path = os.path.abspath(args.source) # url = Path(abs_path).as_uri() # # 无头模式 Edge,设置窗口大小 # options = Options() # options.add_argument("--headless=new") # 新版无头模式 # options.add_argument("--window-size=1920,1080") # 固定视口大小 # options.add_argument("--disable-gpu") # 禁用 GPU 加速(无头下建议) # service = Service(EdgeChromiumDriverManager().install()) # driver = webdriver.Edge(service=service, options=options) # try: # driver.get(url) # WebDriverWait(driver, 15).until( # lambda d: d.execute_script("return document.readyState") == "complete" # ) # controls = extract_controls(driver) # finally: # driver.quit() # report = generate_report(url, controls) # # 输出结果 # if args.output: # with open(args.output, 'w', encoding='utf-8') as f: # f.write(report) # print(f"人性化报告已保存到 {args.output}") # if args.json: # json_path = args.output.rsplit('.', 1)[0] + '.json' # with open(json_path, 'w', encoding='utf-8') as jf: # json.dump({ # "url": url, # "controls": controls # }, jf, ensure_ascii=False, indent=2) # print(f"机器可读 JSON 已保存到 {json_path}") # else: # print(report) # if __name__ == "__main__": # main() import base64 import io import json import requests from PIL import Image # ================= 配置 ================= IMAGE_PATH = r"C:\Users\86166\Desktop\3.png" RESIZED_PATH = r"C:\Users\86166\Desktop\3_resized.png" OUTPUT_FILE = r"C:\Users\86166\Desktop\model_output.txt" API_URL = "http://localhost:4555/v1/chat/completions" MODEL_NAME = "UI-TARS-1.5-7B" TARGET_SIZE = (1000, 1000) # 宽, 高 INSTRUCTION = "点击屏幕中央。请只输出点击的x和y坐标,格式为'x,y',不要输出任何其他内容。" # ======================================= def resize_image_force(image_path, save_path, size): """强制拉伸图片到指定尺寸(不保持比例)""" img = Image.open(image_path).convert("RGB") img_resized = img.resize(size, Image.Resampling.LANCZOS) img_resized.save(save_path, "PNG") print(f"图片已缩放至 {size[0]}×{size[1]},保存至: {save_path}") return save_path def image_to_base64(image_path): """将图片转为 base64 字符串(去除换行)""" with open(image_path, "rb") as f: img_bytes = f.read() b64 = base64.b64encode(img_bytes).decode("utf-8") return b64 def call_model(image_path, instruction): """调用 vLLM 服务""" b64 = image_to_base64(image_path) data_url = f"data:image/png;base64,{b64}" payload = { "model": MODEL_NAME, "messages": [ { "role": "user", "content": [ {"type": "image_url", "image_url": {"url": data_url}}, {"type": "text", "text": instruction} ] } ], "max_tokens": 512, "temperature": 0 } headers = {"Content-Type": "application/json"} response = requests.post(API_URL, headers=headers, json=payload, timeout=120) response.raise_for_status() return response.json() def main(): # 1. 缩放图片 resize_image_force(IMAGE_PATH, RESIZED_PATH, TARGET_SIZE) # 2. 调用模型 print("正在调用模型,请稍候...") result = call_model(RESIZED_PATH, INSTRUCTION) # 3. 提取回复文本 try: content = result["choices"][0]["message"]["content"] except (KeyError, IndexError) as e: print(f"解析响应出错:{e}") print("原始响应:", json.dumps(result, ensure_ascii=False, indent=2)) return # 4. 保存原始回复为 UTF-8 文件 with open(OUTPUT_FILE, "w", encoding="utf-8") as f: f.write(content) print(f"模型回复已保存至: {OUTPUT_FILE}") # 5. 直接打印(控制台可能显示乱码,但文件正确即可) print("\n--- 模型回复(原始内容)---") print(content) if __name__ == "__main__": main()