|
|
@@ -1,573 +0,0 @@
|
|
|
-#!/usr/bin/env python3
|
|
|
-# -*- coding: utf-8 -*-
|
|
|
-"""
|
|
|
-How解构结果可视化脚本
|
|
|
-
|
|
|
-将 how 解构结果转化为 HTML 格式,使用标签页展示多个帖子
|
|
|
-"""
|
|
|
-
|
|
|
-import json
|
|
|
-from pathlib import Path
|
|
|
-from typing import Dict, List
|
|
|
-import sys
|
|
|
-
|
|
|
-# 添加项目根目录到路径
|
|
|
-project_root = Path(__file__).parent.parent.parent
|
|
|
-sys.path.insert(0, str(project_root))
|
|
|
-
|
|
|
-
|
|
|
-def get_relation_color(relation: str) -> str:
|
|
|
- """根据关系类型返回对应的颜色"""
|
|
|
- color_map = {
|
|
|
- "same": "#10b981", # 绿色 - 同义
|
|
|
- "contains": "#3b82f6", # 蓝色 - 包含
|
|
|
- "contained_by": "#8b5cf6", # 紫色 - 被包含
|
|
|
- "coordinate": "#f59e0b", # 橙色 - 同级
|
|
|
- "overlap": "#ec4899", # 粉色 - 部分重叠
|
|
|
- "related": "#6366f1", # 靛蓝 - 相关
|
|
|
- "unrelated": "#9ca3af" # 灰色 - 无关
|
|
|
- }
|
|
|
- return color_map.get(relation, "#9ca3af")
|
|
|
-
|
|
|
-
|
|
|
-def get_relation_label(relation: str) -> str:
|
|
|
- """返回关系类型的中文标签"""
|
|
|
- label_map = {
|
|
|
- "same": "同义",
|
|
|
- "contains": "包含",
|
|
|
- "contained_by": "被包含",
|
|
|
- "coordinate": "同级",
|
|
|
- "overlap": "部分重叠",
|
|
|
- "related": "相关",
|
|
|
- "unrelated": "无关"
|
|
|
- }
|
|
|
- return label_map.get(relation, relation)
|
|
|
-
|
|
|
-
|
|
|
-def generate_match_item_html(match: Dict) -> str:
|
|
|
- """生成单个匹配项的HTML"""
|
|
|
- persona_name = match.get("人设特征名称", "")
|
|
|
- match_result = match.get("匹配结果", {})
|
|
|
- relation = match_result.get("relation", "unrelated")
|
|
|
- score = match_result.get("score", 0.0)
|
|
|
- explanation = match_result.get("explanation", "")
|
|
|
-
|
|
|
- color = get_relation_color(relation)
|
|
|
- label = get_relation_label(relation)
|
|
|
-
|
|
|
- # 根据分数设置背景透明度
|
|
|
- opacity = min(score, 1.0)
|
|
|
- bg_color = f"rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, {opacity * 0.15})"
|
|
|
-
|
|
|
- html = f"""
|
|
|
- <div class="match-item" style="border-left: 3px solid {color}; background: {bg_color};">
|
|
|
- <div class="match-header">
|
|
|
- <span class="persona-name">{persona_name}</span>
|
|
|
- <span class="relation-badge" style="background: {color};">{label}</span>
|
|
|
- <span class="score-badge">分数: {score:.2f}</span>
|
|
|
- </div>
|
|
|
- <div class="match-explanation">{explanation}</div>
|
|
|
- </div>
|
|
|
- """
|
|
|
- return html
|
|
|
-
|
|
|
-
|
|
|
-def generate_feature_html(feature_data: Dict) -> str:
|
|
|
- """生成单个特征的HTML"""
|
|
|
- feature_name = feature_data.get("特征名称", "")
|
|
|
- match_results = feature_data.get("匹配结果", [])
|
|
|
-
|
|
|
- # 按分数排序(从高到低)
|
|
|
- sorted_matches = sorted(match_results, key=lambda x: x.get("匹配结果", {}).get("score", 0), reverse=True)
|
|
|
-
|
|
|
- # 统计匹配类型
|
|
|
- relation_counts = {}
|
|
|
- for match in match_results:
|
|
|
- relation = match.get("匹配结果", {}).get("relation", "unrelated")
|
|
|
- relation_counts[relation] = relation_counts.get(relation, 0) + 1
|
|
|
-
|
|
|
- # 生成统计信息
|
|
|
- stats_html = "<div class='relation-stats'>"
|
|
|
- for relation, count in sorted(relation_counts.items(), key=lambda x: x[1], reverse=True):
|
|
|
- label = get_relation_label(relation)
|
|
|
- color = get_relation_color(relation)
|
|
|
- stats_html += f"<span class='stat-item' style='color: {color};'>{label}: {count}</span>"
|
|
|
- stats_html += "</div>"
|
|
|
-
|
|
|
- matches_html = "".join(generate_match_item_html(match) for match in sorted_matches)
|
|
|
-
|
|
|
- html = f"""
|
|
|
- <div class="feature-section">
|
|
|
- <div class="feature-header">
|
|
|
- <h3>特征: {feature_name}</h3>
|
|
|
- {stats_html}
|
|
|
- </div>
|
|
|
- <div class="matches-container">
|
|
|
- {matches_html}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- """
|
|
|
- return html
|
|
|
-
|
|
|
-
|
|
|
-def generate_inspiration_point_html(point_data: Dict) -> str:
|
|
|
- """生成单个灵感点的HTML"""
|
|
|
- name = point_data.get("名称", "")
|
|
|
- desc = point_data.get("描述", "")
|
|
|
- how_steps = point_data.get("how步骤列表", [])
|
|
|
-
|
|
|
- steps_html = ""
|
|
|
- for step in how_steps:
|
|
|
- step_name = step.get("步骤名称", "")
|
|
|
- features = step.get("特征列表", [])
|
|
|
-
|
|
|
- features_html = "".join(generate_feature_html(f) for f in features)
|
|
|
-
|
|
|
- steps_html += f"""
|
|
|
- <div class="step-section">
|
|
|
- <h4 class="step-name">{step_name}</h4>
|
|
|
- {features_html}
|
|
|
- </div>
|
|
|
- """
|
|
|
-
|
|
|
- html = f"""
|
|
|
- <div class="inspiration-point">
|
|
|
- <div class="point-header">
|
|
|
- <h2>{name}</h2>
|
|
|
- </div>
|
|
|
- <div class="point-description">{desc}</div>
|
|
|
- {steps_html}
|
|
|
- </div>
|
|
|
- """
|
|
|
- return html
|
|
|
-
|
|
|
-
|
|
|
-def generate_post_content_html(post_data: Dict) -> str:
|
|
|
- """生成单个帖子的内容HTML(不包含完整页面结构)"""
|
|
|
- post_id = post_data.get("帖子id", "")
|
|
|
- post_detail = post_data.get("帖子详情", {})
|
|
|
- publish_time = post_detail.get("publish_time", "")
|
|
|
- like_count = post_detail.get("like_count", 0)
|
|
|
- link = post_detail.get("link", "")
|
|
|
-
|
|
|
- how_result = post_data.get("how解构结果", {})
|
|
|
- inspiration_list = how_result.get("灵感点列表", [])
|
|
|
-
|
|
|
- inspiration_html = "".join(generate_inspiration_point_html(p) for p in inspiration_list)
|
|
|
-
|
|
|
- html = f"""
|
|
|
- <div class="post-meta-bar">
|
|
|
- <div class="meta-item">
|
|
|
- <span class="meta-label">帖子ID:</span>
|
|
|
- <span class="meta-value">{post_id}</span>
|
|
|
- </div>
|
|
|
- <div class="meta-item">
|
|
|
- <span class="meta-label">发布时间:</span>
|
|
|
- <span class="meta-value">{publish_time}</span>
|
|
|
- </div>
|
|
|
- <div class="meta-item">
|
|
|
- <span class="meta-label">点赞数:</span>
|
|
|
- <span class="meta-value">{like_count}</span>
|
|
|
- </div>
|
|
|
- <div class="meta-item">
|
|
|
- <a href="{link}" target="_blank" class="view-link">查看原帖 →</a>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- {inspiration_html}
|
|
|
- """
|
|
|
- return html
|
|
|
-
|
|
|
-
|
|
|
-def generate_combined_html(posts_data: List[Dict]) -> str:
|
|
|
- """生成包含所有帖子的单一HTML(带标签页)"""
|
|
|
-
|
|
|
- # 生成标签页按钮
|
|
|
- tabs_html = ""
|
|
|
- for i, post in enumerate(posts_data):
|
|
|
- post_detail = post.get("帖子详情", {})
|
|
|
- title = post_detail.get("title", "无标题")
|
|
|
- active_class = "active" if i == 0 else ""
|
|
|
- tabs_html += f'<button class="tab-button {active_class}" onclick="openTab(event, \'post-{i}\')">{title}</button>\n'
|
|
|
-
|
|
|
- # 生成标签页内容
|
|
|
- contents_html = ""
|
|
|
- for i, post in enumerate(posts_data):
|
|
|
- active_class = "active" if i == 0 else ""
|
|
|
- content = generate_post_content_html(post)
|
|
|
- contents_html += f"""
|
|
|
- <div id="post-{i}" class="tab-content {active_class}">
|
|
|
- {content}
|
|
|
- </div>
|
|
|
- """
|
|
|
-
|
|
|
- html = f"""
|
|
|
- <!DOCTYPE html>
|
|
|
- <html lang="zh-CN">
|
|
|
- <head>
|
|
|
- <meta charset="UTF-8">
|
|
|
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>How解构结果可视化</title>
|
|
|
- <style>
|
|
|
- * {{
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- box-sizing: border-box;
|
|
|
- }}
|
|
|
-
|
|
|
- body {{
|
|
|
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
- background: #f5f5f5;
|
|
|
- color: #333;
|
|
|
- line-height: 1.6;
|
|
|
- }}
|
|
|
-
|
|
|
- .container {{
|
|
|
- max-width: 1400px;
|
|
|
- margin: 0 auto;
|
|
|
- background: white;
|
|
|
- min-height: 100vh;
|
|
|
- box-shadow: 0 0 40px rgba(0,0,0,0.1);
|
|
|
- }}
|
|
|
-
|
|
|
- .header {{
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
- color: white;
|
|
|
- padding: 30px;
|
|
|
- text-align: center;
|
|
|
- }}
|
|
|
-
|
|
|
- .header h1 {{
|
|
|
- font-size: 32px;
|
|
|
- font-weight: bold;
|
|
|
- margin-bottom: 10px;
|
|
|
- }}
|
|
|
-
|
|
|
- .header p {{
|
|
|
- font-size: 16px;
|
|
|
- opacity: 0.9;
|
|
|
- }}
|
|
|
-
|
|
|
- .tabs-container {{
|
|
|
- display: flex;
|
|
|
- background: #f9fafb;
|
|
|
- border-bottom: 2px solid #e5e7eb;
|
|
|
- overflow-x: auto;
|
|
|
- position: sticky;
|
|
|
- top: 0;
|
|
|
- z-index: 100;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-button {{
|
|
|
- flex: 1;
|
|
|
- min-width: 200px;
|
|
|
- padding: 18px 30px;
|
|
|
- background: transparent;
|
|
|
- border: none;
|
|
|
- border-bottom: 3px solid transparent;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 15px;
|
|
|
- font-weight: 500;
|
|
|
- color: #6b7280;
|
|
|
- transition: all 0.3s;
|
|
|
- white-space: nowrap;
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-button:hover {{
|
|
|
- background: #f3f4f6;
|
|
|
- color: #374151;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-button.active {{
|
|
|
- color: #667eea;
|
|
|
- border-bottom-color: #667eea;
|
|
|
- background: white;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-content {{
|
|
|
- display: none;
|
|
|
- padding: 30px;
|
|
|
- animation: fadeIn 0.3s;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-content.active {{
|
|
|
- display: block;
|
|
|
- }}
|
|
|
-
|
|
|
- @keyframes fadeIn {{
|
|
|
- from {{ opacity: 0; transform: translateY(10px); }}
|
|
|
- to {{ opacity: 1; transform: translateY(0); }}
|
|
|
- }}
|
|
|
-
|
|
|
- .post-meta-bar {{
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 25px;
|
|
|
- padding: 20px;
|
|
|
- background: #f9fafb;
|
|
|
- border-radius: 8px;
|
|
|
- margin-bottom: 30px;
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
- }}
|
|
|
-
|
|
|
- .meta-item {{
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- }}
|
|
|
-
|
|
|
- .meta-label {{
|
|
|
- font-weight: 600;
|
|
|
- color: #6b7280;
|
|
|
- font-size: 14px;
|
|
|
- }}
|
|
|
-
|
|
|
- .meta-value {{
|
|
|
- color: #111827;
|
|
|
- font-size: 14px;
|
|
|
- }}
|
|
|
-
|
|
|
- .view-link {{
|
|
|
- color: #667eea;
|
|
|
- text-decoration: none;
|
|
|
- font-weight: 600;
|
|
|
- font-size: 14px;
|
|
|
- transition: color 0.2s;
|
|
|
- }}
|
|
|
-
|
|
|
- .view-link:hover {{
|
|
|
- color: #764ba2;
|
|
|
- }}
|
|
|
-
|
|
|
- .inspiration-point {{
|
|
|
- margin-bottom: 40px;
|
|
|
- border: 1px solid #e5e7eb;
|
|
|
- border-radius: 8px;
|
|
|
- overflow: hidden;
|
|
|
- }}
|
|
|
-
|
|
|
- .point-header {{
|
|
|
- background: #f9fafb;
|
|
|
- padding: 20px;
|
|
|
- border-bottom: 2px solid #e5e7eb;
|
|
|
- }}
|
|
|
-
|
|
|
- .point-header h2 {{
|
|
|
- font-size: 22px;
|
|
|
- color: #1f2937;
|
|
|
- }}
|
|
|
-
|
|
|
- .point-description {{
|
|
|
- padding: 20px;
|
|
|
- background: #fefefe;
|
|
|
- font-size: 15px;
|
|
|
- color: #4b5563;
|
|
|
- line-height: 1.8;
|
|
|
- border-bottom: 1px solid #e5e7eb;
|
|
|
- }}
|
|
|
-
|
|
|
- .step-section {{
|
|
|
- padding: 20px;
|
|
|
- }}
|
|
|
-
|
|
|
- .step-name {{
|
|
|
- font-size: 18px;
|
|
|
- color: #374151;
|
|
|
- margin-bottom: 20px;
|
|
|
- padding-bottom: 10px;
|
|
|
- border-bottom: 2px solid #e5e7eb;
|
|
|
- }}
|
|
|
-
|
|
|
- .feature-section {{
|
|
|
- margin-bottom: 30px;
|
|
|
- }}
|
|
|
-
|
|
|
- .feature-header {{
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 15px;
|
|
|
- padding: 15px;
|
|
|
- background: #f3f4f6;
|
|
|
- border-radius: 6px;
|
|
|
- }}
|
|
|
-
|
|
|
- .feature-header h3 {{
|
|
|
- font-size: 18px;
|
|
|
- color: #111827;
|
|
|
- }}
|
|
|
-
|
|
|
- .relation-stats {{
|
|
|
- display: flex;
|
|
|
- gap: 15px;
|
|
|
- font-size: 13px;
|
|
|
- }}
|
|
|
-
|
|
|
- .stat-item {{
|
|
|
- font-weight: 600;
|
|
|
- }}
|
|
|
-
|
|
|
- .matches-container {{
|
|
|
- display: grid;
|
|
|
- gap: 10px;
|
|
|
- }}
|
|
|
-
|
|
|
- .match-item {{
|
|
|
- padding: 15px;
|
|
|
- border-radius: 6px;
|
|
|
- transition: transform 0.2s;
|
|
|
- }}
|
|
|
-
|
|
|
- .match-item:hover {{
|
|
|
- transform: translateX(5px);
|
|
|
- }}
|
|
|
-
|
|
|
- .match-header {{
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 8px;
|
|
|
- }}
|
|
|
-
|
|
|
- .persona-name {{
|
|
|
- font-weight: 600;
|
|
|
- font-size: 15px;
|
|
|
- color: #111827;
|
|
|
- }}
|
|
|
-
|
|
|
- .relation-badge {{
|
|
|
- padding: 3px 10px;
|
|
|
- border-radius: 12px;
|
|
|
- color: white;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 600;
|
|
|
- }}
|
|
|
-
|
|
|
- .score-badge {{
|
|
|
- padding: 3px 10px;
|
|
|
- border-radius: 12px;
|
|
|
- background: #e5e7eb;
|
|
|
- color: #374151;
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 600;
|
|
|
- }}
|
|
|
-
|
|
|
- .match-explanation {{
|
|
|
- font-size: 14px;
|
|
|
- color: #6b7280;
|
|
|
- line-height: 1.6;
|
|
|
- }}
|
|
|
-
|
|
|
- @media (max-width: 768px) {{
|
|
|
- .header {{
|
|
|
- padding: 20px;
|
|
|
- }}
|
|
|
-
|
|
|
- .header h1 {{
|
|
|
- font-size: 24px;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-button {{
|
|
|
- min-width: 150px;
|
|
|
- padding: 15px 20px;
|
|
|
- font-size: 14px;
|
|
|
- }}
|
|
|
-
|
|
|
- .tab-content {{
|
|
|
- padding: 15px;
|
|
|
- }}
|
|
|
-
|
|
|
- .post-meta-bar {{
|
|
|
- flex-direction: column;
|
|
|
- gap: 10px;
|
|
|
- }}
|
|
|
-
|
|
|
- .feature-header {{
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-start;
|
|
|
- gap: 10px;
|
|
|
- }}
|
|
|
-
|
|
|
- .relation-stats {{
|
|
|
- flex-wrap: wrap;
|
|
|
- }}
|
|
|
- }}
|
|
|
- </style>
|
|
|
- </head>
|
|
|
- <body>
|
|
|
- <div class="container">
|
|
|
- <div class="header">
|
|
|
- <h1>How 解构结果可视化</h1>
|
|
|
- <p>灵感点特征匹配分析</p>
|
|
|
- </div>
|
|
|
- <div class="tabs-container">
|
|
|
- {tabs_html}
|
|
|
- </div>
|
|
|
- {contents_html}
|
|
|
- </div>
|
|
|
-
|
|
|
- <script>
|
|
|
- function openTab(evt, tabId) {{
|
|
|
- // 隐藏所有标签页内容
|
|
|
- var tabContents = document.getElementsByClassName("tab-content");
|
|
|
- for (var i = 0; i < tabContents.length; i++) {{
|
|
|
- tabContents[i].classList.remove("active");
|
|
|
- }}
|
|
|
-
|
|
|
- // 移除所有按钮的 active 类
|
|
|
- var tabButtons = document.getElementsByClassName("tab-button");
|
|
|
- for (var i = 0; i < tabButtons.length; i++) {{
|
|
|
- tabButtons[i].classList.remove("active");
|
|
|
- }}
|
|
|
-
|
|
|
- // 显示当前标签页并添加 active 类
|
|
|
- document.getElementById(tabId).classList.add("active");
|
|
|
- evt.currentTarget.classList.add("active");
|
|
|
- }}
|
|
|
- </script>
|
|
|
- </body>
|
|
|
- </html>
|
|
|
- """
|
|
|
- return html
|
|
|
-
|
|
|
-
|
|
|
-def main():
|
|
|
- """主函数"""
|
|
|
- # 输入输出路径
|
|
|
- script_dir = Path(__file__).parent
|
|
|
- project_root = script_dir.parent.parent
|
|
|
- data_dir = project_root / "data" / "data_1117"
|
|
|
-
|
|
|
- input_dir = data_dir / "当前帖子_how解构结果"
|
|
|
- output_file = data_dir / "当前帖子_how解构结果_可视化.html"
|
|
|
-
|
|
|
- print(f"读取 how 解构结果: {input_dir}")
|
|
|
-
|
|
|
- # 获取所有 JSON 文件
|
|
|
- json_files = list(input_dir.glob("*_how.json"))
|
|
|
- print(f"找到 {len(json_files)} 个文件\n")
|
|
|
-
|
|
|
- # 读取所有帖子数据
|
|
|
- posts_data = []
|
|
|
- for i, file_path in enumerate(json_files, 1):
|
|
|
- print(f"读取文件 [{i}/{len(json_files)}]: {file_path.name}")
|
|
|
- with open(file_path, "r", encoding="utf-8") as f:
|
|
|
- post_data = json.load(f)
|
|
|
- posts_data.append(post_data)
|
|
|
-
|
|
|
- # 生成合并的 HTML
|
|
|
- print(f"\n生成合并的 HTML...")
|
|
|
- html_content = generate_combined_html(posts_data)
|
|
|
-
|
|
|
- # 保存 HTML 文件
|
|
|
- print(f"保存到: {output_file}")
|
|
|
- with open(output_file, "w", encoding="utf-8") as f:
|
|
|
- f.write(html_content)
|
|
|
-
|
|
|
- print(f"\n完成! 可视化文件已保存")
|
|
|
- print(f"请在浏览器中打开: {output_file}")
|
|
|
-
|
|
|
-
|
|
|
-if __name__ == "__main__":
|
|
|
- main()
|