Files
web-capture-api/app.py
2026-07-07 13:23:36 +08:00

587 lines
20 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.
#!/home/hz1/miniconda3/envs/openclaw/bin/python3.12
"""
Web Capture API - 网页截图与代码提取服务
使用 agent-browser (Rust + Playwright) 或 Playwright
"""
import os
import json
import asyncio
import subprocess
import tempfile
import uuid
import time
from pathlib import Path
from flask import Flask, request, jsonify, send_file, render_template
from flask_cors import CORS
from datetime import datetime
# 检查 Playwright 是否可用
try:
from playwright.async_api import async_playwright
PLAYWRIGHT_AVAILABLE = True
# 尝试导入 stealth 反检测
try:
from playwright_stealth import stealth_async
STEALTH_AVAILABLE = True
except ImportError:
STEALTH_AVAILABLE = False
print("⚠️ playwright-stealth 未安装,反爬虫能力降低")
except ImportError:
PLAYWRIGHT_AVAILABLE = False
STEALTH_AVAILABLE = False
print("⚠️ Playwright 未安装,请运行: pip install playwright && playwright install chromium")
# 检查 agent-browser 是否可用
def check_agent_browser():
try:
result = subprocess.run(
["which", "agent-browser"],
capture_output=True,
text=True
)
return result.returncode == 0
except:
return False
AGENT_BROWSER_AVAILABLE = check_agent_browser()
app = Flask(__name__)
CORS(app)
# 配置
CAPTURE_DIR = Path(tempfile.gettempdir()) / "web_captures"
CAPTURE_DIR.mkdir(exist_ok=True)
# agent-browser socket 目录
os.environ.setdefault("AGENT_BROWSER_SOCKET_DIR", "/tmp/agent-browser-sockets")
Path(os.environ["AGENT_BROWSER_SOCKET_DIR"]).mkdir(exist_ok=True, parents=True)
class AgentBrowserSession:
"""agent-browser 会话管理器"""
def __init__(self, session_id=None):
self.session_id = session_id or str(uuid.uuid4())[:8]
self.base_cmd = ["agent-browser", "--session", self.session_id]
def run(self, cmd, timeout=60000):
"""执行 agent-browser 命令"""
full_cmd = self.base_cmd + cmd
# 设置必要的环境变量
env = os.environ.copy()
env['AGENT_BROWSER_SOCKET_DIR'] = '/tmp/agent-browser-sockets'
env['AGENT_BROWSER_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
try:
result = subprocess.run(
full_cmd,
capture_output=True,
text=True,
timeout=timeout / 1000,
env=env
)
return result.returncode == 0, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return False, "", "Command timeout"
except Exception as e:
return False, "", str(e)
def open(self, url):
"""打开网页"""
# 添加反爬虫检测的 User-Agent
env = os.environ.copy()
env['AGENT_BROWSER_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
return self.run(["open", url], timeout=30000)
def set_viewport(self, width, height):
"""设置视口大小"""
return self.run(["set", "viewport", str(width), str(height)])
def screenshot(self, output_path, full_page=False):
"""截图"""
cmd = ["screenshot", str(output_path)]
if full_page:
cmd.append("--full")
return self.run(cmd, timeout=30000)
def get_html(self):
"""获取HTML"""
success, output, error = self.run(
["eval", "document.documentElement.outerHTML"],
timeout=10000
)
return success, output, error
def scroll_down(self, pixels=800):
"""向下滚动"""
return self.run(["scroll", "down", str(pixels)])
def wait(self, ms):
"""等待"""
return self.run(["wait", str(ms)])
def close(self):
"""关闭浏览器"""
try:
self.run(["close"])
except:
pass
def capture_with_agent_browser(
url: str,
action: str = "screenshot",
scroll_times: int = 0,
scroll_delay: int = 1000,
full_page: bool = False,
viewport: dict = None,
wait_time: int = 2000
):
"""
使用 agent-browser 捕获网页
"""
session = AgentBrowserSession()
temp_file = None
try:
# 设置视口
if viewport:
session.set_viewport(viewport.get("width", 1920), viewport.get("height", 1080))
# 打开网页
success, stdout, stderr = session.open(url)
if not success:
# 检查是否是反爬虫拦截
if "403" in stdout or "Access Denied" in stdout:
return {"success": False, "error": "网站反爬虫拦截 (403),建议使用 Playwright 后端或手动添加请求头"}
return {"success": False, "error": f"Failed to open URL: {stderr}"}
# 等待页面加载
session.wait(wait_time)
# 滚动加载动态内容
if scroll_times > 0:
for i in range(scroll_times):
session.scroll_down(800)
session.wait(scroll_delay)
# 执行操作
if action == "screenshot":
temp_file = CAPTURE_DIR / f"{session.session_id}.png"
success, _, error = session.screenshot(temp_file, full_page=full_page)
if success and temp_file.exists():
return {"success": True, "file_path": str(temp_file)}
else:
return {"success": False, "error": f"Screenshot failed: {error}"}
elif action == "html":
success, html, error = session.get_html()
if success:
return {"success": True, "html": html}
else:
return {"success": False, "error": f"Get HTML failed: {error}"}
else:
return {"success": False, "error": f"Unknown action: {action}"}
except Exception as e:
return {"success": False, "error": str(e)}
finally:
session.close()
async def capture_with_playwright(
url: str,
action: str = "screenshot",
scroll_times: int = 0,
scroll_delay: int = 1000,
full_page: bool = False,
viewport: dict = None,
wait_time: int = 2000
):
"""
使用 Playwright 捕获网页
Playwright 有更强的反爬虫能力
"""
if not PLAYWRIGHT_AVAILABLE:
return {"success": False, "error": "Playwright not installed. Run: pip install playwright && playwright install chromium"}
session_id = str(uuid.uuid4())[:8]
try:
async with async_playwright() as p:
# 启动浏览器,添加反检测配置
# 尝试使用真实 Chrome如果安装了
try:
# 首先尝试使用真实 Chrome 浏览器
browser = await p.chromium.launch(
headless=True,
channel='chrome', # 使用真实 Chrome 而不是 Chromium
args=[
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox',
'--ignore-certificate-errors',
]
)
except:
# 如果没有 Chrome使用 Chromium
browser = await p.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox',
'--ignore-certificate-errors',
]
)
viewport_settings = viewport or {"width": 1920, "height": 1080}
# 创建上下文,模拟真实浏览器
context = await browser.new_context(
viewport={
"width": viewport_settings.get("width", 1920),
"height": viewport_settings.get("height", 1080)
},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36',
# 添加更多浏览器特征
locale='en-US',
timezone_id='America/New_York',
color_scheme='light',
has_touch=False,
is_mobile=False,
java_script_enabled=True,
accept_downloads=True,
)
# 注入反检测脚本
await context.add_init_script("""
// 移除 webdriver 标记
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// 添加插件
Object.defineProperty(navigator, 'plugins', {
get: () => {
return [
{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format'},
{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgncjokjjpdkfnmlglggk', description: ''},
{name: 'Native Client', filename: 'internal-nacl-plugin', description: ''}
];
}
});
// 语言列表
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en', 'zh-CN']
});
// 平台
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32'
});
// 硬件并发
Object.defineProperty(navigator, 'hardwareConcurrency', {
get: () => 8
});
// 设备内存
Object.defineProperty(navigator, 'deviceMemory', {
get: () => 8
});
// Chrome 对象
window.chrome = {
runtime: {
PlatformOs: {MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd'},
PlatformArch: {ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64'},
PlatformNaclArch: {ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64'},
Request: function() {},
connect: function() {},
sendMessage: function() {}
},
loadTimes: function() {},
csi: function() {},
app: {}
};
""")
page = await context.new_page()
# 应用 stealth 反检测(如果可用)
if STEALTH_AVAILABLE:
await stealth_async(page)
try:
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
except Exception as e:
# 如果 domcontentloaded 超时,尝试 commit
try:
await page.goto(url, wait_until="commit", timeout=30000)
except:
pass
# 固定等待时间(用于验证、加载等过程)
if wait_time > 0:
await page.wait_for_timeout(wait_time)
# 等待页面基本稳定
try:
await page.wait_for_load_state("networkidle", timeout=5000)
except:
# networkidle 超时没关系,继续执行
pass
if scroll_times > 0:
for i in range(scroll_times):
await page.evaluate("window.scrollBy(0, 800)")
await page.wait_for_timeout(scroll_delay)
await page.wait_for_timeout(500)
result = {}
if action == "screenshot":
temp_file = CAPTURE_DIR / f"{session_id}.png"
await page.screenshot(path=str(temp_file), full_page=full_page)
result = {"success": True, "file_path": str(temp_file)}
elif action == "html":
html = await page.content()
result = {"success": True, "html": html}
else:
result = {"success": False, "error": f"Unknown action: {action}"}
await browser.close()
return result
except Exception as e:
return {"success": False, "error": str(e)}
async def capture_with_cdp(
url_hint: str = "",
action: str = "screenshot",
cdp_port: int = 9222
):
"""
使用 CDP 连接到已打开的 Chrome 浏览器
用于方案三:手动验证后自动截图
"""
if not PLAYWRIGHT_AVAILABLE:
return {"success": False, "error": "Playwright not installed"}
try:
async with async_playwright() as p:
# 连接到已运行的 Chrome
browser = await p.chromium.connect_over_cdp(f"http://localhost:{cdp_port}")
# 获取所有页面
contexts = browser.contexts
result_data = []
for context in contexts:
for page in context.pages:
current_url = page.url
title = await page.title()
# 如果 URL 包含提示词或者是唯一页面
if url_hint in current_url or not url_hint:
if action == "screenshot":
temp_file = CAPTURE_DIR / f"cdp_capture_{uuid.uuid4()[:8]}.png"
await page.screenshot(path=str(temp_file), full_page=True)
result_data.append({
"url": current_url,
"title": title,
"file_path": str(temp_file)
})
elif action == "html":
html = await page.content()
result_data.append({
"url": current_url,
"title": title,
"html": html
})
if result_data:
return {"success": True, "pages": result_data}
else:
return {"success": False, "error": "No matching page found"}
except Exception as e:
return {"success": False, "error": f"CDP connection failed: {str(e)}"}
def capture_webpage(
url: str,
action: str = "screenshot",
scroll_times: int = 0,
scroll_delay: int = 1000,
full_page: bool = False,
viewport: dict = None,
wait_time: int = 2000,
backend: str = "auto"
):
"""
捕获网页
Args:
backend: "auto", "agent-browser", "playwright"
"""
# 选择后端
if backend == "auto":
if AGENT_BROWSER_AVAILABLE:
backend = "agent-browser"
elif PLAYWRIGHT_AVAILABLE:
backend = "playwright"
else:
return {"success": False, "error": "No browser backend available. Install agent-browser or playwright."}
# 使用对应后端
if backend == "agent-browser":
if not AGENT_BROWSER_AVAILABLE:
return {"success": False, "error": "agent-browser not available"}
return capture_with_agent_browser(
url, action, scroll_times, scroll_delay, full_page, viewport, wait_time
)
elif backend == "playwright":
if not PLAYWRIGHT_AVAILABLE:
return {"success": False, "error": "Playwright not available"}
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(
capture_with_playwright(
url, action, scroll_times, scroll_delay, full_page, viewport, wait_time
)
)
loop.close()
return result
except Exception as e:
return {"success": False, "error": str(e)}
else:
return {"success": False, "error": f"Unknown backend: {backend}"}
@app.route('/')
def index():
"""主页"""
return render_template('index.html')
@app.route('/api')
def api_info():
"""API信息"""
return jsonify({
"service": "Web Capture API",
"version": "2.0.0",
"backends": {
"agent-browser": "available" if AGENT_BROWSER_AVAILABLE else "unavailable",
"playwright": "available" if PLAYWRIGHT_AVAILABLE else "unavailable"
},
"endpoints": {
"/api/capture": "POST - Capture webpage screenshot or HTML",
"/health": "GET - Health check"
}
})
@app.route('/health')
def health():
"""健康检查"""
status = "healthy" if (AGENT_BROWSER_AVAILABLE or PLAYWRIGHT_AVAILABLE) else "degraded"
return jsonify({
"status": status,
"agent-browser": "installed" if AGENT_BROWSER_AVAILABLE else "not found",
"playwright": "installed" if PLAYWRIGHT_AVAILABLE else "not found",
"timestamp": datetime.now().isoformat()
})
@app.route('/api/capture', methods=['POST'])
def capture():
"""
捕获网页接口
请求体:
{
"url": "https://example.com",
"action": "screenshot" | "html",
"scroll_times": 0,
"scroll_delay": 1000,
"full_page": false,
"viewport": {"width": 1920, "height": 1080},
"wait_time": 2000,
"backend": "auto" | "agent-browser" | "playwright" | "chrome-cdp",
"cdp_port": 9222 // 用于 chrome-cdp 后端,连接到已打开的 Chrome
}
"""
data = request.get_json()
if not data:
return jsonify({"success": False, "error": "No JSON data provided"}), 400
url = data.get("url")
if not url:
return jsonify({"success": False, "error": "URL is required"}), 400
# 添加协议前缀
if not url.startswith(("http://", "https://")):
url = "https://" + url
action = data.get("action", "screenshot")
scroll_times = int(data.get("scroll_times", 0))
scroll_delay = int(data.get("scroll_delay", 1000))
full_page = bool(data.get("full_page", False))
viewport = data.get("viewport")
wait_time = int(data.get("wait_time", 2000))
backend = data.get("backend", "auto")
cdp_port = int(data.get("cdp_port", 9222))
url_hint = data.get("url_hint", "")
result = capture_webpage(
url=url,
action=action,
scroll_times=scroll_times,
scroll_delay=scroll_delay,
full_page=full_page,
viewport=viewport,
wait_time=wait_time,
backend=backend,
)
if not result["success"]:
return jsonify(result), 400
if action == "screenshot":
file_path = result["file_path"]
return send_file(file_path, mimetype='image/png')
elif action == "html":
return jsonify(result)
if __name__ == '__main__':
print("🌐 Web Capture API Starting...")
print(f"📁 Capture directory: {CAPTURE_DIR}")
print(f"🔧 Backends:")
print(f" - agent-browser: {'' if AGENT_BROWSER_AVAILABLE else ''}")
print(f" - playwright: {'' if PLAYWRIGHT_AVAILABLE else ''}")
if not AGENT_BROWSER_AVAILABLE and not PLAYWRIGHT_AVAILABLE:
print("\n⚠️ 没有可用的浏览器后端,请安装其中一个:")
print(" agent-browser: npm install -g agent-browser && agent-browser install")
print(" playwright: pip install playwright && playwright install chromium")
print("\n🚀 Server running on http://0.0.0.0:16025")
app.run(host='0.0.0.0', port=16025, debug=True)