| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- #!/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()
|