Files
fakabot/screenshot_utils.py

359 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""
支付页面截图工具
支持真实网页截图和备用二维码生成
"""
import os
import subprocess
import time
from io import BytesIO
from typing import Optional
# 尝试导入Selenium相关模块
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
SELENIUM_AVAILABLE = True
except ImportError:
SELENIUM_AVAILABLE = False
def setup_chrome_driver(headless: bool = True, timeout: int = 30):
"""
设置Chrome/Chromium WebDriver
Args:
headless: 是否使用无头模式
timeout: 页面加载超时时间
Returns:
WebDriver实例或None
"""
if not SELENIUM_AVAILABLE:
print("❌ Selenium不可用,使用备用二维码方案")
return None
try:
# Chrome选项配置
chrome_options = Options()
if headless:
chrome_options.add_argument('--headless')
# 基础配置
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_argument('--disable-extensions')
chrome_options.add_argument('--disable-plugins')
chrome_options.add_argument('--disable-web-security')
chrome_options.add_argument('--allow-running-insecure-content')
chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
# 尝试不同的Chrome/Chromium路径
chrome_paths = [
'/usr/bin/chromium-browser', # Alpine Chromium
'/usr/bin/chromium', # Debian Chromium
'/usr/bin/google-chrome', # Google Chrome
'/usr/bin/google-chrome-stable',
'chromium-browser',
'chromium',
'google-chrome'
]
chrome_binary = None
for path in chrome_paths:
try:
result = subprocess.run([path, '--version'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
chrome_binary = path
print(f"✅ 找到浏览器: {path} - {result.stdout.strip()}")
break
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
continue
if not chrome_binary:
print("❌ 未找到Chrome/Chromium浏览器,使用备用二维码方案")
return None
chrome_options.binary_location = chrome_binary
# 尝试使用系统chromedriver或chromium-driver
driver_paths = [
'/usr/bin/chromedriver', # Alpine chromedriver
'/usr/bin/chromium-chromedriver', # Alpine chromium-chromedriver
'/usr/bin/chromium-driver', # Debian chromium-driver
'chromedriver',
'chromium-driver'
]
driver = None
for driver_path in driver_paths:
try:
if os.path.exists(driver_path) or driver_path in ['chromedriver', 'chromium-driver']:
service = Service(driver_path) if os.path.exists(driver_path) else None
driver = webdriver.Chrome(service=service, options=chrome_options)
print(f"✅ 使用驱动: {driver_path}")
break
except Exception as e:
continue
if not driver:
# 最后尝试ChromeDriverManager
try:
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)
print("✅ 使用ChromeDriverManager")
except Exception as e:
print(f"❌ 所有驱动方式都失败: {e}")
return None
# 设置超时
driver.set_page_load_timeout(timeout)
driver.implicitly_wait(10)
return driver
except Exception as e:
print(f"❌ 浏览器驱动初始化失败: {e}")
return None
def capture_payment_qr(payment_url: str, timeout: int = 30) -> Optional[BytesIO]:
"""
截取支付页面的二维码图片
Args:
payment_url: 支付链接
timeout: 超时时间(秒)
Returns:
BytesIO: 图片数据流,失败返回None
"""
if not SELENIUM_AVAILABLE:
print("❌ Selenium不可用,跳过真实截图")
return None
driver = None
try:
driver = setup_chrome_driver()
if not driver:
return None
print(f"🔧 正在截取支付页面: {payment_url}")
driver.get(payment_url)
# 等待页面基础加载完成
wait = WebDriverWait(driver, timeout)
# 1. 等待页面DOM加载完成
try:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
print("✅ 页面DOM加载完成")
except Exception as e:
print(f"⚠️ 等待DOM加载超时: {e}")
# 2. 等待页面标题加载(确保不是空白页)
try:
wait.until(lambda d: d.title and len(d.title.strip()) > 0)
print(f"✅ 页面标题: {driver.title}")
except Exception as e:
print(f"⚠️ 页面标题加载超时: {e}")
# 3. 等待页面body内容出现
try:
wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
print("✅ 页面内容加载完成")
except Exception as e:
print(f"⚠️ 页面内容加载超时: {e}")
# 4. 额外等待确保所有内容加载完成
time.sleep(5)
# 5. 截取页面中心区域(支付核心部分)
print("📸 开始截图...")
# 先获取整个页面截图
screenshot_data = driver.get_screenshot_as_png()
if screenshot_data:
# 使用PIL裁剪中心区域
try:
from PIL import Image
# 将截图数据转换为PIL Image
full_image = Image.open(BytesIO(screenshot_data))
width, height = full_image.size
# 336x375矩形截图 - 左右减7,上面减10,下面加25
crop_width = 336
crop_height = 375
print(f"🔍 原始截图尺寸: {width}x{height}")
# 使用测试成功的简单居中策略,往上偏移避开蓝色按钮
center_x = width // 2
center_y = height // 2 - 8 # 往上偏移8像素,整体下移15像素
left = center_x - crop_width // 2 # 336/2 = 168
top = center_y - crop_height // 2 # 375/2 = 187
right = left + crop_width
bottom = top + crop_height
# 边界检查
if left < 0 or top < 0 or right > width or bottom > height:
print('⚠️ 336x375超出边界,使用最大正方形')
size = min(width, height)
left = (width - size) // 2
top = (height - size) // 2
right = left + size
bottom = top + size
print(f"✅ 居中裁剪336x375: {left},{top} -> {right},{bottom}")
print(f"✅ 裁剪尺寸: {right-left}x{bottom-top}")
# 裁剪图片
cropped_image = full_image.crop((left, top, right, bottom))
# 转换回BytesIO
cropped_buffer = BytesIO()
cropped_image.save(cropped_buffer, format='PNG')
cropped_buffer.seek(0)
print(f"✅ 真实截图成功,原始大小: {len(screenshot_data)} bytes")
print(f"✅ 裁剪后大小: {len(cropped_buffer.getvalue())} bytes")
print(f"✅ 裁剪区域: 390x390 (以二维码为中心)")
return cropped_buffer
except Exception as e:
print(f"⚠️ 图片裁剪失败,使用原始截图: {e}")
screenshot_buffer = BytesIO(screenshot_data)
return screenshot_buffer
else:
print("❌ 截图数据为空")
return None
except Exception as e:
print(f"❌ 真实截图失败: {e}")
import traceback
traceback.print_exc()
return None
finally:
if driver:
try:
driver.quit()
except Exception:
pass
def capture_payment_qr_fallback(payment_url: str) -> Optional[BytesIO]:
"""
备用截图方案:使用qrcode生成支付链接二维码
"""
try:
import qrcode
from PIL import Image, ImageDraw, ImageFont
print(f"🔧 开始生成备用二维码,URL: {payment_url}")
# 生成二维码
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(payment_url)
qr.make(fit=True)
# 创建二维码图片
qr_img = qr.make_image(fill_color="black", back_color="white")
print("✅ 二维码图片生成成功")
# 创建带说明文字的图片
img_width = 400
img_height = 500
img = Image.new('RGB', (img_width, img_height), 'white')
# 粘贴二维码
qr_img = qr_img.resize((300, 300))
img.paste(qr_img, (50, 50))
print("✅ 二维码图片合成成功")
# 添加文字说明
draw = ImageDraw.Draw(img)
try:
# 尝试使用系统字体
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 16)
except:
try:
# 尝试其他常见字体路径
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)
except:
font = ImageFont.load_default()
text = "扫描二维码完成USDT支付"
try:
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
except:
# 兼容旧版PIL
text_width = len(text) * 10
text_x = (img_width - text_width) // 2
draw.text((text_x, 380), text, fill="black", font=font)
print("✅ 文字说明添加成功")
# 保存到BytesIO
img_buffer = BytesIO()
img.save(img_buffer, format='JPEG', quality=90)
img_buffer.seek(0)
print(f"✅ 备用二维码生成成功,图片大小: {len(img_buffer.getvalue())} bytes")
return img_buffer
except Exception as e:
print(f"❌ 备用二维码生成失败: {e}")
import traceback
traceback.print_exc()
return None
def get_payment_screenshot(payment_url: str, use_fallback: bool = True) -> Optional[BytesIO]:
"""
获取支付页面截图
Args:
payment_url: 支付链接
use_fallback: 是否使用备用方案
Returns:
BytesIO: 图片数据流
"""
# 优先尝试真实网页截图
print(f"🔧 尝试真实网页截图: {payment_url}")
# 首先尝试真实截图
screenshot = capture_payment_qr(payment_url)
if screenshot:
print("✅ 真实网页截图成功")
return screenshot
# 真实截图失败时使用备用方案
if use_fallback:
print("⚠️ 真实截图失败,使用备用二维码方案")
return capture_payment_qr_fallback(payment_url)
return None