import sys
import json
import os
import warnings
from datetime import datetime
from PyQt5.QtCore import QUrl, Qt, QDateTime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QLineEdit, QPushButton, QListWidget, QTabWidget,
QVBoxLayout, QWidget, QHBoxLayout, QAction, QToolBar, QStatusBar,
QProgressBar, QInputDialog, QFileDialog, QMessageBox
)
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage, QWebEngineSettings
from PyQt5.QtNetwork import QNetworkProxy, QNetworkCookie # 修复代理设置导入问题
# 忽略弃用警告
warnings.filterwarnings("ignore", category=DeprecationWarning)
class WebEnginePage(QWebEnginePage):
def __init__(self, profile=None, parent=None):
"""修复参数传递问题:支持带profile和不带profile两种初始化方式"""
if profile:
super().__init__(profile, parent) # 使用profile初始化
else:
super().__init__(parent) # 标准初始化
self.parent_window = parent.parent_window if parent and hasattr(parent, 'parent_window') else None
def acceptNavigationRequest(self, url, _type, isMainFrame):
# 检测视频流协议并调用播放器
if url.scheme() in ['rtmp', 'http-flv', 'ws-flv']:
if self.parent_window:
self.parent_window.play_with_jessibuca(url.toString())
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
def createWindow(self, type):
# 在新标签页打开链接
if type == QWebEnginePage.WebBrowserTab:
if self.parent_window:
new_tab = self.parent_window.add_browser_tab("加载中...")
return new_tab.page
return super().createWindow(type)
def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
"""修复:正确映射JavaScript控制台日志级别"""
# 创建日志级别映射字典
level_map = {
QWebEnginePage.InfoMessageLevel: "INFO",
QWebEnginePage.WarningMessageLevel: "WARNING",
QWebEnginePage.ErrorMessageLevel: "ERROR"
}
# 获取可读的日志级别名称
level_str = level_map.get(level, "UNKNOWN")
log_msg = f"[JS {level_str}] {sourceID}:{lineNumber} - {message}"
if self.parent_window:
self.parent_window.status_bar.showMessage(log_msg, 5000)
print(log_msg) # 同时输出到控制台
class WebEngineView(QWebEngineView):
def __init__(self, parent=None):
super().__init__(parent)
self.parent_window = parent
# 修复:正确传递profile参数
self.page = WebEnginePage(profile=self.parent_window.profile, parent=self)
self.setPage(self.page)
self.page.loadFinished.connect(self.inject_jessibuca)
def inject_jessibuca(self):
self.page.runJavaScript("""
if (typeof window.Jessibuca === 'undefined') {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jessibuca@latest/dist/jessibuca.js';
script.onerror = () => console.error('Jessibuca加载失败');
document.head.appendChild(script);
const container = document.createElement('div');
container.id = 'jessibuca-container';
container.style.position = 'fixed';
container.style.zIndex = '99999';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '100%';
container.style.backgroundColor = 'black';
container.style.display = 'none';
document.body.appendChild(container);
}
""")
def createWindow(self, type):
# 创建新标签页
if type == QWebEnginePage.WebBrowserTab:
new_tab = WebEngineView(self.parent_window)
# 修复:直接返回page属性而非调用page()
return new_tab.page
return super().createWindow(type)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("B站多账号浏览器")
self.setMinimumSize(1200, 800)
# 初始化数据存储
self.account_db = {}
self.cookies_db = []
# 创建持久化Cookie配置
self.cookie_storage_path = "cookies_storage"
os.makedirs(self.cookie_storage_path, exist_ok=True)
self.profile = QWebEngineProfile("BiliCookieProfile", self)
self.profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)
self.profile.setPersistentStoragePath(self.cookie_storage_path)
# 关键性能优化1: 全局禁用网络代理[8](@ref)
QNetworkProxy.setApplicationProxy(QNetworkProxy(QNetworkProxy.NoProxy))
# 关键性能优化2: 启用HTTP内存缓存[9](@ref)
self.profile.setHttpCacheType(QWebEngineProfile.MemoryHttpCache)
# 启用HTML5支持的设置
profile_settings = self.profile.settings()
profile_settings.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False)
profile_settings.setAttribute(QWebEngineSettings.FullScreenSupportEnabled, True)
profile_settings.setAttribute(QWebEngineSettings.WebGLEnabled, True)
profile_settings.setAttribute(QWebEngineSettings.Accelerated2dCanvasEnabled, True)
profile_settings.setAttribute(QWebEngineSettings.PluginsEnabled, True)
profile_settings.setAttribute(QWebEngineSettings.AllowRunningInsecureContent, True)
# 主界面布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 导航工具栏
navbar = QToolBar("导航栏")
navbar.setMovable(False) # 禁止工具栏拖动提升性能
self.addToolBar(navbar)
back_btn = QAction("←", self)
back_btn.triggered.connect(self.navigate_back)
navbar.addAction(back_btn)
forward_btn = QAction("→", self)
forward_btn.triggered.connect(self.navigate_forward)
navbar.addAction(forward_btn)
reload_btn = QAction("↻", self)
reload_btn.triggered.connect(self.reload_page)
navbar.addAction(reload_btn)
home_btn = QAction("🏠", self)
home_btn.triggered.connect(self.navigate_home)
navbar.addAction(home_btn)
navbar.addSeparator()
self.url_bar = QLineEdit("https://www.bilibili.com")
self.url_bar.setPlaceholderText("输入网址或搜索内容...")
self.url_bar.returnPressed.connect(self.load_url)
navbar.addWidget(self.url_bar)
# 浏览器标签页
self.browser_tabs = QTabWidget()
self.browser_tabs.setTabsClosable(True)
self.browser_tabs.tabCloseRequested.connect(self.close_tab)
self.browser_tabs.currentChanged.connect(self.update_url_bar)
# 关键性能优化3: 启用标签页滚动
self.browser_tabs.setUsesScrollButtons(True)
main_layout.addWidget(self.browser_tabs, 8)
# 添加初始标签页
self.add_browser_tab("首页", "https://www.bilibili.com")
# 管理面板
self.tabs = QTabWidget()
self.cookie_list = QListWidget()
self.tabs.addTab(self.cookie_list, "Cookie列表")
self.account_list = QListWidget()
self.tabs.addTab(self.account_list, "账号切换")
self.account_list.itemClicked.connect(self.switch_account)
main_layout.addWidget(self.tabs, 2)
# 功能按钮
btn_layout = QHBoxLayout()
self.import_btn = QPushButton("导入Cookie")
self.import_btn.clicked.connect(self.import_cookies)
self.save_btn = QPushButton("保存账号")
self.save_btn.clicked.connect(self.save_account)
self.export_btn = QPushButton("导出Cookie")
self.export_btn.clicked.connect(self.export_cookies)
self.new_tab_btn = QPushButton("新建标签页")
self.new_tab_btn.clicked.connect(self.create_new_tab)
for btn in [self.import_btn, self.save_btn, self.export_btn, self.new_tab_btn]:
btn_layout.addWidget(btn)
main_layout.addLayout(btn_layout)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.progress_bar = QProgressBar()
self.progress_bar.setMaximum(100)
self.progress_bar.setVisible(False)
self.progress_bar.setFixedWidth(200)
self.status_bar.addPermanentWidget(self.progress_bar)
# 加载数据
self.load_accounts()
def add_browser_tab(self, title, url=None):
browser = WebEngineView(self)
browser.titleChanged.connect(lambda title, b=browser: self.update_tab_title(b, title))
browser.loadProgress.connect(self.update_progress)
browser.loadFinished.connect(self.on_load_finished)
index = self.browser_tabs.addTab(browser, title)
self.browser_tabs.setCurrentIndex(index)
# 关键性能优化4: 延迟加载提升启动速度
if url:
QApplication.processEvents() # 确保UI更新
browser.load(QUrl(url))
return browser
def update_tab_title(self, browser, title):
index = self.browser_tabs.indexOf(browser)
if index != -1:
self.browser_tabs.setTabText(index, title[:15] + "..." if len(title) > 15 else title)
def update_progress(self, progress):
self.progress_bar.setVisible(progress < 100)
self.progress_bar.setValue(progress)
def close_tab(self, index):
"""修复资源释放问题:移除多余的括号"""
if self.browser_tabs.count() > 1:
widget = self.browser_tabs.widget(index)
# 关键修复:直接访问page属性而非调用page()
page = widget.page
if page:
# 关键优化: 正确的资源释放顺序
profile = page.profile()
profile.cookieStore().deleteAllCookies()
# 先删除页面再删除视图
page.deleteLater()
widget.deleteLater()
self.browser_tabs.removeTab(index)
else:
self.create_new_tab()
def create_new_tab(self):
self.add_browser_tab("新标签页", "about:blank")
def navigate_back(self):
current_browser = self.browser_tabs.currentWidget()
if current_browser:
current_browser.back()
def navigate_forward(self):
current_browser = self.browser_tabs.currentWidget()
if current_browser:
current_browser.forward()
def reload_page(self):
current_browser = self.browser_tabs.currentWidget()
if current_browser:
current_browser.reload()
def navigate_home(self):
self.load_url("https://www.bilibili.com")
def on_load_finished(self, success):
browser = self.sender()
if browser and self.browser_tabs.currentWidget() == browser:
current_url = browser.url().toString()
self.url_bar.setText(current_url)
self.status_bar.showMessage("页面加载完成" if success else "页面加载失败", 2000)
# 关键优化: 登录页面自动处理
if "login" in current_url or "passport" in current_url:
self.status_bar.showMessage("检测到登录页面,请完成登录", 5000)
self.auto_handle_login_page(browser)
def auto_handle_login_page(self, browser):
"""自动处理登录页面的JavaScript"""
browser.page.runJavaScript("""
// 尝试自动填充已知的登录表单
const loginForm = document.querySelector('form[action*="login"]');
if (loginForm) {
// 尝试填充测试账号
const usernameInput = loginForm.querySelector('input[name="username"], input[name="user"]');
const passwordInput = loginForm.querySelector('input[name="password"]');
if (usernameInput && passwordInput) {
usernameInput.value = "test_account";
passwordInput.value = "test_password";
console.log("自动填充了登录表单");
}
}
""")
def update_url_bar(self):
current_browser = self.browser_tabs.currentWidget()
if current_browser:
current_url = current_browser.url().toString()
self.url_bar.setText(current_url)
# 登录页面特殊处理
if "login" in current_url or "passport" in current_url:
self.url_bar.setStyleSheet("background-color: #FFF8E1;")
else:
self.url_bar.setStyleSheet("")
def load_url(self, url_text=None):
if url_text is None:
url_text = self.url_bar.text().strip()
if not url_text:
return
# 关键优化: 更智能的URL处理[10](@ref)
if not url_text.startswith(("http://", "https://", "file://", "ftp://")):
if "." in url_text: # 包含域名
url_text = "https://" + url_text
else: # 可能是搜索内容
url_text = f"https://www.bilibili.com/search?keyword={url_text}"
current_browser = self.browser_tabs.currentWidget()
if current_browser:
current_browser.load(QUrl(url_text))
self.status_bar.showMessage(f"正在加载: {url_text}", 3000)
def play_with_jessibuca(self, stream_url):
"""使用Jessibuca播放视频流"""
current_browser = self.browser_tabs.currentWidget()
if not current_browser:
return
self.status_bar.showMessage("Jessibuca播放中...", 3000)
current_browser.page.runJavaScript(f"""
const container = document.getElementById('jessibuca-container');
if (container) {{
container.style.display = 'block';
if (!window.jessibucaPlayer) {{
window.jessibucaPlayer = new Jessibuca({{
container: container,
videoBuffer: 0.2,
isResize: true,
text: '直播加载中...',
decoder: 'ffmpeg.js',
forceNoOffscreen: true
}});
}}
window.jessibucaPlayer.play('{stream_url}');
}}
""")
def import_cookies(self):
"""导入Cookie JSON文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择Cookie文件", "", "JSON文件 (*.json)"
)
if file_path:
try:
with open(file_path, "r", encoding='utf-8') as f:
cookies = json.load(f)
if isinstance(cookies, list):
self.cookies_db = cookies
self.cookie_list.clear()
self.cookie_list.addItems([f"{c['name']}: {c['value'][:10]}..." for c in cookies])
self.status_bar.showMessage(f"成功导入 {len(cookies)} 个Cookie", 3000)
self.inject_cookies(cookies)
else:
QMessageBox.warning(self, "错误", "无效的Cookie格式")
except Exception as e:
QMessageBox.critical(self, "导入失败", f"错误: {str(e)}")
def export_cookies(self):
"""导出Cookie到文件"""
file_path, _ = QFileDialog.getSaveFileName(
self, "保存Cookie文件", "", "JSON文件 (*.json)"
)
if file_path:
try:
cookies = self.get_current_cookies()
with open(file_path, "w", encoding='utf-8') as f:
json.dump(cookies, f, indent=2, ensure_ascii=False)
self.status_bar.showMessage("Cookie导出成功", 3000)
except Exception as e:
QMessageBox.critical(self, "导出失败", f"错误: {str(e)}")
def get_current_cookies(self):
"""获取当前标签页的Cookie(模拟)"""
# 实际实现需要异步获取,这里简化处理
return self.cookies_db if self.cookies_db else []
def inject_cookies(self, cookies):
"""将Cookie注入当前页面 - 修复登录问题[9](@ref)"""
current_browser = self.browser_tabs.currentWidget()
if current_browser:
store = current_browser.page.profile().cookieStore()
# 先清除现有Cookie
store.deleteAllCookies()
# 异步注入新的Cookie
for cookie_data in cookies:
# 创建Qt的Cookie对象
qt_cookie = QNetworkCookie(
cookie_data['name'].encode('utf-8'),
cookie_data['value'].encode('utf-8')
)
# 设置Cookie属性
if 'domain' in cookie_data:
qt_cookie.setDomain(cookie_data['domain'])
if 'path' in cookie_data:
qt_cookie.setPath(cookie_data['path'])
if 'expiry' in cookie_data:
# 转换为QDateTime
expiry = QDateTime.fromSecsSinceEpoch(cookie_data['expiry'])
qt_cookie.setExpirationDate(expiry)
# 设置安全属性
qt_cookie.setSecure(cookie_data.get('secure', False))
qt_cookie.setHttpOnly(cookie_data.get('httpOnly', False))
# 注入Cookie
store.setCookie(qt_cookie, QUrl(cookie_data.get('url', 'https://www.bilibili.com')))
self.status_bar.showMessage("Cookie注入成功,请刷新页面", 3000)
def save_account(self):
"""保存当前账号配置"""
account_name, ok = QInputDialog.getText(
self, "保存账号", "输入账号名称:"
)
if ok and account_name:
cookies = self.get_current_cookies()
if cookies:
self.account_db[account_name] = {
"cookies": cookies,
"saved_date": datetime.now().isoformat()
}
self.account_list.addItem(account_name)
# 持久化存储账号数据
try:
with open("accounts.json", "w", encoding='utf-8') as f:
json.dump(self.account_db, f, indent=2, ensure_ascii=False)
QMessageBox.information(self, "成功", "账号保存成功")
except Exception as e:
QMessageBox.critical(self, "保存失败", f"账号保存失败: {str(e)}")
else:
QMessageBox.warning(self, "错误", "没有获取到有效Cookie")
def switch_account(self, item):
"""切换账号"""
account_name = item.text()
if account_name in self.account_db:
cookies = self.account_db[account_name].get("cookies", [])
self.inject_cookies(cookies)
self.status_bar.showMessage(f"已切换至账号: {account_name}", 3000)
# 自动刷新当前页面
self.reload_page()
else:
QMessageBox.warning(self, "错误", "找不到该账号的Cookie信息")
def load_accounts(self):
"""从文件加载保存的账号"""
try:
if os.path.exists("accounts.json"):
with open("accounts.json", "r", encoding='utf-8') as f:
self.account_db = json.load(f)
self.account_list.addItems(self.account_db.keys())
except Exception as e:
print(f"加载账号失败: {str(e)}")
def closeEvent(self, event):
"""窗口关闭时释放全局资源[8](@ref)"""
# 清理HTTP缓存和访问记录
self.profile.clearHttpCache()
self.profile.clearAllVisitedLinks()
# 保存账号数据
try:
with open("accounts.json", "w", encoding='utf-8') as f:
json.dump(self.account_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"保存账号失败: {str(e)}")
super().closeEvent(event)
if __name__ == "__main__":
# 关键性能优化: 启用WebEngine调试日志[9](@ref)
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--enable-logging"
app = QApplication(sys.argv)
# 关键性能优化: 启用Qt内置的OpenGL渲染[7](@ref)
app.setAttribute(Qt.AA_UseOpenGLES)
window = MainWindow()
window.show()
sys.exit(app.exec_())
最新发布