import sys
import json
import csv
import os
import time
import re
import requests
import concurrent.futures
from urllib.parse import urlparse, urljoin
from requests.exceptions import RequestException
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
QLabel, QLineEdit, QPushButton, QTextEdit, QTableWidget, QTableWidgetItem,
QTreeWidget, QTreeWidgetItem, QHeaderView, QFileDialog, QMessageBox,
QSplitter, QSpinBox, QAction, QDialog, QFormLayout, QDialogButtonBox,
QProgressBar, QGroupBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QIcon
# 忽略SSL证书验证的警告信息
requests.packages.urllib3.disable_warnings()
class FingerprintManager:
def __init__(self):
self.fingerprints = []
self.default_file_path = os.path.join(os.path.expanduser("~"), "cms_fingerprints.json")
if not self.load_from_default_file():
self.load_default_fingerprints()
def load_default_fingerprints(self):
# 优化默认指纹库,确保正则表达式正确
self.fingerprints = [
{
"cms": "WordPress",
"version": "",
"confidence": 0,
"http_headers": [
{"header": "X-Powered-By", "pattern": "PHP/.*", "score": 10, "type": "general"}
],
"html_content": [
{"pattern": "<meta name=\"generator\" content=\"WordPress ([\\d.]+)\"",
"score": 150, "type": "core", "version_group": 1},
{"pattern": "wp-content/themes/([^/]+)", "score": 80, "type": "specific"},
{"pattern": "wp-includes/js/wp-util.js", "score": 90, "type": "specific"}
],
"url_paths": [
{"path": "/wp-admin", "score": 80, "type": "specific"},
{"path": "/wp-login.php", "score": 100, "type": "core"}
]
},
{
"cms": "示例站点",
"version": "",
"confidence": 0,
"html_content": [
{"pattern": "恭喜, 站点创建成功!", "score": 120, "type": "core"},
{"pattern": "<h3>这是默认index.html,本页面由系统自动生成</h3>", "score": 100, "type": "core"}
],
"url_paths": []
},
{
"cms": "Nginx",
"version": "",
"confidence": 0,
"http_headers": [
{"header": "Server", "pattern": "nginx/([\\d.]+)", "score": 90, "type": "core", "version_group": 1}
],
"html_content": [
{"pattern": "If you see this page, the nginx web server is successfully installed", "score": 120, "type": "core"}
]
},
{
"cms": "Drupal",
"version": "",
"html_content": [
{"pattern": "<meta name=\"generator\" content=\"Drupal ([\\d.]+)\"",
"score": 150, "type": "core", "version_group": 1},
{"pattern": "sites/default/files", "score": 70, "type": "specific"}
],
"url_paths": [
{"path": "/sites/all", "score": 80, "type": "specific"}
]
},
{
"cms": "ThinkPHP",
"version": "",
"html_content": [
{"pattern": "think\\\\Exception", "score": 100, "type": "core"},
{"pattern": "app\\\\controller", "score": 80, "type": "specific"}
]
},
{
"cms": "Yii",
"version": "",
"html_content": [
{"pattern": "yii\\\\base\\\\Exception", "score": 100, "type": "core"},
{"pattern": "yii\\\\web\\\\HttpException", "score": 90, "type": "specific"}
]
},
{
"cms": "Phalcon",
"version": "",
"html_content": [
{"pattern": "Phalcon\\\\Exception", "score": 100, "type": "core"}
]
},
{
"cms": "FuelPHP",
"version": "",
"html_content": [
{"pattern": "Fuel\\\\Exception", "score": 100, "type": "core"}
]
},
{
"cms": "Habari",
"version": "",
"html_content": [
{"pattern": "Habari\\\\Core\\\\Exception", "score": 100, "type": "core"}
]
},
{
"cms": "帝国CMS",
"version": "",
"html_content": [
{"pattern": "ecmsinfo\\(", "score": 100, "type": "core"}
]
}
]
self.save_to_default_file()
def load_from_default_file(self):
try:
if os.path.exists(self.default_file_path):
with open(self.default_file_path, 'r', encoding='utf-8') as f:
loaded_data = json.load(f)
valid_fingerprints = []
for fp in loaded_data:
if self._is_valid_fingerprint(fp):
cleaned_fp = self._clean_fingerprint(fp)
valid_fingerprints.append(cleaned_fp)
else:
print(f"跳过无效指纹: {fp}")
self.fingerprints = valid_fingerprints
return True
return False
except Exception as e:
print(f"从默认文件加载指纹失败: {e}")
return False
def _clean_fingerprint(self, fp):
"""清理指纹中的正则表达式,修复常见错误"""
for header in fp.get('http_headers', []):
if 'pattern' in header:
header['pattern'] = self._fix_regex_pattern(header['pattern'])
for html in fp.get('html_content', []):
if 'pattern' in html:
html['pattern'] = self._fix_regex_pattern(html['pattern'])
for url in fp.get('url_paths', []):
if 'pattern' in url:
url['pattern'] = self._fix_regex_pattern(url['pattern'])
return fp
def _fix_regex_pattern(self, pattern):
"""修复常见的正则表达式错误"""
if not pattern:
return ""
# 修复未转义的反斜杠
fixed = re.sub(r'(?<!\\)\\(?!["\\/])', r'\\\\', pattern)
# 修复未闭合的括号
open_count = fixed.count('(')
close_count = fixed.count(')')
if open_count > close_count:
fixed += ')' * (open_count - close_count)
# 修复不完整的字符类
if '[' in fixed and ']' not in fixed:
fixed += ']'
return fixed
def _is_valid_fingerprint(self, fp):
required_fields = ["cms"]
for field in required_fields:
if field not in fp:
return False
if not fp["cms"].strip():
return False
for key in ["http_headers", "html_content", "url_paths"]:
if key not in fp:
fp[key] = []
return True
def save_to_default_file(self):
try:
dir_path = os.path.dirname(self.default_file_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(self.default_file_path, 'w', encoding='utf-8') as f:
json.dump(self.fingerprints, f, indent=4, ensure_ascii=False)
return True
except Exception as e:
print(f"保存指纹到默认文件失败: {e}")
return False
def add_fingerprint(self, fingerprint):
if self._is_valid_fingerprint(fingerprint):
cleaned = self._clean_fingerprint(fingerprint)
self.fingerprints.append(cleaned)
self.save_to_default_file()
return True
print(f"无法添加无效指纹: {fingerprint}")
return False
def remove_fingerprint(self, index):
if 0 <= index < len(self.fingerprints):
self.fingerprints.pop(index)
self.save_to_default_file()
def update_fingerprint(self, index, fingerprint):
if 0 <= index < len(self.fingerprints) and self._is_valid_fingerprint(fingerprint):
cleaned = self._clean_fingerprint(fingerprint)
self.fingerprints[index] = cleaned
self.save_to_default_file()
return True
return False
def clear_fingerprints(self):
self.fingerprints = []
self.save_to_default_file()
return True
def restore_default_fingerprints(self):
self.load_default_fingerprints()
return True
def get_fingerprints(self):
return self.fingerprints
def export_fingerprints(self, filename):
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.fingerprints, f, indent=4, ensure_ascii=False)
return True
except Exception as e:
print(f"导出失败: {e}")
return False
def import_fingerprints(self, filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
imported_data = json.load(f)
valid_fingerprints = []
for fp in imported_data:
if self._is_valid_fingerprint(fp):
cleaned = self._clean_fingerprint(fp)
valid_fingerprints.append(cleaned)
else:
print(f"导入时跳过无效指纹: {fp}")
if valid_fingerprints:
self.fingerprints = valid_fingerprints
self.save_to_default_file()
return True
print("导入的指纹全部无效")
return False
except Exception as e:
print(f"导入失败: {e}")
return False
class DetectionWorker(QThread):
progress_signal = pyqtSignal(int, int, str)
result_signal = pyqtSignal(dict)
log_signal = pyqtSignal(str)
finished_signal = pyqtSignal()
def __init__(self, urls, fingerprints, max_threads=10, retry_count=2):
super().__init__()
self.urls = urls
self.fingerprints = fingerprints
self.max_threads = max_threads
self.running = True
self.retry_count = retry_count
self.timeout = 15 # 超时时间(秒)
# 缓存响应以提高性能
self.response_cache = {}
def run(self):
self.log_signal.emit("开始检测...")
total = len(self.urls)
for i, url in enumerate(self.urls):
if not self.running:
break
self.progress_signal.emit(i+1, total, url)
result = self.detect_cms(url)
self.result_signal.emit(result)
self.log_signal.emit("检测完成!")
self.finished_signal.emit()
def stop(self):
self.running = False
def preprocess_html(self, html):
"""优化HTML预处理:保留标签结构,不过度压缩"""
processed = re.sub(r'\n\s+', '\n', html)
processed = re.sub(r'>\s+<', '><', processed)
return processed.strip()
def escape_special_chars(self, pattern):
"""安全转义正则特殊字符"""
if not pattern:
return ""
safe_pattern = re.sub(r'\\(?![\\.*+?^${}()|[\]sSdDwWtnbfvr])', r'\\\\', pattern)
return safe_pattern
def validate_regex(self, pattern):
"""验证正则表达式是否有效"""
if not pattern:
return True, pattern
try:
re.compile(pattern)
return True, pattern
except re.error as e:
fixed = pattern
if "bad escape" in str(e):
fixed = re.sub(r'(?<!\\)\\(?!["\\/])', r'\\\\', pattern)
elif "unterminated subpattern" in str(e):
open_count = pattern.count('(')
close_count = pattern.count(')')
if open_count > close_count:
fixed = pattern + ')' * (open_count - close_count)
try:
re.compile(fixed)
self.log_signal.emit(f"自动修复正则表达式: {pattern} -> {fixed}")
return True, fixed
except re.error:
return False, pattern
def extract_version(self, content, pattern, group_idx):
"""从匹配结果中提取版本号"""
if not pattern or group_idx is None:
return ""
try:
match = re.search(pattern, content, re.IGNORECASE)
if match and len(match.groups()) >= group_idx:
return match.group(group_idx).strip()
except re.error as e:
self.log_signal.emit(f"版本提取正则错误 {pattern}: {str(e)}")
return ""
def fetch_url_content(self, url):
"""带重试机制的URL内容获取"""
# 检查缓存
if url in self.response_cache:
return self.response_cache[url]
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
for attempt in range(self.retry_count + 1):
try:
response = requests.get(
url,
headers=headers,
allow_redirects=True,
verify=False,
timeout=self.timeout
)
response.encoding = response.apparent_encoding
# 缓存响应
self.response_cache[url] = response
return response
except RequestException as e:
self.log_signal.emit(f"请求尝试 {attempt+1} 失败: {str(e)}")
if attempt >= self.retry_count:
return None
time.sleep(1)
return None
def build_full_url(self, base_url, path):
"""构建完整的URL"""
if not path.startswith('/'):
path = '/' + path
parsed = urlparse(base_url)
return f"{parsed.scheme}://{parsed.netloc}{path}"
def check_url_path(self, base_url, path, pattern, item_score, weight):
"""检查URL路径特征 - 主动访问并验证"""
full_url = self.build_full_url(base_url, path)
feature_desc = f"URL路径: {full_url}"
# 尝试获取响应
response = self.fetch_url_content(full_url)
if response and response.status_code == 200:
# 如果有正则模式,检查内容
if pattern:
is_valid, fixed_pattern = self.validate_regex(pattern)
if is_valid:
try:
if re.search(fixed_pattern, response.text, re.IGNORECASE):
return True, feature_desc, item_score * weight
except re.error as e:
self.log_signal.emit(f"URL路径正则错误: {str(e)}")
# 如果没有正则模式,只要状态200就算匹配
else:
return True, feature_desc, item_score * weight
return False, feature_desc, 0
def detect_cms(self, url):
original_url = url
if not url.startswith(('http://', 'https://')):
urls_to_try = [f'http://{url}', f'https://{url}']
else:
urls_to_try = [url]
response = None
for test_url in urls_to_try:
response = self.fetch_url_content(test_url)
if response:
url = test_url
break
if not response:
return {
"url": original_url,
"status": -1,
"results": [{"cms": "无法访问", "version": "", "confidence": 0, "judgment_basis": ["无法建立连接"]}],
"primary": {"cms": "无法访问", "version": "", "confidence": 0}
}
status_code = response.status_code
headers = response.headers
html_content = response.text
final_url = response.url
processed_html = self.preprocess_html(html_content)
self.log_signal.emit(f"获取内容: {final_url} (状态码: {status_code})")
cms_matches = []
min_score_threshold = 50
for cms in self.fingerprints:
total_score = 0
version = ""
# 记录详细的判断依据
judgment_basis = []
matched_features = []
unmatched_features = []
# 1. 匹配HTTP头特征
for header_item in cms.get('http_headers', []):
header_name = header_item.get('header', '').lower()
pattern = header_item.get('pattern', '')
item_score = header_item.get('score', 0)
feature_type = header_item.get('type', 'general')
if not header_name or not pattern:
continue
is_valid, fixed_pattern = self.validate_regex(pattern)
if not is_valid:
self.log_signal.emit(f"跳过无效HTTP头正则: {pattern}")
continue
weight = 2 if feature_type == 'core' else 1
adjusted_score = item_score * weight
feature_desc = f"HTTP头[{header_name}]匹配模式[{fixed_pattern}]"
if header_name in headers:
header_value = str(headers[header_name])
try:
if re.search(fixed_pattern, header_value, re.IGNORECASE):
total_score += adjusted_score
matched_features.append(f"{feature_desc} (+{adjusted_score})")
judgment_basis.append(f"✓ {feature_desc},匹配成功,加{adjusted_score}分")
if 'version_group' in header_item:
version = self.extract_version(
header_value, fixed_pattern, header_item['version_group']
) or version
else:
unmatched_features.append(f"{feature_desc} (未匹配)")
judgment_basis.append(f"✗ {feature_desc},未匹配")
except re.error as e:
self.log_signal.emit(f"HTTP头正则执行错误 {fixed_pattern}: {str(e)}")
else:
unmatched_features.append(f"{feature_desc} (Header不存在)")
judgment_basis.append(f"✗ {feature_desc},Header不存在")
# 2. 匹配HTML内容特征
for html_item in cms.get('html_content', []):
pattern = html_item.get('pattern', '').strip()
item_score = html_item.get('score', 0)
feature_type = html_item.get('type', 'general')
if not pattern:
continue
is_valid, fixed_pattern = self.validate_regex(pattern)
if not is_valid:
self.log_signal.emit(f"跳过无效HTML正则: {pattern}")
continue
weight = 2.5 if feature_type == 'core' else (1.5 if feature_type == 'specific' else 1)
adjusted_score = int(item_score * weight)
feature_desc = f"HTML内容匹配模式[{fixed_pattern[:50]}{'...' if len(fixed_pattern)>50 else ''}]"
try:
if '<' in fixed_pattern and '>' in fixed_pattern:
escaped_pattern = self.escape_special_chars(fixed_pattern)
flexible_pattern = re.sub(r'\s+', r'\\s+', escaped_pattern)
match_found = re.search(flexible_pattern, processed_html, re.IGNORECASE | re.DOTALL)
else:
match_found = re.search(fixed_pattern, processed_html, re.IGNORECASE | re.DOTALL)
if match_found:
total_score += adjusted_score
matched_features.append(f"{feature_desc} (+{adjusted_score})")
judgment_basis.append(f"✓ {feature_desc},匹配成功,加{adjusted_score}分")
if 'version_group' in html_item:
version = self.extract_version(
processed_html, fixed_pattern, html_item['version_group']
) or version
else:
unmatched_features.append(f"{feature_desc} (未匹配)")
judgment_basis.append(f"✗ {feature_desc},未匹配")
except re.error as e:
self.log_signal.emit(f"HTML正则执行错误 {fixed_pattern}: {str(e)}")
# 3. 匹配URL路径特征 - 使用线程池并发处理
url_path_tasks = []
with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, self.max_threads)) as executor:
for url_item in cms.get('url_paths', []):
path = url_item.get('path', '')
pattern = url_item.get('pattern', '')
item_score = url_item.get('score', 0)
feature_type = url_item.get('type', 'general')
if not path:
continue
weight = 2 if feature_type == 'core' else 1
adjusted_score = item_score * weight
# 提交任务到线程池
task = executor.submit(
self.check_url_path,
final_url, path, pattern, item_score, weight
)
url_path_tasks.append((task, adjusted_score, path))
# 处理URL路径特征结果
for task, adjusted_score, path in url_path_tasks:
try:
matched, desc, score = task.result()
if matched:
total_score += score
matched_features.append(f"{desc} (+{score})")
judgment_basis.append(f"✓ {desc},访问成功,加{score}分")
else:
unmatched_features.append(f"{desc} (访问失败或未匹配)")
judgment_basis.append(f"✗ {desc},访问失败或未匹配")
except Exception as e:
self.log_signal.emit(f"URL路径检查出错: {str(e)}")
# 计算置信度
max_possible = sum(
(h.get('score', 0) * (2 if h.get('type') == 'core' else 1))
for h in cms.get('http_headers', [])
) + sum(
(h.get('score', 0) * (2.5 if h.get('type') == 'core' else 1))
for h in cms.get('html_content', [])
) + sum(
(u.get('score', 0) * (2 if u.get('type') == 'core' else 1))
for u in cms.get('url_paths', [])
)
confidence = min(100, int((total_score / max_possible) * 100)) if max_possible > 0 else 0
# 汇总判断依据
if matched_features:
judgment_basis.insert(0, f"匹配到{len(matched_features)}个特征,总分{total_score}")
else:
judgment_basis.insert(0, f"未匹配到任何特征,总分0")
if total_score >= min_score_threshold:
cms_matches.append({
"cms": cms['cms'],
"version": version or cms.get('version', ''),
"score": total_score,
"confidence": confidence,
"judgment_basis": judgment_basis, # 存储详细判断依据
"features": matched_features
})
cms_matches.sort(key=lambda x: (-x['confidence'], -x['score']))
filtered_results = []
if cms_matches:
max_score = cms_matches[0]['score']
for match in cms_matches:
if match['score'] >= max_score * 0.8 or match['confidence'] >= 70:
filtered_results.append(match)
# 如果没有匹配到任何结果,添加一个默认结果并说明原因
if not filtered_results:
filtered_results.append({
"cms": "未知",
"version": "",
"confidence": 0,
"judgment_basis": ["未匹配到任何已知CMS的特征", "请检查指纹库是否完整或添加新指纹"]
})
primary_result = filtered_results[0] if filtered_results else {
"cms": "未知", "version": "", "confidence": 0
}
return {
"url": final_url,
"status": status_code,
"results": filtered_results,
"primary": primary_result
}
class AddFingerprintDialog(QDialog):
def __init__(self, parent=None, fingerprint=None):
super().__init__(parent)
self.fingerprint = fingerprint
self.setWindowTitle("编辑指纹" if fingerprint else "添加指纹")
self.setGeometry(300, 300, 600, 500)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
form_layout = QFormLayout()
self.cms_input = QLineEdit()
self.version_input = QLineEdit()
form_layout.addRow("CMS名称*:", self.cms_input)
form_layout.addRow("默认版本:", self.version_input)
regex_help = QLabel("正则表达式提示: 反斜杠需要输入两次(\\\\),特殊字符(如. * + ?)需要转义")
regex_help.setStyleSheet("color: #2980b9; font-size: 12px;")
form_layout.addRow(regex_help)
type_note = QLabel("特征类型说明: core(核心特征,权重高) > specific(特定特征) > general(通用特征)")
type_note.setStyleSheet("color: #666; font-size: 12px;")
form_layout.addRow(type_note)
layout.addLayout(form_layout)
# HTTP头特征表格
http_group = QWidget()
http_layout = QVBoxLayout(http_group)
http_layout.addWidget(QLabel("HTTP头特征:"))
self.http_table = QTableWidget(0, 4)
self.http_table.setHorizontalHeaderLabels(["Header", "Pattern", "Score", "Type(core/general)"])
self.http_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
http_btn_layout = QHBoxLayout()
add_http_btn = QPushButton("添加")
add_http_btn.clicked.connect(lambda: self.add_row(self.http_table, ["", "", "50", "general"]))
remove_http_btn = QPushButton("移除")
remove_http_btn.clicked.connect(lambda: self.remove_row(self.http_table))
http_btn_layout.addWidget(add_http_btn)
http_btn_layout.addWidget(remove_http_btn)
http_layout.addWidget(self.http_table)
http_layout.addLayout(http_btn_layout)
layout.addWidget(http_group)
# HTML内容特征表格
html_group = QWidget()
html_layout = QVBoxLayout(html_group)
html_layout.addWidget(QLabel("HTML内容特征:"))
self.html_table = QTableWidget(0, 4)
self.html_table.setHorizontalHeaderLabels(["Pattern", "Score", "Type(core/specific)", "版本提取组(可选)"])
self.html_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
html_btn_layout = QHBoxLayout()
add_html_btn = QPushButton("添加")
add_html_btn.clicked.connect(lambda: self.add_row(self.html_table, ["", "80", "specific", ""]))
remove_html_btn = QPushButton("移除")
remove_html_btn.clicked.connect(lambda: self.remove_row(self.html_table))
html_btn_layout.addWidget(add_html_btn)
html_btn_layout.addWidget(remove_html_btn)
html_layout.addWidget(self.html_table)
html_layout.addLayout(html_btn_layout)
layout.addWidget(html_group)
# URL路径特征表格
url_group = QWidget()
url_layout = QVBoxLayout(url_group)
url_layout.addWidget(QLabel("URL路径特征 (将主动访问这些路径):"))
self.url_table = QTableWidget(0, 4)
self.url_table.setHorizontalHeaderLabels(["Path", "Pattern(可选)", "Score", "Type(core/specific)"])
self.url_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
url_btn_layout = QHBoxLayout()
add_url_btn = QPushButton("添加")
add_url_btn.clicked.connect(lambda: self.add_row(self.url_table, ["", "", "60", "specific"]))
remove_url_btn = QPushButton("移除")
remove_url_btn.clicked.connect(lambda: self.remove_row(self.url_table))
url_btn_layout.addWidget(add_url_btn)
url_btn_layout.addWidget(remove_url_btn)
url_layout.addWidget(self.url_table)
url_layout.addLayout(url_btn_layout)
layout.addWidget(url_group)
# 测试正则按钮
test_btn = QPushButton("测试选中的正则表达式")
test_btn.clicked.connect(self.test_selected_regex)
layout.addWidget(test_btn)
# 确认按钮
btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
btn_box.accepted.connect(self.accept)
btn_box.rejected.connect(self.reject)
layout.addWidget(btn_box)
self.setLayout(layout)
self.load_fingerprint_data()
def test_selected_regex(self):
current_table = None
pattern = ""
if self.http_table.currentRow() >= 0:
current_table = self.http_table
item = self.http_table.item(self.http_table.currentRow(), 1)
if item:
pattern = item.text()
elif self.html_table.currentRow() >= 0:
current_table = self.html_table
item = self.html_table.item(self.html_table.currentRow(), 0)
if item:
pattern = item.text()
if not pattern:
QMessageBox.information(self, "测试结果", "请选择一个正则表达式进行测试")
return
try:
re.compile(pattern)
QMessageBox.information(self, "测试结果", f"正则表达式有效:\n{pattern}")
except re.error as e:
fixed = pattern
if "bad escape" in str(e):
fixed = re.sub(r'(?<!\\)\\(?!["\\/])', r'\\\\', pattern)
elif "unterminated subpattern" in str(e):
open_count = pattern.count('(')
close_count = pattern.count(')')
if open_count > close_count:
fixed = pattern + ')' * (open_count - close_count)
try:
re.compile(fixed)
QMessageBox.information(
self, "修复成功",
f"原表达式无效: {str(e)}\n修复后表达式: {fixed}"
)
if current_table == self.http_table:
self.http_table.item(self.http_table.currentRow(), 1).setText(fixed)
else:
self.html_table.item(self.html_table.currentRow(), 0).setText(fixed)
except re.error as e2:
QMessageBox.warning(
self, "测试失败",
f"正则表达式无效: {str(e2)}\n表达式: {pattern}"
)
def add_row(self, table, default_values):
row = table.rowCount()
table.insertRow(row)
for col, val in enumerate(default_values):
table.setItem(row, col, QTableWidgetItem(val))
def remove_row(self, table):
row = table.currentRow()
if row >= 0:
table.removeRow(row)
def load_fingerprint_data(self):
if not self.fingerprint:
return
self.cms_input.setText(self.fingerprint.get("cms", ""))
self.version_input.setText(self.fingerprint.get("version", ""))
for header in self.fingerprint.get("http_headers", []):
self.add_row(self.http_table, [
header.get("header", ""),
header.get("pattern", ""),
str(header.get("score", 50)),
header.get("type", "general")
])
for html in self.fingerprint.get("html_content", []):
self.add_row(self.html_table, [
html.get("pattern", ""),
str(html.get("score", 80)),
html.get("type", "specific"),
str(html.get("version_group", "")) if "version_group" in html else ""
])
for path in self.fingerprint.get("url_paths", []):
self.add_row(self.url_table, [
path.get("path", ""),
path.get("pattern", ""),
str(path.get("score", 60)),
path.get("type", "specific")
])
def validate_regex(self, pattern):
try:
if pattern:
re.compile(pattern)
return True
except re.error as e:
QMessageBox.warning(self, "正则错误", f"模式 '{pattern}' 无效: {str(e)}\n请使用测试按钮修复")
return False
def get_fingerprint(self):
cms_name = self.cms_input.text().strip()
if not cms_name:
QMessageBox.warning(self, "输入错误", "CMS名称不能为空")
return None
for row in range(self.html_table.rowCount()):
pattern_item = self.html_table.item(row, 0)
if pattern_item and not self.validate_regex(pattern_item.text().strip()):
return None
fingerprint = {
"cms": cms_name,
"version": self.version_input.text().strip(),
"confidence": 0,
"http_headers": [],
"html_content": [],
"url_paths": []
}
for row in range(self.http_table.rowCount()):
header = self.http_table.item(row, 0).text().strip() if self.http_table.item(row, 0) else ""
pattern = self.http_table.item(row, 1).text().strip() if self.http_table.item(row, 1) else ""
score = int(self.http_table.item(row, 2).text() or 50)
f_type = self.http_table.item(row, 3).text().strip() or "general"
if header and pattern:
fingerprint["http_headers"].append({
"header": header,
"pattern": pattern,
"score": score,
"type": f_type
})
for row in range(self.html_table.rowCount()):
pattern = self.html_table.item(row, 0).text().strip() if self.html_table.item(row, 0) else ""
score = int(self.html_table.item(row, 1).text() or 80)
f_type = self.html_table.item(row, 2).text().strip() or "specific"
version_group = self.html_table.item(row, 3).text().strip()
if pattern:
item = {
"pattern": pattern,
"score": score,
"type": f_type
}
if version_group and version_group.isdigit():
item["version_group"] = int(version_group)
fingerprint["html_content"].append(item)
for row in range(self.url_table.rowCount()):
path = self.url_table.item(row, 0).text().strip() if self.url_table.item(row, 0) else ""
pattern = self.url_table.item(row, 1).text().strip() if self.url_table.item(row, 1) else ""
score = int(self.url_table.item(row, 2).text() or 60)
f_type = self.url_table.item(row, 3).text().strip() or "specific"
if path:
fingerprint["url_paths"].append({
"path": path,
"pattern": pattern,
"score": score,
"type": f_type
})
return fingerprint
class JudgmentBasisDialog(QDialog):
"""判断依据展示对话框"""
def __init__(self, parent=None, result=None):
super().__init__(parent)
self.result = result
self.setWindowTitle(f"识别依据 - {result['url']}")
self.setGeometry(400, 200, 800, 600)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# 基本信息
basic_info = QLabel(f"""
<h3>URL: {self.result['url']}</h3>
<p>状态码: {self.result['status']}</p>
""")
layout.addWidget(basic_info)
# 识别结果
results_group = QGroupBox("识别结果汇总")
results_layout = QVBoxLayout()
for i, res in enumerate(self.result['results']):
is_primary = (i == 0) # 第一个结果是主要结果
result_label = QLabel(f"""
<p><b>{'★ ' if is_primary else ''}{res['cms']} v{res['version']}</b>
置信度: {res['confidence']}%</p>
""")
results_layout.addWidget(result_label)
results_group.setLayout(results_layout)
layout.addWidget(results_group)
# 详细判断依据
basis_group = QTabWidget()
for res in self.result['results']:
text_edit = QTextEdit()
text_edit.setReadOnly(True)
# 显示所有判断依据
text_edit.setText("\n".join(res['judgment_basis']))
basis_group.addTab(text_edit, f"{res['cms']} (置信度{res['confidence']}%)")
layout.addWidget(basis_group)
# 关闭按钮
btn_box = QDialogButtonBox(QDialogButtonBox.Ok)
btn_box.accepted.connect(self.accept)
layout.addWidget(btn_box)
self.setLayout(layout)
class CMSDetectorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("多CMS识别工具 (带判断依据)")
self.setGeometry(100, 100, 1200, 800)
self.fingerprint_manager = FingerprintManager()
self.results = []
self.create_menu()
self.init_ui()
self.apply_styles()
def create_menu(self):
menubar = self.menuBar()
file_menu = menubar.addMenu("文件")
import_action = QAction("导入网站列表", self)
import_action.triggered.connect(self.import_urls)
file_menu.addAction(import_action)
export_action = QAction("导出结果", self)
export_action.triggered.connect(self.export_results)
file_menu.addAction(export_action)
file_menu.addSeparator()
exit_action = QAction("退出", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
fingerprint_menu = menubar.addMenu("指纹库")
add_fingerprint_action = QAction("添加指纹", self)
add_fingerprint_action.triggered.connect(self.add_fingerprint)
fingerprint_menu.addAction(add_fingerprint_action)
import_fingerprint_action = QAction("导入指纹库", self)
import_fingerprint_action.triggered.connect(self.import_fingerprints)
fingerprint_menu.addAction(import_fingerprint_action)
export_fingerprint_action = QAction("导出指纹库", self)
export_fingerprint_action.triggered.connect(self.export_fingerprints)
fingerprint_menu.addAction(export_fingerprint_action)
clear_fingerprint_action = QAction("清空指纹库", self)
clear_fingerprint_action.triggered.connect(self.clear_fingerprints)
fingerprint_menu.addAction(clear_fingerprint_action)
restore_default_action = QAction("恢复默认指纹库", self)
restore_default_action.triggered.connect(self.restore_default_fingerprints)
fingerprint_menu.addAction(restore_default_action)
help_menu = menubar.addMenu("帮助")
about_action = QAction("关于", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def init_ui(self):
main_widget = QWidget()
main_layout = QVBoxLayout()
self.tabs = QTabWidget()
self.detection_tab = self.create_detection_tab()
self.fingerprint_tab = self.create_fingerprint_tab()
self.tabs.addTab(self.detection_tab, "网站检测")
self.tabs.addTab(self.fingerprint_tab, "指纹库管理")
main_layout.addWidget(self.tabs)
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
self.status_bar = self.statusBar()
self.status_label = QLabel("就绪")
self.status_bar.addWidget(self.status_label)
self.detection_thread = None
def apply_styles(self):
self.setStyleSheet("""
QMainWindow { background-color: #f0f0f0; }
QTabWidget::pane { border: 1px solid #cccccc; background: white; }
QTableWidget {
background-color: white;
alternate-background-color: #f8f8f8;
gridline-color: #e0e0e0;
}
QHeaderView::section {
background-color: #e0e0e0;
padding: 4px;
border: 1px solid #d0d0d0;
}
QPushButton {
background-color: #4a86e8;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
}
QPushButton:hover { background-color: #3a76d8; }
QPushButton:pressed { background-color: #2a66c8; }
QPushButton:disabled { background-color: #a0a0a0; }
QPushButton#clearBtn { background-color: #e74c3c; }
QPushButton#clearBtn:hover { background-color: #c0392b; }
QPushButton#restoreBtn { background-color: #27ae60; }
QPushButton#restoreBtn:hover { background-color: #219653; }
""")
def create_detection_tab(self):
tab = QWidget()
layout = QVBoxLayout()
# URL输入区域
control_layout = QHBoxLayout()
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("输入网站URL (例如: example.com 或 http://example.com)")
add_url_btn = QPushButton("添加URL")
add_url_btn.clicked.connect(self.add_single_url)
import_btn = QPushButton("导入URL列表")
import_btn.clicked.connect(self.import_urls)
clear_btn = QPushButton("清空列表")
clear_btn.clicked.connect(self.clear_urls)
control_layout.addWidget(self.url_input, 4)
control_layout.addWidget(add_url_btn, 1)
control_layout.addWidget(import_btn, 1)
control_layout.addWidget(clear_btn, 1)
layout.addLayout(control_layout)
# URL列表区域
url_list_layout = QVBoxLayout()
url_list_layout.addWidget(QLabel("待检测网站列表:"))
self.url_list = QTextEdit()
self.url_list.setPlaceholderText("每行一个URL")
self.url_list.setMinimumHeight(80)
url_list_layout.addWidget(self.url_list)
layout.addLayout(url_list_layout)
# 检测控制区域
detection_control_layout = QHBoxLayout()
self.thread_spin = QSpinBox()
self.thread_spin.setRange(1, 20)
self.thread_spin.setValue(5)
self.thread_spin.setPrefix("线程数: ")
self.retry_spin = QSpinBox()
self.retry_spin.setRange(0, 3)
self.retry_spin.setValue(1)
self.retry_spin.setPrefix("重试次数: ")
self.timeout_spin = QSpinBox()
self.timeout_spin.setRange(5, 60)
self.timeout_spin.setValue(15)
self.timeout_spin.setPrefix("超时时间(秒): ")
self.detect_btn = QPushButton("开始检测")
self.detect_btn.clicked.connect(self.start_detection)
self.stop_btn = QPushButton("停止检测")
self.stop_btn.clicked.connect(self.stop_detection)
self.stop_btn.setEnabled(False)
detection_control_layout.addWidget(self.thread_spin)
detection_control_layout.addWidget(self.retry_spin)
detection_control_layout.addWidget(self.timeout_spin)
detection_control_layout.addStretch()
detection_control_layout.addWidget(self.detect_btn)
detection_control_layout.addWidget(self.stop_btn)
layout.addLayout(detection_control_layout)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setTextVisible(True)
layout.addWidget(self.progress_bar)
# 结果展示区域
splitter = QSplitter(Qt.Vertical)
self.result_table = QTableWidget(0, 6) # 增加一列显示操作
self.result_table.setHorizontalHeaderLabels(["URL", "状态", "CMS类型", "版本", "置信度(%)", "操作"])
self.result_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.result_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
self.result_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
self.result_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
self.result_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
self.result_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
self.result_table.setAlternatingRowColors(True)
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
self.log_area.setMinimumHeight(150)
splitter.addWidget(self.result_table)
splitter.addWidget(self.log_area)
splitter.setSizes([400, 150])
layout.addWidget(splitter, 1)
tab.setLayout(layout)
return tab
def create_fingerprint_tab(self):
tab = QWidget()
layout = QVBoxLayout()
btn_layout = QHBoxLayout()
add_btn = QPushButton("添加指纹")
add_btn.clicked.connect(self.add_fingerprint)
edit_btn = QPushButton("编辑指纹")
edit_btn.clicked.connect(self.edit_fingerprint)
remove_btn = QPushButton("删除指纹")
remove_btn.clicked.connect(self.remove_fingerprint)
clear_btn = QPushButton("清空指纹库")
clear_btn.setObjectName("clearBtn")
clear_btn.clicked.connect(self.clear_fingerprints)
restore_btn = QPushButton("恢复默认")
restore_btn.setObjectName("restoreBtn")
restore_btn.clicked.connect(self.restore_default_fingerprints)
import_btn = QPushButton("导入指纹库")
import_btn.clicked.connect(self.import_fingerprints)
export_btn = QPushButton("导出指纹库")
export_btn.clicked.connect(self.export_fingerprints)
btn_layout.addWidget(add_btn)
btn_layout.addWidget(edit_btn)
btn_layout.addWidget(remove_btn)
btn_layout.addWidget(clear_btn)
btn_layout.addWidget(restore_btn)
btn_layout.addStretch()
btn_layout.addWidget(import_btn)
btn_layout.addWidget(export_btn)
layout.addLayout(btn_layout)
self.fingerprint_tree = QTreeWidget()
self.fingerprint_tree.setHeaderLabels(["CMS名称", "版本", "核心特征数", "总特征数"])
self.fingerprint_tree.setColumnWidth(0, 200)
self.fingerprint_tree.setSortingEnabled(True)
self.populate_fingerprint_tree()
layout.addWidget(self.fingerprint_tree, 1)
tab.setLayout(layout)
return tab
def populate_fingerprint_tree(self):
self.fingerprint_tree.clear()
fingerprints = self.fingerprint_manager.get_fingerprints()
for i, fp in enumerate(fingerprints):
try:
cms_name = fp["cms"]
version = fp.get("version", "")
core_features = 0
total_features = 0
for h in fp.get("http_headers", []):
total_features += 1
if h.get("type") == "core":
core_features += 1
for h in fp.get("html_content", []):
total_features += 1
if h.get("type") == "core":
core_features += 1
for u in fp.get("url_paths", []):
total_features += 1
if u.get("type") == "core":
core_features += 1
item = QTreeWidgetItem([
cms_name, version, str(core_features), str(total_features)
])
item.setData(0, Qt.UserRole, i)
self.fingerprint_tree.addTopLevelItem(item)
except Exception as e:
self.log(f"处理指纹时出错: {e},已跳过")
def add_single_url(self):
url = self.url_input.text().strip()
if url:
current_text = self.url_list.toPlainText()
new_text = current_text + (("\n" + url) if current_text else url)
self.url_list.setPlainText(new_text)
self.url_input.clear()
def import_urls(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "导入URL列表", "", "文本文件 (*.txt);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
urls = [line.strip() for line in f if line.strip()]
self.url_list.setPlainText("\n".join(urls))
self.log(f"成功导入 {len(urls)} 个URL")
except Exception as e:
QMessageBox.critical(self, "导入错误", f"导入失败: {str(e)}")
def clear_urls(self):
self.url_list.clear()
def start_detection(self):
urls_text = self.url_list.toPlainText().strip()
if not urls_text:
QMessageBox.warning(self, "警告", "请先添加要检测的URL")
return
urls = [url.strip() for url in urls_text.splitlines() if url.strip()]
if not urls:
QMessageBox.warning(self, "警告", "没有有效的URL")
return
self.result_table.setRowCount(0)
self.results = []
max_threads = self.thread_spin.value()
retry_count = self.retry_spin.value()
timeout = self.timeout_spin.value()
self.detection_thread = DetectionWorker(
urls, self.fingerprint_manager.get_fingerprints(),
max_threads, retry_count
)
self.detection_thread.timeout = timeout
self.detection_thread.progress_signal.connect(self.update_progress)
self.detection_thread.result_signal.connect(self.add_result)
self.detection_thread.log_signal.connect(self.log)
self.detection_thread.finished_signal.connect(self.detection_finished)
self.detect_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.progress_bar.setRange(0, len(urls))
self.progress_bar.setValue(0)
self.detection_thread.start()
def stop_detection(self):
if self.detection_thread and self.detection_thread.isRunning():
self.detection_thread.stop()
self.log("检测已停止")
self.detection_finished()
def detection_finished(self):
self.detect_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.status_label.setText("检测完成")
def update_progress(self, current, total, url):
self.progress_bar.setMaximum(total)
self.progress_bar.setValue(current)
self.status_label.setText(f"正在检测: {url} ({current}/{total})")
def show_judgment_basis(self, result):
"""显示判断依据对话框"""
dialog = JudgmentBasisDialog(self, result)
dialog.exec_()
def add_result(self, result):
self.results.append(result)
row = self.result_table.rowCount()
self.result_table.insertRow(row)
# URL
url_item = QTableWidgetItem(result["url"])
url_item.setFlags(url_item.flags() ^ Qt.ItemIsEditable)
self.result_table.setItem(row, 0, url_item)
# 状态码
status = result["status"]
status_item = QTableWidgetItem(str(status))
status_item.setFlags(status_item.flags() ^ Qt.ItemIsEditable)
if status == 200:
status_item.setForeground(Qt.darkGreen)
elif 400 <= status < 500:
status_item.setForeground(Qt.darkRed)
elif status >= 500:
status_item.setForeground(Qt.darkMagenta)
self.result_table.setItem(row, 1, status_item)
# CMS类型(主结果)
primary = result["primary"]
cms_item = QTableWidgetItem(primary["cms"])
cms_item.setFlags(cms_item.flags() ^ Qt.ItemIsEditable)
self.result_table.setItem(row, 2, cms_item)
# 版本
version_item = QTableWidgetItem(primary["version"])
version_item.setFlags(version_item.flags() ^ Qt.ItemIsEditable)
self.result_table.setItem(row, 3, version_item)
# 置信度
confidence = primary["confidence"]
confidence_item = QTableWidgetItem(f"{confidence}%")
confidence_item.setFlags(confidence_item.flags() ^ Qt.ItemIsEditable)
if confidence >= 90:
confidence_item.setForeground(Qt.darkGreen)
elif confidence >= 70:
confidence_item.setForeground(Qt.darkBlue)
elif confidence >= 50:
confidence_item.setForeground(Qt.darkOrange)
else:
confidence_item.setForeground(Qt.darkGray)
self.result_table.setItem(row, 4, confidence_item)
# 查看依据按钮
view_btn = QPushButton("查看依据")
# 使用lambda表达式传递当前result
view_btn.clicked.connect(lambda checked, res=result: self.show_judgment_basis(res))
self.result_table.setCellWidget(row, 5, view_btn)
def add_fingerprint(self):
dialog = AddFingerprintDialog(self)
if dialog.exec_() == QDialog.Accepted:
fingerprint = dialog.get_fingerprint()
if fingerprint and self.fingerprint_manager.add_fingerprint(fingerprint):
self.populate_fingerprint_tree()
self.log(f"已添加指纹: {fingerprint['cms']}")
def edit_fingerprint(self):
selected_items = self.fingerprint_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "警告", "请选择一个指纹进行编辑")
return
item = selected_items[0]
index = item.data(0, Qt.UserRole)
fingerprints = self.fingerprint_manager.get_fingerprints()
if index is None or not (0 <= index < len(fingerprints)):
QMessageBox.warning(self, "错误", "无效的指纹索引")
return
fingerprint = fingerprints[index]
dialog = AddFingerprintDialog(self, fingerprint)
if dialog.exec_() == QDialog.Accepted:
updated = dialog.get_fingerprint()
if updated and self.fingerprint_manager.update_fingerprint(index, updated):
self.populate_fingerprint_tree()
self.log(f"已更新指纹: {updated['cms']}")
def remove_fingerprint(self):
selected_items = self.fingerprint_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "警告", "请选择要删除的指纹")
return
item = selected_items[0]
cms_name = item.text(0)
index = item.data(0, Qt.UserRole)
reply = QMessageBox.question(
self, "确认删除", f"确定要删除 '{cms_name}' 的指纹吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.fingerprint_manager.remove_fingerprint(index)
self.populate_fingerprint_tree()
self.log(f"已删除指纹: {cms_name}")
def clear_fingerprints(self):
if not self.fingerprint_manager.get_fingerprints():
QMessageBox.information(self, "提示", "指纹库已为空")
return
reply = QMessageBox.question(
self, "确认清空", "确定要清空所有指纹吗?此操作不可恢复!",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.fingerprint_manager.clear_fingerprints()
self.populate_fingerprint_tree()
self.log("已清空所有指纹")
def restore_default_fingerprints(self):
reply = QMessageBox.question(
self, "确认恢复", "确定要恢复默认指纹库吗?当前指纹将被替换!",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.fingerprint_manager.restore_default_fingerprints()
self.populate_fingerprint_tree()
self.log("已恢复默认指纹库")
def import_fingerprints(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "导入指纹库", "", "JSON文件 (*.json);;所有文件 (*)"
)
if file_path and self.fingerprint_manager.import_fingerprints(file_path):
self.populate_fingerprint_tree()
self.log(f"成功导入指纹库: {file_path}")
def export_fingerprints(self):
file_path, _ = QFileDialog.getSaveFileName(
self, "导出指纹库", "cms_fingerprints.json", "JSON文件 (*.json)"
)
if file_path and self.fingerprint_manager.export_fingerprints(file_path):
self.log(f"成功导出指纹库: {file_path}")
def export_results(self):
if not self.results:
QMessageBox.warning(self, "警告", "没有结果可导出")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "导出结果", "", "CSV文件 (*.csv);;JSON文件 (*.json)"
)
if not file_path:
return
try:
if file_path.endswith(".csv"):
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(["URL", "状态", "CMS类型", "版本", "置信度(%)"])
for result in self.results:
primary = result["primary"]
writer.writerow([
result["url"],
result["status"],
primary["cms"],
primary["version"],
primary["confidence"]
])
elif file_path.endswith(".json"):
# 导出完整结果,包括判断依据
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(self.results, f, indent=4, ensure_ascii=False)
self.log(f"结果已导出到: {file_path}")
except Exception as e:
QMessageBox.critical(self, "导出错误", f"导出失败: {str(e)}")
def log(self, message):
timestamp = time.strftime("%H:%M:%S")
self.log_area.append(f"[{timestamp}] {message}")
def show_about(self):
about_text = """
<h2>多CMS识别工具 (带判断依据)</h2>
<p>版本: 2.3.0</p>
<p>功能特点:</p>
<ul>
<li>显示详细的识别判断依据</li>
<li>URL路径特征主动访问验证</li>
<li>并发检测提高效率</li>
<li>核心特征加权识别,准确率高</li>
<li>支持正则表达式测试和验证</li>
<li>可自定义超时时间和重试次数</li>
</ul>
<p>使用说明: 点击结果中的"查看依据"按钮可查看详细的识别依据</p>
"""
QMessageBox.about(self, "关于", about_text)
def closeEvent(self, event):
if self.detection_thread and self.detection_thread.isRunning():
reply = QMessageBox.question(
self, "检测中", "检测仍在进行中,确定要退出吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.detection_thread.stop()
event.accept()
else:
event.ignore()
else:
event.accept()
if __name__ == "__main__":
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
app.setStyle("Fusion")
window = CMSDetectorApp()
window.show()
sys.exit(app.exec_()) 修改代码提高验证效率 其他无需修改 完整输出
最新发布