| 功能模块 | C++方案 | Python方案 | 影响程度 |
|---|---|---|---|
| 桌面窗口管理 | ✅ QMainWindow | ✅ PyQt6/PySide6 | ✅ 无影响 |
| 登录界面 | ✅ Qt Widgets/Web | ✅ PyQt6 Widgets/WebEngine | ✅ 无影响 |
| 主界面布局 | ✅ Qt Layout | ✅ PyQt6 Layout | ✅ 无影响 |
| Chrome浏览器自动化 | ✅ QWebEngineView | ✅ Selenium/Playwright | ✅ 无影响(甚至更简单) |
| Chrome插件支持 | ✅ QWebEngineProfile | ⚠️ 需要额外处理 | ⚠️ 轻微影响 |
| Steam集成 | ✅ 进程调用/API | ✅ subprocess/requests | ✅ 无影响 |
| 自动更新 | ✅ QNetworkAccessManager | ✅ requests + 自定义 | ✅ 无影响 |
| 窗口大小控制 | ✅ QMainWindow API | ✅ PyQt6 API | ✅ 无影响 |
| 多Tab页系统 | ✅ QTabWidget | ✅ QTabWidget | ✅ 无影响 |
结论:功能上基本无影响,Python完全可以实现所有需求!
| 技术组件 | 选型 | 说明 |
|---|---|---|
| 桌面框架 | PyQt6 / PySide6 | Qt的Python绑定,功能与C++版完全一致 |
| 编程语言 | Python 3.10+ | 开发效率高,代码简洁 |
| 浏览器自动化 | Selenium / Playwright | 比QWebEngine更强大的自动化能力 |
| Web界面 | QWebEngineView | 支持HTML/React界面 |
| 加密库 | cryptography / pycryptodome | Python加密库,功能完整 |
| 打包工具 | PyInstaller / cx_Freeze | 打包为exe |
| 代码保护 | PyArmor / Nuitka | 代码混淆和编译保护 |
buff-python-app/
├── requirements.txt
├── setup.py
├── main.py # 应用入口
├── src/
│ ├── core/ # 核心逻辑(Python)
│ │ ├── auth/ # 认证模块
│ │ │ ├── license.py # 许可证验证
│ │ │ ├── crypto.py # 加密算法
│ │ │ └── hwid.py # 硬件ID
│ │ ├── steam/ # Steam集成
│ │ │ ├── authenticator.py # Steam验证器
│ │ │ └── trade.py # 交易确认
│ │ ├── browser/ # 浏览器控制
│ │ │ ├── automation.py # 自动化引擎(Selenium/Playwright)
│ │ │ └── extension.py # 插件管理
│ │ └── updater/ # 更新模块
│ │ └── updater.py
│ ├── ui/ # 界面层
│ │ ├── main_window.py # 主窗口
│ │ ├── login_window.py # 登录窗口
│ │ ├── components/ # UI组件
│ │ │ ├── header.py # 顶部栏
│ │ │ ├── sidebar.py # 左侧菜单
│ │ │ └── tab_view.py # Tab页系统
│ │ └── webview.py # Web视图容器
│ └── utils/ # 工具函数
│ ├── config.py
│ └── logger.py
├── resources/
│ ├── chromium/ # Chrome浏览器
│ ├── extensions/ # Chrome插件
│ └── sda/ # Steam工具
└── dist/ # 构建输出
# main_window.py
from PyQt6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout
from PyQt6.QtCore import Qt
from ui.components.header import Header
from ui.components.sidebar import Sidebar
from ui.components.tab_view import TabView
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Buff App")
self.setMinimumSize(1000, 600)
self.resize(1200, 800)
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 顶部栏
self.header = Header()
main_layout.addWidget(self.header)
# 内容区域(左侧菜单 + 右侧Tab)
content_layout = QHBoxLayout()
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(0)
# 左侧菜单
self.sidebar = Sidebar()
content_layout.addWidget(self.sidebar, 0) # 固定宽度
# 右侧Tab页
self.tab_view = TabView()
content_layout.addWidget(self.tab_view, 1) # 可伸缩
main_layout.addLayout(content_layout)
# 连接信号
self.header.exit_clicked.connect(self.close)
self.sidebar.menu_clicked.connect(self.tab_view.add_tab)
def resize_window(self, width: int, height: int):
"""通过按钮控制窗口大小"""
self.resize(width, height)
def maximize_window(self):
"""最大化/还原窗口"""
if self.isMaximized():
self.showNormal()
else:
self.showMaximized()
# browser/automation.py
from playwright.sync_api import sync_playwright, Browser, Page, BrowserContext
from pathlib import Path
import os
import sys
from typing import Optional, List, Dict
from src.utils.logger import get_logger
logger = get_logger(__name__)
class BrowserAutomation:
def __init__(self):
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.playwright = None
def launch_browser(self, headless: bool = False, extensions: List[str] = None):
"""启动浏览器"""
try:
self.playwright = sync_playwright().start()
# Chrome路径(打包后的资源路径)
chrome_path = self._get_chrome_path()
if not os.path.exists(chrome_path):
raise FileNotFoundError(f"Chrome浏览器未找到: {chrome_path}")
# 准备启动参数
launch_args = [
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-blink-features=AutomationControlled',
'--no-first-run',
'--no-default-browser-check'
]
# 加载插件
if extensions:
for ext_path in extensions:
if os.path.exists(ext_path):
launch_args.append(f'--load-extension={ext_path}')
self.browser = self.playwright.chromium.launch(
executable_path=chrome_path,
headless=headless,
args=launch_args
)
# 创建上下文
context_options = {
'viewport': {'width': 1920, 'height': 1080},
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
self.context = self.browser.new_context(**context_options)
self.page = self.context.new_page()
logger.info("浏览器启动成功")
return self.page
except Exception as e:
logger.error(f"浏览器启动失败: {e}")
raise
def _get_chrome_path(self) -> str:
"""获取Chrome浏览器路径"""
if getattr(sys, 'frozen', False):
# 打包后的路径
base_path = sys._MEIPASS
else:
# 开发环境
base_path = Path(__file__).parent.parent.parent
# 尝试多个可能的路径
possible_paths = [
Path(base_path) / "resources" / "chromium" / "chrome.exe",
Path(base_path) / "resources" / "chromium" / "chrome-win" / "chrome.exe",
Path(os.path.dirname(sys.executable)) / "resources" / "chromium" / "chrome.exe"
]
for path in possible_paths:
if path.exists():
return str(path)
# 如果都找不到,返回默认路径
return str(possible_paths[0])
def load_extensions(self, extension_paths: List[str]):
"""加载Chrome插件"""
if not self.browser:
raise RuntimeError("浏览器未启动")
for ext_path in extension_paths:
if not os.path.exists(ext_path):
logger.warning(f"插件路径不存在: {ext_path}")
continue
# 重新创建上下文以加载插件
context_options = {
'viewport': {'width': 1920, 'height': 1080}
}
# Playwright通过启动参数加载插件,需要重启浏览器
logger.info(f"加载插件: {ext_path}")
def navigate(self, url: str, wait_until: str = "load"):
"""导航到URL"""
if not self.page:
raise RuntimeError("页面未创建")
try:
self.page.goto(url, wait_until=wait_until)
logger.info(f"导航到: {url}")
except Exception as e:
logger.error(f"导航失败: {e}")
raise
def fill_form(self, form_data: Dict[str, str]):
"""填充表单"""
if not self.page:
raise RuntimeError("页面未创建")
for selector, value in form_data.items():
try:
self.page.fill(selector, value)
logger.debug(f"填充表单字段: {selector}")
except Exception as e:
logger.error(f"填充表单失败 {selector}: {e}")
raise
def click_element(self, selector: str, timeout: int = 30000):
"""点击元素"""
if not self.page:
raise RuntimeError("页面未创建")
try:
self.page.click(selector, timeout=timeout)
logger.debug(f"点击元素: {selector}")
except Exception as e:
logger.error(f"点击元素失败 {selector}: {e}")
raise
def wait_for_element(self, selector: str, timeout: int = 30000):
"""等待元素出现"""
if not self.page:
raise RuntimeError("页面未创建")
try:
self.page.wait_for_selector(selector, timeout=timeout)
logger.debug(f"元素已出现: {selector}")
except Exception as e:
logger.error(f"等待元素超时 {selector}: {e}")
raise
def execute_script(self, script: str):
"""执行JavaScript"""
if not self.page:
raise RuntimeError("页面未创建")
try:
result = self.page.evaluate(script)
logger.debug("执行JavaScript脚本")
return result
except Exception as e:
logger.error(f"执行脚本失败: {e}")
raise
def screenshot(self, path: str, full_page: bool = False):
"""截图"""
if not self.page:
raise RuntimeError("页面未创建")
try:
self.page.screenshot(path=path, full_page=full_page)
logger.info(f"截图保存: {path}")
except Exception as e:
logger.error(f"截图失败: {e}")
raise
def get_page_content(self) -> str:
"""获取页面内容"""
if not self.page:
raise RuntimeError("页面未创建")
return self.page.content()
def close(self):
"""关闭浏览器"""
try:
if self.page:
self.page.close()
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
logger.info("浏览器已关闭")
except Exception as e:
logger.error(f"关闭浏览器失败: {e}")
# steam/authenticator.py
import subprocess
import json
import hmac
import hashlib
import time
from pathlib import Path
from cryptography.fernet import Fernet
class SteamAuthenticator:
def __init__(self):
self.sda_path = self._get_sda_path()
self.accounts = {}
def _get_sda_path(self) -> str:
"""获取SDA路径"""
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(__file__)
sda_path = Path(base_path) / "resources" / "sda" / "SteamDesktopAuthenticator.exe"
return str(sda_path)
def get_guard_code(self, username: str) -> str:
"""获取Steam Guard验证码"""
# 方法1:调用SDA命令行
try:
result = subprocess.run(
[self.sda_path, '--user', username, '--get-code'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return result.stdout.strip()
except Exception as e:
print(f"SDA调用失败: {e}")
# 方法2:读取配置文件自行生成
return self._generate_code_from_config(username)
def _generate_code_from_config(self, username: str) -> str:
"""从SDA配置文件生成验证码"""
config_path = Path.home() / "AppData" / "Roaming" / "SteamDesktopAuthenticator" / "maFiles" / f"{username}.maFile"
if not config_path.exists():
return ""
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
shared_secret = config.get('shared_secret', '')
if not shared_secret:
return ""
# Steam TOTP算法
return self._generate_steam_totp(shared_secret)
def _generate_steam_totp(self, shared_secret: str) -> str:
"""生成Steam TOTP验证码"""
steam_chars = "23456789BCDFGHJKMNPQRTVWXY"
current_time = int(time.time())
time_code = current_time // 30
# 转换为大端字节序
time_buffer = time_code.to_bytes(8, byteorder='big')
# HMAC-SHA1
secret_bytes = base64.b64decode(shared_secret)
hmac_digest = hmac.new(secret_bytes, time_buffer, hashlib.sha1).digest()
# 生成5位代码
start = hmac_digest[19] & 0x0F
full_code = int.from_bytes(hmac_digest[start:start+4], byteorder='big') & 0x7FFFFFFF
code = ""
for _ in range(5):
code += steam_chars[full_code % len(steam_chars)]
full_code //= len(steam_chars)
return code
def confirm_trade(self, username: str, trade_id: str) -> bool:
"""确认Steam交易"""
try:
result = subprocess.run(
[self.sda_path, '--confirm-trade', trade_id, '--user', username],
capture_output=True,
text=True,
timeout=10
)
return result.returncode == 0
except Exception as e:
print(f"交易确认失败: {e}")
return False
# core/auth/license.py
import hashlib
import hmac
import requests
import platform
import subprocess
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
class LicenseValidator:
def __init__(self):
self.api_url = "https://your-api.com/validate"
self.public_key = self._load_public_key()
def get_hardware_id(self) -> str:
"""获取硬件ID"""
components = []
# CPU ID
try:
result = subprocess.run(
['wmic', 'cpu', 'get', 'ProcessorId'],
capture_output=True,
text=True
)
cpu_id = result.stdout.strip().split('\n')[1].strip()
components.append(cpu_id)
except:
components.append(platform.processor())
# 主板序列号
try:
result = subprocess.run(
['wmic', 'baseboard', 'get', 'SerialNumber'],
capture_output=True,
text=True
)
board_id = result.stdout.strip().split('\n')[1].strip()
components.append(board_id)
except:
components.append(platform.machine())
# MAC地址
import uuid
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
for elements in range(0,2*6,2)][::-1])
components.append(mac)
# 生成硬件ID
combined = '|'.join(components)
hwid = hashlib.sha256(combined.encode()).hexdigest()
return hwid
def validate_online(self, license_key: str) -> bool:
"""在线验证许可证"""
hwid = self.get_hardware_id()
try:
response = requests.post(
self.api_url,
json={
'license': license_key,
'hwid': hwid,
'version': '1.0.0'
},
timeout=10
)
if response.status_code == 200:
data = response.json()
# 验证服务器签名
if data.get('valid') and self._verify_signature(data):
return True
except Exception as e:
print(f"在线验证失败: {e}")
return False
def _verify_signature(self, data: dict) -> bool:
"""验证RSA签名"""
try:
message = data['data'].encode()
signature = base64.b64decode(data['signature'])
self.public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except:
return False
def _load_public_key(self):
"""加载RSA公钥"""
# 从资源文件或代码中加载公钥
public_key_pem = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
"""
# 解析并返回公钥对象
# ... 实现细节
pass
| 方面 | Python优势 |
|---|---|
| 开发效率 | ⭐⭐⭐⭐⭐ 代码简洁,开发速度快 |
| 浏览器自动化 | ⭐⭐⭐⭐⭐ Selenium/Playwright生态成熟,功能强大 |
| 第三方库 | ⭐⭐⭐⭐⭐ PyPI库丰富,Steam API、加密等都有现成库 |
| 调试便利 | ⭐⭐⭐⭐⭐ Python调试工具完善,开发体验好 |
| 学习曲线 | ⭐⭐⭐⭐⭐ Python语法简单,团队上手快 |
| 方面 | Python劣势 | 影响程度 |
|---|---|---|
| 安全性 | ⭐⭐ Python容易被反编译 | ⚠️ 中等影响(需用PyArmor/Nuitka保护) |
| 性能 | ⭐⭐⭐ 比C++慢,但桌面应用足够 | ✅ 影响较小 |
| 打包体积 | ⭐⭐⭐ 包含Python运行时,体积较大 | ⚠️ 轻微影响(约50-100MB) |
| 启动速度 | ⭐⭐⭐ 比C++慢,但可接受 | ✅ 影响较小 |
| Chrome插件加载 | ⭐⭐⭐ 需要额外处理 | ⚠️ 轻微影响(Playwright支持) |
# 安装
pip install pyarmor
# 混淆保护
pyarmor gen --enable-rft main.py
# 打包
pyinstaller --onefile --key=your-key main.py
优点:
缺点:
# 安装
pip install nuitka
# 编译为exe(会先编译为C++再编译为二进制)
python -m nuitka --onefile --windows-icon=icon.ico main.py
优点:
缺点:
# 1. 核心敏感代码用Nuitka编译为扩展模块
# core/auth/license_core.py -> license_core.pyd
# 2. 其他代码用PyArmor保护
pyarmor gen --enable-rft src/
# 3. 打包
pyinstaller --onefile main.py
# requirements.txt
PyQt6==6.6.0
playwright==1.40.0
cryptography==41.0.7
requests==2.31.0
# setup.py
from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('PyQt6')
datas += [('resources', 'resources')]
# 打包命令
pyinstaller --onefile \
--windowed \
--name="BuffApp" \
--icon="resources/icon.ico" \
--add-data="resources;resources" \
--hidden-import=PyQt6 \
--hidden-import=playwright \
main.py
# 在PyInstaller spec文件中配置
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[
('resources/chromium', 'resources/chromium'),
('resources/extensions', 'resources/extensions'),
('resources/sda', 'resources/sda'),
],
hiddenimports=['PyQt6', 'playwright'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
| 操作 | C++方案 | Python方案 | 差异 |
|---|---|---|---|
| 应用启动 | ~0.5秒 | ~1-2秒 | 慢1-1.5秒 |
| 窗口响应 | 即时 | 即时 | 无差异 |
| 浏览器启动 | ~1秒 | ~1-2秒 | 慢0.5-1秒 |
| 自动化操作 | 快速 | 快速 | 无差异 |
| 内存占用 | ~100MB | ~150-200MB | 多50-100MB |
结论:性能差异在桌面应用中可接受,用户体验无明显影响。
PyQt6 / PySide6 # 桌面框架
Playwright # 浏览器自动化(比Selenium更现代)
cryptography # 加密库
requests # HTTP请求
PyInstaller # 打包工具
Nuitka # 核心模块编译保护
PyArmor # 代码混淆
结论:Python完全可以替代C++,功能无影响,开发效率更高!