Files
auto_agent/jingtai.py
T

307 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()