huhan3000/ai-tools/scripts/neo4j_web_app.py

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)