420 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			420 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
| #!/usr/bin/env python3
 | |
| """
 | |
| 圐圙文化网络 - Web 可视化应用
 | |
| 使用 Flask + Neo4j + D3.js
 | |
| """
 | |
| 
 | |
| from flask import Flask, render_template, jsonify, request
 | |
| from neo4j import GraphDatabase
 | |
| import json
 | |
| 
 | |
| app = Flask(__name__)
 | |
| 
 | |
| class KulueNetworkAPI:
 | |
|     def __init__(self, uri="bolt://localhost:7687", user="neo4j", password="password"):
 | |
|         self.driver = GraphDatabase.driver(uri, auth=(user, password))
 | |
|     
 | |
|     def close(self):
 | |
|         self.driver.close()
 | |
|     
 | |
|     def get_network_data(self):
 | |
|         """获取完整网络数据用于可视化"""
 | |
|         with self.driver.session() as session:
 | |
|             # 获取所有节点
 | |
|             nodes_result = session.run("""
 | |
|                 MATCH (w:Word)
 | |
|                 RETURN w.name as name, w.category as category, 
 | |
|                        w.meaning as meaning, w.region as region, w.dynasty as dynasty
 | |
|             """)
 | |
|             
 | |
|             nodes = []
 | |
|             for record in nodes_result:
 | |
|                 nodes.append({
 | |
|                     'id': record['name'],
 | |
|                     'name': record['name'],
 | |
|                     'category': record['category'],
 | |
|                     'meaning': record['meaning'],
 | |
|                     'region': record['region'],
 | |
|                     'dynasty': record['dynasty']
 | |
|                 })
 | |
|             
 | |
|             # 获取所有关系
 | |
|             links_result = session.run("""
 | |
|                 MATCH (source:Word)-[r]-(target:Word)
 | |
|                 RETURN source.name as source, target.name as target, 
 | |
|                        type(r) as type, r.type as subtype
 | |
|             """)
 | |
|             
 | |
|             links = []
 | |
|             processed_pairs = set()
 | |
|             
 | |
|             for record in links_result:
 | |
|                 source = record['source']
 | |
|                 target = record['target']
 | |
|                 
 | |
|                 # 避免重复的无向边
 | |
|                 pair = tuple(sorted([source, target]))
 | |
|                 if pair not in processed_pairs:
 | |
|                     processed_pairs.add(pair)
 | |
|                     links.append({
 | |
|                         'source': source,
 | |
|                         'target': target,
 | |
|                         'type': record['type'],
 | |
|                         'subtype': record['subtype']
 | |
|                     })
 | |
|             
 | |
|             return {'nodes': nodes, 'links': links}
 | |
|     
 | |
|     def search_word(self, word_name):
 | |
|         """搜索特定词汇的关联"""
 | |
|         with self.driver.session() as session:
 | |
|             result = session.run("""
 | |
|                 MATCH (center:Word {name: $word})-[r]-(connected:Word)
 | |
|                 RETURN center, r, connected
 | |
|             """, word=word_name)
 | |
|             
 | |
|             data = []
 | |
|             for record in result:
 | |
|                 center = record['center']
 | |
|                 relation = record['r']
 | |
|                 connected = record['connected']
 | |
|                 
 | |
|                 data.append({
 | |
|                     'center': dict(center),
 | |
|                     'relation': {
 | |
|                         'type': relation.type,
 | |
|                         'properties': dict(relation)
 | |
|                     },
 | |
|                     'connected': dict(connected)
 | |
|                 })
 | |
|             
 | |
|             return data
 | |
|     
 | |
|     def get_categories_stats(self):
 | |
|         """获取类别统计"""
 | |
|         with self.driver.session() as session:
 | |
|             result = session.run("""
 | |
|                 MATCH (w:Word)
 | |
|                 RETURN w.category as category, count(w) as count
 | |
|                 ORDER BY count DESC
 | |
|             """)
 | |
|             
 | |
|             return [{'category': record['category'], 'count': record['count']} 
 | |
|                     for record in result]
 | |
|     
 | |
|     def get_sound_shift_paths(self, start_word):
 | |
|         """获取音转路径"""
 | |
|         with self.driver.session() as session:
 | |
|             result = session.run("""
 | |
|                 MATCH path = (start:Word {name: $start})-[:SOUND_SHIFT*1..3]-(end:Word)
 | |
|                 RETURN [node in nodes(path) | node.name] as path_nodes,
 | |
|                        length(path) as path_length
 | |
|                 ORDER BY path_length
 | |
|             """, start=start_word)
 | |
|             
 | |
|             return [{'path': record['path_nodes'], 'length': record['path_length']} 
 | |
|                     for record in result]
 | |
| 
 | |
| # 创建API实例
 | |
| kulue_api = KulueNetworkAPI()
 | |
| 
 | |
| @app.route('/')
 | |
| def index():
 | |
|     """主页"""
 | |
|     return render_template('index.html')
 | |
| 
 | |
| @app.route('/api/network')
 | |
| def get_network():
 | |
|     """获取网络数据API"""
 | |
|     try:
 | |
|         data = kulue_api.get_network_data()
 | |
|         return jsonify(data)
 | |
|     except Exception as e:
 | |
|         return jsonify({'error': str(e)}), 500
 | |
| 
 | |
| @app.route('/api/search/<word>')
 | |
| def search_word(word):
 | |
|     """搜索词汇API"""
 | |
|     try:
 | |
|         data = kulue_api.search_word(word)
 | |
|         return jsonify(data)
 | |
|     except Exception as e:
 | |
|         return jsonify({'error': str(e)}), 500
 | |
| 
 | |
| @app.route('/api/stats/categories')
 | |
| def get_categories():
 | |
|     """获取类别统计API"""
 | |
|     try:
 | |
|         data = kulue_api.get_categories_stats()
 | |
|         return jsonify(data)
 | |
|     except Exception as e:
 | |
|         return jsonify({'error': str(e)}), 500
 | |
| 
 | |
| @app.route('/api/sound-shift/<word>')
 | |
| def get_sound_shift(word):
 | |
|     """获取音转路径API"""
 | |
|     try:
 | |
|         data = kulue_api.get_sound_shift_paths(word)
 | |
|         return jsonify(data)
 | |
|     except Exception as e:
 | |
|         return jsonify({'error': str(e)}), 500
 | |
| 
 | |
| # HTML 模板
 | |
| html_template = '''
 | |
| <!DOCTYPE html>
 | |
| <html>
 | |
| <head>
 | |
|     <title>圐圙文化网络</title>
 | |
|     <script src="https://d3js.org/d3.v7.min.js"></script>
 | |
|     <style>
 | |
|         body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
 | |
|         .container { max-width: 1200px; margin: 0 auto; }
 | |
|         .controls { margin-bottom: 20px; }
 | |
|         .network-container { border: 1px solid #ccc; }
 | |
|         .node { cursor: pointer; }
 | |
|         .link { stroke: #999; stroke-opacity: 0.6; }
 | |
|         .tooltip { position: absolute; background: rgba(0,0,0,0.8); color: white; 
 | |
|                    padding: 10px; border-radius: 5px; pointer-events: none; }
 | |
|         .legend { margin-top: 20px; }
 | |
|         .legend-item { display: inline-block; margin-right: 20px; }
 | |
|         .legend-color { width: 20px; height: 20px; display: inline-block; margin-right: 5px; }
 | |
|     </style>
 | |
| </head>
 | |
| <body>
 | |
|     <div class="container">
 | |
|         <h1>圐圙文化网络可视化</h1>
 | |
|         
 | |
|         <div class="controls">
 | |
|             <input type="text" id="searchInput" placeholder="搜索词汇...">
 | |
|             <button onclick="searchWord()">搜索</button>
 | |
|             <button onclick="resetView()">重置</button>
 | |
|         </div>
 | |
|         
 | |
|         <div id="network" class="network-container"></div>
 | |
|         
 | |
|         <div class="legend" id="legend"></div>
 | |
|         
 | |
|         <div id="info" style="margin-top: 20px;"></div>
 | |
|     </div>
 | |
|     
 | |
|     <div id="tooltip" class="tooltip" style="display: none;"></div>
 | |
| 
 | |
|     <script>
 | |
|         // 网络可视化代码
 | |
|         const width = 1160;
 | |
|         const height = 600;
 | |
|         
 | |
|         const svg = d3.select("#network")
 | |
|             .append("svg")
 | |
|             .attr("width", width)
 | |
|             .attr("height", height);
 | |
|         
 | |
|         const g = svg.append("g");
 | |
|         
 | |
|         // 缩放功能
 | |
|         const zoom = d3.zoom()
 | |
|             .scaleExtent([0.1, 3])
 | |
|             .on("zoom", (event) => {
 | |
|                 g.attr("transform", event.transform);
 | |
|             });
 | |
|         
 | |
|         svg.call(zoom);
 | |
|         
 | |
|         // 颜色映射
 | |
|         const colorScale = d3.scaleOrdinal()
 | |
|             .domain(['地理', '器物', '政治', '文化', '核心'])
 | |
|             .range(['#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#1f77b4']);
 | |
|         
 | |
|         let simulation, nodes, links;
 | |
|         
 | |
|         // 加载网络数据
 | |
|         async function loadNetwork() {
 | |
|             try {
 | |
|                 const response = await fetch('/api/network');
 | |
|                 const data = await response.json();
 | |
|                 
 | |
|                 nodes = data.nodes;
 | |
|                 links = data.links;
 | |
|                 
 | |
|                 createVisualization();
 | |
|                 createLegend();
 | |
|                 
 | |
|             } catch (error) {
 | |
|                 console.error('加载数据失败:', error);
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         function createVisualization() {
 | |
|             // 创建力导向图
 | |
|             simulation = d3.forceSimulation(nodes)
 | |
|                 .force("link", d3.forceLink(links).id(d => d.id).distance(100))
 | |
|                 .force("charge", d3.forceManyBody().strength(-300))
 | |
|                 .force("center", d3.forceCenter(width / 2, height / 2));
 | |
|             
 | |
|             // 绘制连线
 | |
|             const link = g.append("g")
 | |
|                 .selectAll("line")
 | |
|                 .data(links)
 | |
|                 .enter().append("line")
 | |
|                 .attr("class", "link")
 | |
|                 .attr("stroke-width", d => d.type === 'SOUND_SHIFT' ? 3 : 1)
 | |
|                 .attr("stroke", d => d.type === 'SOUND_SHIFT' ? '#ff0000' : '#999');
 | |
|             
 | |
|             // 绘制节点
 | |
|             const node = g.append("g")
 | |
|                 .selectAll("circle")
 | |
|                 .data(nodes)
 | |
|                 .enter().append("circle")
 | |
|                 .attr("class", "node")
 | |
|                 .attr("r", d => d.category === '核心' ? 15 : 10)
 | |
|                 .attr("fill", d => colorScale(d.category))
 | |
|                 .call(d3.drag()
 | |
|                     .on("start", dragstarted)
 | |
|                     .on("drag", dragged)
 | |
|                     .on("end", dragended));
 | |
|             
 | |
|             // 添加标签
 | |
|             const label = g.append("g")
 | |
|                 .selectAll("text")
 | |
|                 .data(nodes)
 | |
|                 .enter().append("text")
 | |
|                 .text(d => d.name)
 | |
|                 .attr("font-size", "12px")
 | |
|                 .attr("dx", 15)
 | |
|                 .attr("dy", 4);
 | |
|             
 | |
|             // 鼠标事件
 | |
|             node.on("mouseover", function(event, d) {
 | |
|                 d3.select("#tooltip")
 | |
|                     .style("display", "block")
 | |
|                     .style("left", (event.pageX + 10) + "px")
 | |
|                     .style("top", (event.pageY - 10) + "px")
 | |
|                     .html(`<strong>${d.name}</strong><br/>
 | |
|                            类别: ${d.category}<br/>
 | |
|                            含义: ${d.meaning}<br/>
 | |
|                            地区: ${d.region}<br/>
 | |
|                            朝代: ${d.dynasty}`);
 | |
|             })
 | |
|             .on("mouseout", function() {
 | |
|                 d3.select("#tooltip").style("display", "none");
 | |
|             })
 | |
|             .on("click", function(event, d) {
 | |
|                 searchWord(d.name);
 | |
|             });
 | |
|             
 | |
|             // 更新位置
 | |
|             simulation.on("tick", () => {
 | |
|                 link
 | |
|                     .attr("x1", d => d.source.x)
 | |
|                     .attr("y1", d => d.source.y)
 | |
|                     .attr("x2", d => d.target.x)
 | |
|                     .attr("y2", d => d.target.y);
 | |
|                 
 | |
|                 node
 | |
|                     .attr("cx", d => d.x)
 | |
|                     .attr("cy", d => d.y);
 | |
|                 
 | |
|                 label
 | |
|                     .attr("x", d => d.x)
 | |
|                     .attr("y", d => d.y);
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         function createLegend() {
 | |
|             const legend = d3.select("#legend");
 | |
|             const categories = ['地理', '器物', '政治', '文化', '核心'];
 | |
|             
 | |
|             categories.forEach(category => {
 | |
|                 const item = legend.append("div").attr("class", "legend-item");
 | |
|                 item.append("div")
 | |
|                     .attr("class", "legend-color")
 | |
|                     .style("background-color", colorScale(category));
 | |
|                 item.append("span").text(category);
 | |
|             });
 | |
|         }
 | |
|         
 | |
|         // 拖拽功能
 | |
|         function dragstarted(event, d) {
 | |
|             if (!event.active) simulation.alphaTarget(0.3).restart();
 | |
|             d.fx = d.x;
 | |
|             d.fy = d.y;
 | |
|         }
 | |
|         
 | |
|         function dragged(event, d) {
 | |
|             d.fx = event.x;
 | |
|             d.fy = event.y;
 | |
|         }
 | |
|         
 | |
|         function dragended(event, d) {
 | |
|             if (!event.active) simulation.alphaTarget(0);
 | |
|             d.fx = null;
 | |
|             d.fy = null;
 | |
|         }
 | |
|         
 | |
|         // 搜索功能
 | |
|         async function searchWord(word) {
 | |
|             if (!word) word = document.getElementById('searchInput').value;
 | |
|             if (!word) return;
 | |
|             
 | |
|             try {
 | |
|                 const response = await fetch(`/api/search/${word}`);
 | |
|                 const data = await response.json();
 | |
|                 
 | |
|                 // 高亮相关节点
 | |
|                 d3.selectAll(".node")
 | |
|                     .attr("stroke", d => {
 | |
|                         const isRelated = data.some(item => 
 | |
|                             item.center.name === d.name || item.connected.name === d.name
 | |
|                         );
 | |
|                         return isRelated ? "#000" : "none";
 | |
|                     })
 | |
|                     .attr("stroke-width", d => {
 | |
|                         const isRelated = data.some(item => 
 | |
|                             item.center.name === d.name || item.connected.name === d.name
 | |
|                         );
 | |
|                         return isRelated ? 3 : 0;
 | |
|                     });
 | |
|                 
 | |
|                 // 显示搜索结果
 | |
|                 const info = d3.select("#info");
 | |
|                 info.html(`<h3>搜索结果: ${word}</h3>`);
 | |
|                 
 | |
|                 if (data.length > 0) {
 | |
|                     const list = info.append("ul");
 | |
|                     data.forEach(item => {
 | |
|                         list.append("li")
 | |
|                             .html(`${item.connected.name} (${item.relation.type}) - ${item.connected.meaning}`);
 | |
|                     });
 | |
|                 } else {
 | |
|                     info.append("p").text("未找到相关词汇");
 | |
|                 }
 | |
|                 
 | |
|             } catch (error) {
 | |
|                 console.error('搜索失败:', error);
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         function resetView() {
 | |
|             d3.selectAll(".node")
 | |
|                 .attr("stroke", "none")
 | |
|                 .attr("stroke-width", 0);
 | |
|             
 | |
|             d3.select("#info").html("");
 | |
|             document.getElementById('searchInput').value = "";
 | |
|         }
 | |
|         
 | |
|         // 页面加载时初始化
 | |
|         loadNetwork();
 | |
|     </script>
 | |
| </body>
 | |
| </html>
 | |
| '''
 | |
| 
 | |
| # 创建模板目录和文件
 | |
| import os
 | |
| os.makedirs('templates', exist_ok=True)
 | |
| with open('templates/index.html', 'w', encoding='utf-8') as f:
 | |
|     f.write(html_template)
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     app.run(debug=True, host='0.0.0.0', port=5000) |