<cms:contentload>标签的使用

本文介绍OpenCms中<cms:contentload>标签的使用方法,包括如何通过collector属性选择不同的XML内容资源加载策略,以及param、editable和preload等属性的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[ 王毅原创,转载注明]

标签说明:
    <cms:contentload> 载入一个 XML 内容资源的 collection ,根据给定的 collector 属性,确定是单个资源还是资源列表。
    也就是说,这个标签可以载入一个xml结构化类型的资源的列表,也可以载入单个的一个xml结构化类型的资源,依靠的仅仅是collector这个属性的值的不同。

标签属性:
  • collector:必选属性
    这个属性用来确定读取结构化内容。collectors是由OpenCms中的配置文件opencms-vfs.xml定义的。原始内容如下:
            <collectors>
                <collector class="org.opencms.file.collectors.CmsPriorityResourceCollector" order="100" />
                <collector class="org.opencms.file.collectors.CmsDefaultResourceCollector" order="110" />
                <collector class="org.opencms.file.collectors.CmsDateResourceCollector" order="120" />
                <collector class="org.opencms.file.collectors.CmsTimeFrameCategoryCollector" order="130" />
                <collector class="org.opencms.file.collectors.CmsCategoryResourceCollector" order="140" />
            </collectors>
这个标签的可选值有以下几种:
  1. singleFile
  2. allInFolder
  3. allInFolderDateReleasedDesc
  4. allInFolderNavPos
  5. allInSubTree
  6. allInSubTreeDateReleasedDesc
  7. allInSubTreeNavPos
  8. allInFolderPriorityDateDesc
  9. allInFolderPriorityTitleDesc
  10. allInSubTreePriorityDateDesc
  11. allInSubTreePriorityTitleDesc
    至于这些值的意思,我觉得看字面意思也能明白,在这里就不做解释了。
    注意的是,这个值可以直接在属性里面指定,也可以在属性里面指定。大家如果注意的话,就会发现在TemplateOne里面,资源有一个属性为“collector”,你可以通过下面这个值:
            ${property.collector}
来得到属性中的值,这样的话,显得更为灵活,当你需要排序的方式发生改变时候,不需要你修改页面的硬代码,直接修改一个资源的属性就可以了。
  • param
    这个属性用来给“collector”提供额外的其他信息,最常见的用法如下:
  1.         <cms:contentload collector ="singleFile" param="${opencms.uri}">  用来在加载当个资源文件的时候要通过param告诉“collector”遍历的是这个uri做指向的资源。
  2.         <cms:contentload collector ="allInFolder"  param="/xmlcontent/article_${number}.html|article|4"> 用来指定加载的是那些资源,是那种结构化类型,加载多少项。
    这里也需要注意以下,这个属性的值,可以直接指定,也可以向上面的 collector属性一样有属性得到,还可以通过普通的页面参数得到,还有一种比较特殊的就是在加载单个文件的时候,可以使用 ${opencms.uri} ${opencms.filename}
  • editable
    这个属性指定了当前xml内容资源是否支持直接编辑的特性。
    可选的值也当然只有“是”和“否”了,也就是“true”和“false”。
    这个属性是可选的,当你什么都不写的时候,有一个“false”作为默认值。
  • preload 
    这个属性定义了是否预先加载xml资源的内容。
    这个属性可选的值有“true”。

【标签体】
    可以是任何的HTML, JSP or JSTL代码。这个代码在返回的集合不止一个结构化内容项的时候被循环执行。


我的相关日志:

2007-03-23 |  [转]OpenCms for MySql 安装图解
2007-03-23 |  [转][图文]OpenCms VFS plugin for Eclipse 安装与使用
2008-03-21 |  <cms:contentload>标签的使用
2007-12-14 |  <cms:editable>标签的使用
2007-12-13 |  <cms:contentshow>标签的使用
2007-12-12 |  <cms:contentloop> 标签的使用
2007-12-11 |  <cms:contentinfo>标签的使用
2007-12-10 |  <cms:contentcheck>标签的使用
2007-06-19 |  OpenCms的<cms:info>标签
2007-04-16 |  再次讨论关于OpenCms的label标签的用法
2007-04-10 |  直接用OpenCms的API而不是标签来得到资源信息
2007-03-23 |  [转]OpenCms JSP标签详解——cms:user标签


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, &#39;r&#39;, encoding=&#39;utf-8&#39;) 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(&#39;http_headers&#39;, []): if &#39;pattern&#39; in header: header[&#39;pattern&#39;] = self._fix_regex_pattern(header[&#39;pattern&#39;]) for html in fp.get(&#39;html_content&#39;, []): if &#39;pattern&#39; in html: html[&#39;pattern&#39;] = self._fix_regex_pattern(html[&#39;pattern&#39;]) for url in fp.get(&#39;url_paths&#39;, []): if &#39;pattern&#39; in url: url[&#39;pattern&#39;] = self._fix_regex_pattern(url[&#39;pattern&#39;]) return fp def _fix_regex_pattern(self, pattern): """修复常见的正则表达式错误""" if not pattern: return "" # 修复未转义的反斜杠 fixed = re.sub(r&#39;(?<!\\)\\(?!["\\/])&#39;, r&#39;\\\\&#39;, pattern) # 修复未闭合的括号 open_count = fixed.count(&#39;(&#39;) close_count = fixed.count(&#39;)&#39;) if open_count > close_count: fixed += &#39;)&#39; * (open_count - close_count) # 修复不完整的字符类 if &#39;[&#39; in fixed and &#39;]&#39; not in fixed: fixed += &#39;]&#39; 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, &#39;w&#39;, encoding=&#39;utf-8&#39;) 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, &#39;w&#39;, encoding=&#39;utf-8&#39;) 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, &#39;r&#39;, encoding=&#39;utf-8&#39;) 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&#39;\n\s+&#39;, &#39;\n&#39;, html) processed = re.sub(r&#39;>\s+<&#39;, &#39;><&#39;, processed) return processed.strip() def escape_special_chars(self, pattern): """安全转义正则特殊字符""" if not pattern: return "" safe_pattern = re.sub(r&#39;\\(?![\\.*+?^${}()|[\]sSdDwWtnbfvr])&#39;, r&#39;\\\\&#39;, 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&#39;(?<!\\)\\(?!["\\/])&#39;, r&#39;\\\\&#39;, pattern) elif "unterminated subpattern" in str(e): open_count = pattern.count(&#39;(&#39;) close_count = pattern.count(&#39;)&#39;) if open_count > close_count: fixed = pattern + &#39;)&#39; * (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 = { &#39;User-Agent&#39;: &#39;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36&#39;, &#39;Accept&#39;: &#39;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&#39;, &#39;Accept-Language&#39;: &#39;zh-CN,zh;q=0.9&#39; } 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(&#39;/&#39;): path = &#39;/&#39; + 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((&#39;http://&#39;, &#39;https://&#39;)): urls_to_try = [f&#39;http://{url}&#39;, f&#39;https://{url}&#39;] 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(&#39;http_headers&#39;, []): header_name = header_item.get(&#39;header&#39;, &#39;&#39;).lower() pattern = header_item.get(&#39;pattern&#39;, &#39;&#39;) item_score = header_item.get(&#39;score&#39;, 0) feature_type = header_item.get(&#39;type&#39;, &#39;general&#39;) 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 == &#39;core&#39; 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 &#39;version_group&#39; in header_item: version = self.extract_version( header_value, fixed_pattern, header_item[&#39;version_group&#39;] ) 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(&#39;html_content&#39;, []): pattern = html_item.get(&#39;pattern&#39;, &#39;&#39;).strip() item_score = html_item.get(&#39;score&#39;, 0) feature_type = html_item.get(&#39;type&#39;, &#39;general&#39;) 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 == &#39;core&#39; else (1.5 if feature_type == &#39;specific&#39; else 1) adjusted_score = int(item_score * weight) feature_desc = f"HTML内容匹配模式[{fixed_pattern[:50]}{&#39;...&#39; if len(fixed_pattern)>50 else &#39;&#39;}]" try: if &#39;<&#39; in fixed_pattern and &#39;>&#39; in fixed_pattern: escaped_pattern = self.escape_special_chars(fixed_pattern) flexible_pattern = re.sub(r&#39;\s+&#39;, r&#39;\\s+&#39;, 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 &#39;version_group&#39; in html_item: version = self.extract_version( processed_html, fixed_pattern, html_item[&#39;version_group&#39;] ) 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(&#39;url_paths&#39;, []): path = url_item.get(&#39;path&#39;, &#39;&#39;) pattern = url_item.get(&#39;pattern&#39;, &#39;&#39;) item_score = url_item.get(&#39;score&#39;, 0) feature_type = url_item.get(&#39;type&#39;, &#39;general&#39;) if not path: continue weight = 2 if feature_type == &#39;core&#39; 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(&#39;score&#39;, 0) * (2 if h.get(&#39;type&#39;) == &#39;core&#39; else 1)) for h in cms.get(&#39;http_headers&#39;, []) ) + sum( (h.get(&#39;score&#39;, 0) * (2.5 if h.get(&#39;type&#39;) == &#39;core&#39; else 1)) for h in cms.get(&#39;html_content&#39;, []) ) + sum( (u.get(&#39;score&#39;, 0) * (2 if u.get(&#39;type&#39;) == &#39;core&#39; else 1)) for u in cms.get(&#39;url_paths&#39;, []) ) 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[&#39;cms&#39;], "version": version or cms.get(&#39;version&#39;, &#39;&#39;), "score": total_score, "confidence": confidence, "judgment_basis": judgment_basis, # 存储详细判断依据 "features": matched_features }) cms_matches.sort(key=lambda x: (-x[&#39;confidence&#39;], -x[&#39;score&#39;])) filtered_results = [] if cms_matches: max_score = cms_matches[0][&#39;score&#39;] for match in cms_matches: if match[&#39;score&#39;] >= max_score * 0.8 or match[&#39;confidence&#39;] >= 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&#39;(?<!\\)\\(?!["\\/])&#39;, r&#39;\\\\&#39;, pattern) elif "unterminated subpattern" in str(e): open_count = pattern.count(&#39;(&#39;) close_count = pattern.count(&#39;)&#39;) if open_count > close_count: fixed = pattern + &#39;)&#39; * (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"模式 &#39;{pattern}&#39; 无效: {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[&#39;url&#39;]}") self.setGeometry(400, 200, 800, 600) self.init_ui() def init_ui(self): layout = QVBoxLayout() # 基本信息 basic_info = QLabel(f""" <h3>URL: {self.result[&#39;url&#39;]}</h3> <p>状态码: {self.result[&#39;status&#39;]}</p> """) layout.addWidget(basic_info) # 识别结果 results_group = QGroupBox("识别结果汇总") results_layout = QVBoxLayout() for i, res in enumerate(self.result[&#39;results&#39;]): is_primary = (i == 0) # 第一个结果是主要结果 result_label = QLabel(f""" <p><b>{&#39;★ &#39; if is_primary else &#39;&#39;}{res[&#39;cms&#39;]} v{res[&#39;version&#39;]}</b> 置信度: {res[&#39;confidence&#39;]}%</p> """) results_layout.addWidget(result_label) results_group.setLayout(results_layout) layout.addWidget(results_group) # 详细判断依据 basis_group = QTabWidget() for res in self.result[&#39;results&#39;]: text_edit = QTextEdit() text_edit.setReadOnly(True) # 显示所有判断依据 text_edit.setText("\n".join(res[&#39;judgment_basis&#39;])) basis_group.addTab(text_edit, f"{res[&#39;cms&#39;]} (置信度{res[&#39;confidence&#39;]}%)") 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, &#39;r&#39;, encoding=&#39;utf-8&#39;) 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[&#39;cms&#39;]}") 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[&#39;cms&#39;]}") 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"确定要删除 &#39;{cms_name}&#39; 的指纹吗?", 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, &#39;w&#39;, newline=&#39;&#39;, encoding=&#39;utf-8&#39;) 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, &#39;w&#39;, encoding=&#39;utf-8&#39;) 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, &#39;AA_EnableHighDpiScaling&#39;): QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) if hasattr(Qt, &#39;AA_UseHighDpiPixmaps&#39;): QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app = QApplication(sys.argv) app.setStyle("Fusion") window = CMSDetectorApp() window.show() sys.exit(app.exec_()) 修改代码提高验证效率 其他无需修改 完整输出
最新发布
07-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值