init: KG_ICH 项目初始化
- data/: 非遗地理编码数据(GIS shapefile + CSV) - dofile/kg_project/: 知识图谱构建代码(纳入主仓库) - dofile/visulization/: 可视化数据与路线图 - officefile/: 文献、草稿、bib 文档 - officefile/latex/: Overleaf 同步目录(独立管理,不纳入) - output/: 输出目录 - logs/: 日志目录
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Data files
|
||||
data/
|
||||
output/
|
||||
logs/
|
||||
|
||||
# Configuration files (entire directory)
|
||||
config/
|
||||
|
||||
# Neo4j (entire directory)
|
||||
neo4j/
|
||||
|
||||
# Ontology files
|
||||
ontology/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
|
||||
# Batch files
|
||||
*.bat
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
*.ipynb
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
整合基础政务实体和深层语义实体
|
||||
方案3:合并项目节点,完全整合
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
def merge_kg_data():
|
||||
"""整合知识图谱数据"""
|
||||
|
||||
output_dir = Path(__file__).parent / 'output'
|
||||
|
||||
# 读取4个文件
|
||||
print("=== 读取原始文件 ===")
|
||||
nodes_basic = pd.read_csv(output_dir / 'nodes.csv', encoding='utf-8-sig')
|
||||
nodes_deep = pd.read_csv(output_dir / 'nodes_desc.csv', encoding='utf-8-sig')
|
||||
rels_basic = pd.read_csv(output_dir / 'rels.csv', encoding='utf-8-sig')
|
||||
rels_deep = pd.read_csv(output_dir / 'rels_desc.csv', encoding='utf-8-sig')
|
||||
|
||||
print(f"基础节点: {len(nodes_basic)}")
|
||||
print(f"深层节点: {len(nodes_deep)}")
|
||||
print(f"基础关系: {len(rels_basic)}")
|
||||
print(f"深层关系: {len(rels_deep)}")
|
||||
|
||||
# 1. 合并ICH项目节点
|
||||
print("\n=== 合并ICH项目节点 ===")
|
||||
basic_projects = nodes_basic[nodes_basic['type'] == 'ICH_Project'].copy()
|
||||
deep_projects = nodes_deep[nodes_deep['type'] == 'ICH_Project'].copy()
|
||||
|
||||
print(f"基础项目节点: {len(basic_projects)}")
|
||||
print(f"深层项目节点: {len(deep_projects)}")
|
||||
|
||||
# 合并项目节点的属性
|
||||
merged_projects = []
|
||||
|
||||
for pid in basic_projects['id']:
|
||||
basic_row = basic_projects[basic_projects['id'] == pid].iloc[0]
|
||||
|
||||
# 查找深层项目节点
|
||||
deep_row = deep_projects[deep_projects['id'] == pid]
|
||||
|
||||
if len(deep_row) > 0:
|
||||
# 合并属性
|
||||
deep_row = deep_row.iloc[0]
|
||||
|
||||
# 解析属性JSON
|
||||
basic_props = json.loads(basic_row['properties']) if basic_row['properties'] else {}
|
||||
deep_props = json.loads(deep_row['properties']) if deep_row['properties'] else {}
|
||||
|
||||
# 合并属性(深层属性优先)
|
||||
merged_props = {**basic_props, **deep_props}
|
||||
|
||||
merged_projects.append({
|
||||
'id': pid,
|
||||
'label': basic_row['label'], # 保留基础标签
|
||||
'type': 'ICH_Project',
|
||||
'properties': json.dumps(merged_props, ensure_ascii=False)
|
||||
})
|
||||
else:
|
||||
# 只在基础中存在
|
||||
merged_projects.append({
|
||||
'id': pid,
|
||||
'label': basic_row['label'],
|
||||
'type': 'ICH_Project',
|
||||
'properties': basic_row['properties']
|
||||
})
|
||||
|
||||
print(f"合并后项目节点: {len(merged_projects)}")
|
||||
|
||||
# 2. 合并其他节点(去除ICH_Project)
|
||||
print("\n=== 合并其他节点 ===")
|
||||
other_basic_nodes = nodes_basic[nodes_basic['type'] != 'ICH_Project']
|
||||
other_deep_nodes = nodes_deep[nodes_deep['type'] != 'ICH_Project']
|
||||
|
||||
all_nodes = pd.concat([
|
||||
pd.DataFrame(merged_projects),
|
||||
other_basic_nodes,
|
||||
other_deep_nodes
|
||||
], ignore_index=True)
|
||||
|
||||
print(f"合并后总节点数: {len(all_nodes)}")
|
||||
print(f" - ICH_Project: {len(merged_projects)}")
|
||||
print(f" - 其他节点: {len(all_nodes) - len(merged_projects)}")
|
||||
|
||||
# 3. 合并关系
|
||||
print("\n=== 合并关系 ===")
|
||||
all_rels = pd.concat([rels_basic, rels_deep], ignore_index=True)
|
||||
|
||||
# 去重关系
|
||||
all_rels = all_rels.drop_duplicates(subset=['source', 'target', 'type'], keep='first')
|
||||
|
||||
print(f"合并后关系数: {len(all_rels)}")
|
||||
|
||||
# 4. 统计信息
|
||||
print("\n=== 整合后统计 ===")
|
||||
print(f"总节点数: {len(all_nodes)}")
|
||||
print(f"总关系数: {len(all_rels)}")
|
||||
|
||||
print("\n节点类型分布:")
|
||||
for ntype, count in all_nodes.groupby('type').size().items():
|
||||
print(f" {ntype}: {count}")
|
||||
|
||||
print(f"\n关系类型数量: {len(all_rels['type'].unique())}")
|
||||
|
||||
# 5. 保存整合后的文件
|
||||
print("\n=== 保存整合文件 ===")
|
||||
output_file_nodes = output_dir / 'kg_merged_nodes.csv'
|
||||
output_file_rels = output_dir / 'kg_merged_rels.csv'
|
||||
|
||||
all_nodes.to_csv(output_file_nodes, index=False, encoding='utf-8-sig')
|
||||
all_rels.to_csv(output_file_rels, index=False, encoding='utf-8-sig')
|
||||
|
||||
print(f"节点已保存: {output_file_nodes}")
|
||||
print(f"关系已保存: {output_file_rels}")
|
||||
|
||||
# 6. 验证
|
||||
print("\n=== 验证 ===")
|
||||
|
||||
# 检查项目节点完整性
|
||||
project_count = len(all_nodes[all_nodes['type'] == 'ICH_Project'])
|
||||
print(f"ICH项目节点数: {project_count} (应该是268)")
|
||||
|
||||
# 检查关系完整性
|
||||
rel_sources = set(all_rels['source'].unique())
|
||||
node_ids = set(all_nodes['id'].unique())
|
||||
|
||||
missing_sources = rel_sources - node_ids
|
||||
if missing_sources:
|
||||
print(f"警告: {len(missing_sources)} 个关系的源节点不在节点文件中")
|
||||
else:
|
||||
print("所有关系的源节点都存在于节点文件中")
|
||||
|
||||
# 统计每个项目的关系数
|
||||
project_rels = all_rels[all_rels['source'].str.startswith('ICH-')].groupby('source').size()
|
||||
print(f"\n有关系的项目数: {len(project_rels)}/268")
|
||||
|
||||
# 展示几个示例项目的统计
|
||||
print("\n示例项目关系统计:")
|
||||
sample_projects = ['ICH-1', 'ICH-118', 'ICH-229']
|
||||
for pid in sample_projects:
|
||||
basic_rel_count = len(rels_basic[rels_basic['source'] == pid])
|
||||
deep_rel_count = len(rels_deep[rels_deep['source'] == pid])
|
||||
total_rel_count = len(all_rels[all_rels['source'] == pid])
|
||||
print(f" {pid}: 基础{basic_rel_count} + 深层{deep_rel_count} = 总计{total_rel_count} 条关系")
|
||||
|
||||
print("\n完成!")
|
||||
|
||||
return all_nodes, all_rels
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
merge_kg_data()
|
||||
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
将重新抽取的结果合并到现有的节点和关系文件中
|
||||
"""
|
||||
|
||||
import json
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# 添加模块路径
|
||||
sys.path.append(str(Path(__file__).parent / 'src'))
|
||||
|
||||
from data_processing.entity_normalizer import EntityNormalizer
|
||||
from data_processing.relationship_builder import RelationshipBuilder
|
||||
|
||||
def merge_retry_results():
|
||||
"""合并重新抽取的结果"""
|
||||
|
||||
# 文件路径
|
||||
retry_file = Path(__file__).parent / 'output' / 'retry_projects.json'
|
||||
nodes_file = Path(__file__).parent / 'output' / 'nodes_llm.csv'
|
||||
rels_file = Path(__file__).parent / 'output' / 'rels_llm.csv'
|
||||
ontology_file = Path(__file__).parent / 'config' / 'entity_ontology.yaml'
|
||||
|
||||
print("=== 加载数据 ===")
|
||||
|
||||
# 读取重新抽取的结果
|
||||
with open(retry_file, 'r', encoding='utf-8') as f:
|
||||
retry_data = json.load(f)
|
||||
|
||||
print(f"重新抽取的项目数: {len(retry_data)}")
|
||||
|
||||
# 读取现有的节点和关系
|
||||
existing_nodes_df = pd.read_csv(nodes_file, encoding='utf-8-sig')
|
||||
existing_rels_df = pd.read_csv(rels_file, encoding='utf-8-sig')
|
||||
|
||||
print(f"现有节点数: {len(existing_nodes_df)}")
|
||||
print(f"现有关系数: {len(existing_rels_df)}")
|
||||
|
||||
# 初始化规范化器和关系构建器
|
||||
normalizer = EntityNormalizer(str(ontology_file))
|
||||
builder = RelationshipBuilder(str(ontology_file))
|
||||
|
||||
# 规范化重新抽取的实体
|
||||
print("\n=== 规范化实体 ===")
|
||||
|
||||
# 收集所有需要规范化的实体
|
||||
all_entities_to_normalize = []
|
||||
for item in retry_data:
|
||||
result = item['extraction_result']
|
||||
entities = result.get('entities', [])
|
||||
# 添加项目ID以便跟踪
|
||||
for entity in entities:
|
||||
entity['_project_id'] = item['project_id']
|
||||
all_entities_to_normalize.extend(entities)
|
||||
|
||||
# 批量规范化
|
||||
extraction_results = []
|
||||
for item in retry_data:
|
||||
extraction_results.append(item['extraction_result'])
|
||||
|
||||
normalizer.normalize_batch(extraction_results)
|
||||
|
||||
# 生成新的实体节点
|
||||
new_entity_nodes = normalizer.get_entity_nodes()
|
||||
print(f"新生成实体节点数: {len(new_entity_nodes)}")
|
||||
|
||||
# 添加项目节点
|
||||
new_project_nodes = []
|
||||
for item in retry_data:
|
||||
new_project_nodes.append({
|
||||
'id': item['project_id'],
|
||||
'label': item['project_name'],
|
||||
'type': 'ICH_Project',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
print(f"新增项目节点数: {len(new_project_nodes)}")
|
||||
|
||||
# 构建关系
|
||||
print("\n=== 构建关系 ===")
|
||||
all_new_relationships = []
|
||||
|
||||
# 获取当前所有节点的ID到标签映射(用于查找目标实体ID)
|
||||
all_nodes_for_lookup = pd.concat([
|
||||
existing_nodes_df,
|
||||
pd.DataFrame(new_entity_nodes)
|
||||
], ignore_index=True)
|
||||
|
||||
# 创建名称->ID的映射字典
|
||||
name_to_id = {}
|
||||
for idx, row in all_nodes_for_lookup.iterrows():
|
||||
if row['type'] != 'ICH_Project': # 只映射实体节点
|
||||
name_to_id[(row['type'], row['label'])] = row['id']
|
||||
|
||||
for item in retry_data:
|
||||
project_id = item['project_id']
|
||||
result = item['extraction_result']
|
||||
|
||||
entities = result.get('entities', [])
|
||||
relationships = result.get('relationships', [])
|
||||
|
||||
# 构建实体名称到ID的映射(仅限当前项目)
|
||||
entity_name_to_id = {}
|
||||
for entity in entities:
|
||||
# 实体名称可能在name字段或attributes.name字段
|
||||
entity_name = entity.get('name') or entity.get('attributes', {}).get('name', '')
|
||||
entity_type = entity.get('type', '')
|
||||
normalized_name = normalizer.normalize_text(entity_name)
|
||||
|
||||
# 在规范化器的映射中查找(键是normalized_text,不是元组)
|
||||
entity_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
if not entity_id:
|
||||
# 在所有节点中查找(可能是已存在的实体)
|
||||
entity_id = name_to_id.get((entity_type, entity_name))
|
||||
|
||||
if entity_id:
|
||||
entity_name_to_id[entity_name] = entity_id
|
||||
|
||||
# 手动构建关系
|
||||
for rel in relationships:
|
||||
# 关系中的字段名是source_entity和target_entity
|
||||
source_name = rel.get('source_entity') or rel.get('source')
|
||||
target_name = rel.get('target_entity') or rel.get('target')
|
||||
rel_type = rel.get('type')
|
||||
rel_props = rel.get('properties', {})
|
||||
|
||||
# 查找源实体ID
|
||||
if source_name == project_id:
|
||||
source_id = project_id
|
||||
else:
|
||||
# 先尝试在当前项目实体中查找
|
||||
source_id = entity_name_to_id.get(source_name)
|
||||
if not source_id:
|
||||
# 在规范化器的映射中查找
|
||||
normalized_name = normalizer.normalize_text(source_name)
|
||||
source_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
|
||||
# 查找目标实体ID
|
||||
# 先尝试在当前项目实体中查找
|
||||
target_id = entity_name_to_id.get(target_name)
|
||||
if not target_id:
|
||||
# 在规范化器的映射中查找
|
||||
normalized_name = normalizer.normalize_text(target_name)
|
||||
target_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
|
||||
# 如果都找到了,添加关系
|
||||
if source_id and target_id:
|
||||
all_new_relationships.append({
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type,
|
||||
'properties': json.dumps(rel_props, ensure_ascii=False) if rel_props else '{}'
|
||||
})
|
||||
|
||||
print(f"项目 {project_id}: {len([r for r in all_new_relationships if r['source'] == project_id])} 条关系")
|
||||
|
||||
print(f"新增关系总数: {len(all_new_relationships)}")
|
||||
|
||||
# 合并节点
|
||||
print("\n=== 合并节点 ===")
|
||||
# 过滤掉已经存在的项目节点
|
||||
existing_project_ids = set(existing_nodes_df[existing_nodes_df['type'] == 'ICH_Project']['id'].tolist())
|
||||
new_project_nodes_filtered = [n for n in new_project_nodes if n['id'] not in existing_project_ids]
|
||||
|
||||
all_nodes = pd.concat([
|
||||
existing_nodes_df,
|
||||
pd.DataFrame(new_entity_nodes),
|
||||
pd.DataFrame(new_project_nodes_filtered)
|
||||
], ignore_index=True)
|
||||
|
||||
print(f"合并后节点数: {len(all_nodes)} (新增 {len(new_entity_nodes) + len(new_project_nodes_filtered)} 个)")
|
||||
|
||||
# 合并关系
|
||||
print("\n=== 合并关系 ===")
|
||||
all_rels = pd.concat([
|
||||
existing_rels_df,
|
||||
pd.DataFrame(all_new_relationships)
|
||||
], ignore_index=True)
|
||||
|
||||
print(f"合并后关系数: {len(all_rels)} (新增 {len(all_new_relationships)} 条)")
|
||||
|
||||
# 保存结果
|
||||
print("\n=== 保存结果 ===")
|
||||
all_nodes.to_csv(nodes_file, index=False, encoding='utf-8-sig')
|
||||
all_rels.to_csv(rels_file, index=False, encoding='utf-8-sig')
|
||||
|
||||
print(f"节点已保存: {nodes_file}")
|
||||
print(f"关系已保存: {rels_file}")
|
||||
|
||||
# 验证
|
||||
print("\n=== 验证 ===")
|
||||
for item in retry_data:
|
||||
project_id = item['project_id']
|
||||
rel_count = len(all_rels[all_rels['source'] == project_id])
|
||||
print(f"{project_id}: {rel_count} 条关系")
|
||||
|
||||
print("\n完成!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
merge_retry_results()
|
||||
@@ -0,0 +1,38 @@
|
||||
# 黑龙江省非物质文化遗产知识图谱构建 - 依赖包
|
||||
|
||||
# DeepSeek和LLM相关
|
||||
langchain-deepseek>=0.1.0
|
||||
langchain-core>=0.1.0
|
||||
openai>=1.0.0 # 备用
|
||||
|
||||
# Neo4j数据库
|
||||
neo4j>=5.15.0
|
||||
py2neo>=2021.2.4
|
||||
|
||||
# 数据处理
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
openpyxl>=3.1.0 # Excel读取
|
||||
|
||||
# NLP和文本处理
|
||||
jieba>=0.42.0
|
||||
# 可选:深度学习框架
|
||||
# torch>=2.0.0
|
||||
# transformers>=4.30.0
|
||||
|
||||
# Web框架(用于后续开发)
|
||||
# fastapi>=0.100.0
|
||||
# uvicorn>=0.23.0
|
||||
# flask>=3.0.0
|
||||
|
||||
# 可视化(用于后续开发)
|
||||
# matplotlib>=3.7.0
|
||||
# seaborn>=0.12.0
|
||||
|
||||
# 配置和日志
|
||||
pyyaml>=6.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# 其他工具
|
||||
tqdm>=4.65.0
|
||||
requests>=2.31.0
|
||||
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
重新抽取失败项目的脚本
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import yaml
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
# 添加模块路径
|
||||
sys.path.append(str(Path(__file__).parent / 'src'))
|
||||
|
||||
from knowledge_extraction.deep_entity_extractor import DeepEntityExtractor
|
||||
|
||||
async def retry_failed_projects():
|
||||
"""重新抽取失败的项目"""
|
||||
|
||||
# 加载配置
|
||||
config_file = Path(__file__).parent / 'config' / 'deep_extraction_config.yaml'
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# 读取原始数据
|
||||
data_file = Path(__file__).parent.parent.parent / 'data' / '黑龙江国家级和省级非遗名单.xlsx'
|
||||
df = pd.read_excel(data_file, engine='openpyxl')
|
||||
|
||||
# 找到失败的项目
|
||||
# 第0列是序号,需要拼接成ICH-xxx格式
|
||||
failed_data = df[df.iloc[:, 0].isin([118, 229])]
|
||||
|
||||
print(f"找到 {len(failed_data)} 个失败项目")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化抽取器
|
||||
extractor = DeepEntityExtractor(str(config_file))
|
||||
|
||||
results = []
|
||||
|
||||
for idx, row in failed_data.iterrows():
|
||||
project_num = int(row.iloc[0]) # 序号(118, 229)
|
||||
project_id = f'ICH-{project_num}' # 拼接成ICH-xxx格式
|
||||
project_name = row.iloc[3] # 项目名称(第4列)
|
||||
description = row.iloc[7] if len(row) > 7 else "" # 完整描述(第8列备注)
|
||||
|
||||
print(f"\n正在抽取: {project_id} - {project_name}")
|
||||
print(f"描述长度: {len(description)} 字符")
|
||||
|
||||
# 准备输入数据
|
||||
input_data = {
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'description': description
|
||||
}
|
||||
|
||||
# 抽取实体和关系
|
||||
try:
|
||||
result = await extractor.extract_from_remark(
|
||||
project_id=project_id,
|
||||
project_name=project_name,
|
||||
remark_text=description
|
||||
)
|
||||
|
||||
if result:
|
||||
results.append({
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'extraction_result': result
|
||||
})
|
||||
print(f"[OK] 抽取成功: {len(result.get('entities', []))} 个实体, {len(result.get('relationships', []))} 条关系")
|
||||
else:
|
||||
print(f"[FAIL] 抽取失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 抽取异常: {str(e)}")
|
||||
|
||||
# 保存结果
|
||||
output_file = Path(__file__).parent / 'output' / 'retry_projects.json'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n结果已保存到: {output_file}")
|
||||
print(f"成功: {len(results)}/{len(failed_data)}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(retry_failed_projects())
|
||||
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
重新抽取失败项目的脚本
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import yaml
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
# 添加模块路径
|
||||
sys.path.append(str(Path(__file__).parent / 'src'))
|
||||
|
||||
from knowledge_extraction.deep_entity_extractor import DeepEntityExtractor
|
||||
from data_processing.entity_normalizer import EntityNormalizer
|
||||
from data_processing.relationship_builder import RelationshipBuilder
|
||||
|
||||
async def retry_and_merge():
|
||||
"""重新抽取失败项目并直接合并到现有文件"""
|
||||
|
||||
# 加载配置
|
||||
config_file = Path(__file__).parent / 'config' / 'deep_extraction_config.yaml'
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# 读取原始数据
|
||||
data_file = Path(__file__).parent.parent.parent / 'data' / '黑龙江国家级和省级非遗名单.xlsx'
|
||||
df = pd.read_excel(data_file, engine='openpyxl')
|
||||
|
||||
# 找到失败的项目
|
||||
failed_data = df[df.iloc[:, 0].isin([118, 229])]
|
||||
|
||||
print(f"找到 {len(failed_data)} 个失败项目")
|
||||
print("=" * 60)
|
||||
|
||||
# 初始化组件
|
||||
extractor = DeepEntityExtractor(str(config_file))
|
||||
ontology_file = Path(__file__).parent / 'config' / 'entity_ontology.yaml'
|
||||
normalizer = EntityNormalizer(str(ontology_file))
|
||||
builder = RelationshipBuilder(str(ontology_file))
|
||||
|
||||
# 读取现有的实体注册表(如果存在)
|
||||
existing_nodes_file = Path(__file__).parent / 'output' / 'nodes_llm.csv'
|
||||
existing_rels_file = Path(__file__).parent / 'output' / 'rels_llm.csv'
|
||||
|
||||
existing_nodes_df = pd.read_csv(existing_nodes_file, encoding='utf-8-sig')
|
||||
existing_rels_df = pd.read_csv(existing_rels_file, encoding='utf-8-sig')
|
||||
|
||||
print(f"现有节点数: {len(existing_nodes_df)}")
|
||||
print(f"现有关系数: {len(existing_rels_df)}")
|
||||
|
||||
# 将现有实体加载到规范化器中
|
||||
print("\n=== 加载现有实体到规范化器 ===")
|
||||
for idx, row in existing_nodes_df.iterrows():
|
||||
if row['type'] != 'ICH_Project':
|
||||
# 将现有实体添加到规范化器的注册表
|
||||
entity_type = row['type']
|
||||
entity_text = row['label']
|
||||
entity_id = row['id']
|
||||
|
||||
# 规范化文本
|
||||
normalized_text = normalizer.normalize_text(entity_text)
|
||||
|
||||
# 添加到映射表
|
||||
if normalized_text not in normalizer.text_to_id_map:
|
||||
normalizer.text_to_id_map[normalized_text] = entity_id
|
||||
normalizer.entity_registry[entity_id] = {
|
||||
'type': entity_type,
|
||||
'text': entity_text,
|
||||
'normalized_text': normalized_text,
|
||||
'canonical_name': normalized_text # 添加这个字段
|
||||
}
|
||||
|
||||
print(f"已加载 {len(normalizer.entity_registry)} 个现有实体")
|
||||
|
||||
# 重新抽取失败的项目
|
||||
extraction_results = []
|
||||
|
||||
for idx, row in failed_data.iterrows():
|
||||
project_num = int(row.iloc[0])
|
||||
project_id = f'ICH-{project_num}'
|
||||
project_name = row.iloc[3]
|
||||
description = row.iloc[7] if len(row) > 7 else ""
|
||||
|
||||
print(f"\n正在抽取: {project_id} - {project_name}")
|
||||
print(f"描述长度: {len(description)} 字符")
|
||||
|
||||
try:
|
||||
result = await extractor.extract_from_remark(
|
||||
project_id=project_id,
|
||||
project_name=project_name,
|
||||
remark_text=description
|
||||
)
|
||||
|
||||
if result:
|
||||
extraction_results.append({
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'extraction_result': result
|
||||
})
|
||||
print(f"[OK] 抽取成功: {len(result.get('entities', []))} 个实体, {len(result.get('relationships', []))} 条关系")
|
||||
else:
|
||||
print(f"[FAIL] 抽取失败")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 抽取异常: {str(e)}")
|
||||
|
||||
if not extraction_results:
|
||||
print("\n没有成功抽取的项目")
|
||||
return
|
||||
|
||||
# 规范化新抽取的实体(会自动去重)
|
||||
print("\n=== 规范化新抽取的实体 ===")
|
||||
for item in extraction_results:
|
||||
result = item['extraction_result']
|
||||
entities = result.get('entities', [])
|
||||
|
||||
for entity in entities:
|
||||
# 实体名称可能在name字段或attributes.name字段
|
||||
entity_name = entity.get('name') or entity.get('attributes', {}).get('name', '')
|
||||
entity_type = entity.get('type', '')
|
||||
|
||||
# 规范化实体(会自动去重)
|
||||
normalizer.normalize_entity(entity, similarity_threshold=0.85)
|
||||
|
||||
# 获取所有实体节点(包括新增的)
|
||||
all_entity_nodes = normalizer.get_entity_nodes()
|
||||
print(f"规范化后实体节点数: {len(all_entity_nodes)}")
|
||||
|
||||
# 构建关系
|
||||
print("\n=== 构建关系 ===")
|
||||
all_new_relationships = []
|
||||
|
||||
for item in extraction_results:
|
||||
project_id = item['project_id']
|
||||
result = item['extraction_result']
|
||||
|
||||
entities = result.get('entities', [])
|
||||
relationships = result.get('relationships', [])
|
||||
|
||||
# 构建实体名称到ID的映射
|
||||
entity_name_to_id = {}
|
||||
for entity in entities:
|
||||
entity_name = entity.get('name') or entity.get('attributes', {}).get('name', '')
|
||||
entity_type = entity.get('type', '')
|
||||
normalized_name = normalizer.normalize_text(entity_name)
|
||||
entity_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
if entity_id:
|
||||
entity_name_to_id[entity_name] = entity_id
|
||||
|
||||
# 手动构建关系
|
||||
for rel in relationships:
|
||||
source_name = rel.get('source_entity') or rel.get('source')
|
||||
target_name = rel.get('target_entity') or rel.get('target')
|
||||
rel_type = rel.get('type')
|
||||
rel_props = rel.get('properties', {})
|
||||
|
||||
# 查找源实体ID
|
||||
if source_name == project_id:
|
||||
source_id = project_id
|
||||
else:
|
||||
normalized_name = normalizer.normalize_text(source_name)
|
||||
source_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
|
||||
# 查找目标实体ID
|
||||
normalized_name = normalizer.normalize_text(target_name)
|
||||
target_id = normalizer.text_to_id_map.get(normalized_name)
|
||||
|
||||
# 如果都找到了,添加关系
|
||||
if source_id and target_id:
|
||||
all_new_relationships.append({
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type,
|
||||
'properties': json.dumps(rel_props, ensure_ascii=False) if rel_props else '{}'
|
||||
})
|
||||
|
||||
print(f"项目 {project_id}: {len([r for r in all_new_relationships if r['source'] == project_id])} 条关系")
|
||||
|
||||
# 合并节点和关系
|
||||
print("\n=== 合并数据 ===")
|
||||
|
||||
# 项目节点:只添加缺失的项目
|
||||
existing_project_ids = set(existing_nodes_df[existing_nodes_df['type'] == 'ICH_Project']['id'].tolist())
|
||||
new_project_nodes = [
|
||||
{'id': item['project_id'], 'label': item['project_name'], 'type': 'ICH_Project', 'properties': '{}'}
|
||||
for item in extraction_results
|
||||
if item['project_id'] not in existing_project_ids
|
||||
]
|
||||
|
||||
# 合并所有节点
|
||||
all_nodes_df = pd.concat([
|
||||
existing_nodes_df[existing_nodes_df['type'] != 'ICH_Project'], # 现有实体节点
|
||||
pd.DataFrame(all_entity_nodes), # 所有实体节点(包括新增和去重后的现有)
|
||||
pd.DataFrame(new_project_nodes), # 新增项目节点
|
||||
existing_nodes_df[existing_nodes_df['type'] == 'ICH_Project'] # 现有项目节点
|
||||
], ignore_index=True)
|
||||
|
||||
# 去重节点(按ID)
|
||||
all_nodes_df = all_nodes_df.drop_duplicates(subset=['id'], keep='first')
|
||||
|
||||
# 合并关系
|
||||
all_rels_df = pd.concat([
|
||||
existing_rels_df,
|
||||
pd.DataFrame(all_new_relationships)
|
||||
], ignore_index=True)
|
||||
|
||||
# 去重关系
|
||||
all_rels_df = all_rels_df.drop_duplicates(subset=['source', 'target', 'type'], keep='first')
|
||||
|
||||
print(f"合并后节点数: {len(all_nodes_df)} (新增 {len(all_entity_nodes) - len(existing_nodes_df[existing_nodes_df['type'] != 'ICH_Project'])} 个实体)")
|
||||
print(f"合并后关系数: {len(all_rels_df)} (新增 {len(all_new_relationships)} 条)")
|
||||
|
||||
# 保存结果
|
||||
print("\n=== 保存结果 ===")
|
||||
all_nodes_df.to_csv(existing_nodes_file, index=False, encoding='utf-8-sig')
|
||||
all_rels_df.to_csv(existing_rels_file, index=False, encoding='utf-8-sig')
|
||||
|
||||
print(f"节点已保存: {existing_nodes_file}")
|
||||
print(f"关系已保存: {existing_rels_file}")
|
||||
|
||||
# 验证
|
||||
print("\n=== 验证 ===")
|
||||
for item in extraction_results:
|
||||
project_id = item['project_id']
|
||||
rel_count = len(all_rels_df[all_rels_df['source'] == project_id])
|
||||
print(f"{project_id}: {rel_count} 条关系")
|
||||
|
||||
print("\n完成!")
|
||||
|
||||
# 保存抽取结果以供检查
|
||||
output_file = Path(__file__).parent / 'output' / 'retry_projects.json'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(extraction_results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(retry_and_merge())
|
||||
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
数据预处理脚本 - 读取黑龙江非遗Excel数据
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
import logging
|
||||
|
||||
class ExcelDataReader:
|
||||
"""Excel数据读取器"""
|
||||
|
||||
def __init__(self, excel_path: str):
|
||||
"""
|
||||
初始化数据读取器
|
||||
|
||||
Args:
|
||||
excel_path: Excel文件路径
|
||||
"""
|
||||
self.excel_path = Path(excel_path)
|
||||
self.data = None
|
||||
self.logger = self._setup_logger()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""设置日志"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
def read_excel(self) -> pd.DataFrame:
|
||||
"""
|
||||
读取Excel文件
|
||||
|
||||
Returns:
|
||||
DataFrame: 数据框
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"开始读取Excel文件: {self.excel_path}")
|
||||
|
||||
# 读取Excel文件
|
||||
self.data = pd.read_excel(self.excel_path)
|
||||
|
||||
self.logger.info(f"成功读取 {len(self.data)} 行数据")
|
||||
self.logger.info(f"列名: {list(self.data.columns)}")
|
||||
|
||||
# 显示前5行
|
||||
self.logger.info("\n前5行数据:")
|
||||
self.logger.info(self.data.head())
|
||||
|
||||
return self.data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"读取Excel文件失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def analyze_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
分析数据
|
||||
|
||||
Returns:
|
||||
Dict: 分析结果
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("请先读取Excel文件")
|
||||
|
||||
analysis = {
|
||||
"total_records": len(self.data),
|
||||
"columns": list(self.data.columns),
|
||||
"column_types": {col: str(dtype) for col, dtype in self.data.dtypes.items()},
|
||||
"missing_values": self.data.isnull().sum().to_dict(),
|
||||
"statistics": {}
|
||||
}
|
||||
|
||||
# 分析类别分布
|
||||
if '类别' in self.data.columns:
|
||||
category_counts = self.data['类别'].value_counts()
|
||||
analysis['category_distribution'] = category_counts.to_dict()
|
||||
|
||||
# 分析级别分布
|
||||
if '项目级别' in self.data.columns:
|
||||
level_counts = self.data['项目级别'].value_counts()
|
||||
analysis['level_distribution'] = level_counts.to_dict()
|
||||
|
||||
# 分析地域分布
|
||||
if '项目申报单位/地区' in self.data.columns:
|
||||
location_counts = self.data['项目申报单位/地区'].value_counts()
|
||||
analysis['location_distribution'] = location_counts.head(20).to_dict()
|
||||
|
||||
# 传承人覆盖率
|
||||
if '代表性传承人' in self.data.columns:
|
||||
has_inheritor = self.data['代表性传承人'].notna().sum()
|
||||
analysis['inheritor_coverage'] = {
|
||||
"total": len(self.data),
|
||||
"has_inheritor": int(has_inheritor),
|
||||
"coverage_rate": float(has_inheritor / len(self.data) * 100)
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
def clean_data(self) -> pd.DataFrame:
|
||||
"""
|
||||
清洗数据
|
||||
|
||||
Returns:
|
||||
DataFrame: 清洗后的数据
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("请先读取Excel文件")
|
||||
|
||||
self.logger.info("开始清洗数据")
|
||||
|
||||
# 去除空行
|
||||
original_len = len(self.data)
|
||||
self.data = self.data.dropna(how='all')
|
||||
self.logger.info(f"去除空行: {original_len} -> {len(self.data)}")
|
||||
|
||||
# 填充缺失值
|
||||
for col in self.data.columns:
|
||||
if self.data[col].dtype == 'object':
|
||||
self.data[col] = self.data[col].fillna('')
|
||||
else:
|
||||
self.data[col] = self.data[col].fillna(0)
|
||||
|
||||
# 去重(基于项目名称、项目批次、项目保护单位、代表性传承人四个字段)
|
||||
dedup_cols = ['项目名称', '项目批次', '项目保护单位', '代表性传承人']
|
||||
existing_cols = [col for col in dedup_cols if col in self.data.columns]
|
||||
|
||||
if existing_cols:
|
||||
before_dedup = len(self.data)
|
||||
self.data = self.data.drop_duplicates(subset=existing_cols, keep='first')
|
||||
duplicate_count = before_dedup - len(self.data)
|
||||
dedup_rate = (duplicate_count / before_dedup * 100) if before_dedup > 0 else 0
|
||||
self.logger.info(f"去重(基于{len(existing_cols)}个字段: {', '.join(existing_cols)}): {before_dedup} -> {len(self.data)} (删除{duplicate_count}条,去重率{dedup_rate:.1f}%)")
|
||||
|
||||
return self.data
|
||||
|
||||
def convert_to_kg_format(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
转换为知识图谱格式
|
||||
|
||||
Returns:
|
||||
List[Dict]: 知识图谱节点列表
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("请先读取Excel文件")
|
||||
|
||||
self.logger.info("转换为知识图谱格式")
|
||||
|
||||
kg_nodes = []
|
||||
|
||||
for idx, row in self.data.iterrows():
|
||||
try:
|
||||
# 创建非遗项目节点
|
||||
project_node = {
|
||||
"id": f"ICH-{idx:04d}",
|
||||
"type": "ICH_Project",
|
||||
"properties": {
|
||||
"project_id": f"ICH-{idx:04d}",
|
||||
"name": str(row.get('项目名称', '')),
|
||||
"level": str(row.get('项目级别', '')),
|
||||
"batch": str(row.get('批次号', '')),
|
||||
"category": str(row.get('类别', '')),
|
||||
"declaration_area": str(row.get('项目申报单位/地区', '')),
|
||||
"description": str(row.get('备注', '')),
|
||||
"source_row": idx + 2 # Excel行号(从2开始,第一行是表头)
|
||||
}
|
||||
}
|
||||
|
||||
kg_nodes.append(project_node)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"转换第{idx}行数据失败: {str(e)}")
|
||||
continue
|
||||
|
||||
self.logger.info(f"成功转换 {len(kg_nodes)} 个节点")
|
||||
return kg_nodes
|
||||
|
||||
def extract_inheritors(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
提取传承人信息
|
||||
|
||||
Returns:
|
||||
List[Dict]: 传承人节点列表
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("请先读取Excel文件")
|
||||
|
||||
self.logger.info("提取传承人信息")
|
||||
|
||||
inheritors = []
|
||||
inheritor_id = 0
|
||||
|
||||
for idx, row in self.data.iterrows():
|
||||
inheritor_names = row.get('代表性传承人', '')
|
||||
if pd.isna(inheritor_names) or not str(inheritor_names).strip():
|
||||
continue
|
||||
|
||||
# 处理多个传承人(用顿号分隔)
|
||||
names = str(inheritor_names).replace('、', ',').replace(',', ',').split(',')
|
||||
|
||||
for name in names:
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
inheritor_id += 1
|
||||
inheritor_node = {
|
||||
"id": f"INH-{inheritor_id:04d}",
|
||||
"type": "Inheritor",
|
||||
"properties": {
|
||||
"inheritor_id": f"INH-{inheritor_id:04d}",
|
||||
"name": name,
|
||||
"project_name": str(row.get('项目名称', '')),
|
||||
"source_row": idx + 2
|
||||
}
|
||||
}
|
||||
|
||||
inheritors.append(inheritor_node)
|
||||
|
||||
self.logger.info(f"提取了 {len(inheritors)} 个传承人")
|
||||
return inheritors
|
||||
|
||||
def extract_relations(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
提取关系
|
||||
|
||||
Returns:
|
||||
List[Dict]: 关系列表
|
||||
"""
|
||||
if self.data is None:
|
||||
raise ValueError("请先读取Excel文件")
|
||||
|
||||
self.logger.info("提取关系")
|
||||
|
||||
relations = []
|
||||
relation_id = 0
|
||||
|
||||
for idx, row in self.data.iterrows():
|
||||
project_id = f"ICH-{idx:04d}"
|
||||
|
||||
# 项目-类别关系
|
||||
category = row.get('类别', '')
|
||||
if pd.notna(category) and str(category).strip():
|
||||
relation_id += 1
|
||||
relations.append({
|
||||
"id": f"REL-{relation_id:04d}",
|
||||
"type": "belongs_to",
|
||||
"from": project_id,
|
||||
"to": f"CAT-{str(category)}",
|
||||
"properties": {}
|
||||
})
|
||||
|
||||
# 项目-传承人关系
|
||||
inheritor_names = row.get('代表性传承人', '')
|
||||
if pd.notna(inheritor_names) and str(inheritor_names).strip():
|
||||
names = str(inheritor_names).replace('、', ',').replace(',', ',').split(',')
|
||||
for name in names:
|
||||
name = name.strip()
|
||||
if name:
|
||||
relation_id += 1
|
||||
relations.append({
|
||||
"id": f"REL-{relation_id:04d}",
|
||||
"type": "has_inheritor",
|
||||
"from": project_id,
|
||||
"to": f"INH-{name}", # 简化处理
|
||||
"properties": {}
|
||||
})
|
||||
|
||||
self.logger.info(f"提取了 {len(relations)} 个关系")
|
||||
return relations
|
||||
|
||||
def save_analysis_report(self, output_path: str):
|
||||
"""
|
||||
保存分析报告
|
||||
|
||||
Args:
|
||||
output_path: 输出路径
|
||||
"""
|
||||
analysis = self.analyze_data()
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(analysis, f, indent=2, ensure_ascii=False)
|
||||
|
||||
self.logger.info(f"分析报告已保存到: {output_path}")
|
||||
|
||||
def save_kg_data(self, nodes: List[Dict], relations: List[Dict], output_dir: str):
|
||||
"""
|
||||
保存知识图谱数据
|
||||
|
||||
Args:
|
||||
nodes: 节点列表
|
||||
relations: 关系列表
|
||||
output_dir: 输出目录
|
||||
"""
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存节点
|
||||
nodes_file = output_path / "nodes.json"
|
||||
with open(nodes_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(nodes, f, indent=2, ensure_ascii=False)
|
||||
self.logger.info(f"节点已保存到: {nodes_file}")
|
||||
|
||||
# 保存关系
|
||||
relations_file = output_path / "relations.json"
|
||||
with open(relations_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(relations, f, indent=2, ensure_ascii=False)
|
||||
self.logger.info(f"关系已保存到: {relations_file}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 配置
|
||||
excel_path = r"E:\Project\2026_KG_ICH\data\黑龙江国家级和省级非遗名单.xlsx"
|
||||
output_dir = r"E:\Project\2026_KG_ICH\dofile\kg_project\output"
|
||||
|
||||
# 创建读取器
|
||||
reader = ExcelDataReader(excel_path)
|
||||
|
||||
# 读取数据
|
||||
reader.read_excel()
|
||||
|
||||
# 分析数据
|
||||
print("\n" + "="*60)
|
||||
print("数据分析")
|
||||
print("="*60)
|
||||
analysis = reader.analyze_data()
|
||||
print(f"\n总记录数: {analysis['total_records']}")
|
||||
print(f"\n类别分布:")
|
||||
for cat, count in analysis.get('category_distribution', {}).items():
|
||||
print(f" {cat}: {count}")
|
||||
|
||||
print(f"\n传承人覆盖率: {analysis.get('inheritor_coverage', {}).get('coverage_rate', 0):.1f}%")
|
||||
|
||||
# 保存分析报告
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
reader.save_analysis_report(f"{output_dir}/data_analysis.json")
|
||||
|
||||
# 清洗数据
|
||||
print("\n" + "="*60)
|
||||
print("数据清洗")
|
||||
print("="*60)
|
||||
reader.clean_data()
|
||||
|
||||
# 转换为知识图谱格式
|
||||
print("\n" + "="*60)
|
||||
print("转换为知识图谱格式")
|
||||
print("="*60)
|
||||
|
||||
nodes = reader.convert_to_kg_format()
|
||||
inheritors = reader.extract_inheritors()
|
||||
relations = reader.extract_relations()
|
||||
|
||||
# 合并所有节点
|
||||
all_nodes = nodes + inheritors
|
||||
|
||||
print(f"\n节点统计:")
|
||||
print(f" 非遗项目节点: {len(nodes)}")
|
||||
print(f" 传承人节点: {len(inheritors)}")
|
||||
print(f" 总节点数: {len(all_nodes)}")
|
||||
print(f" 关系数: {len(relations)}")
|
||||
|
||||
# 保存知识图谱数据
|
||||
reader.save_kg_data(all_nodes, relations, output_dir)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("处理完成!")
|
||||
print("="*60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,438 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
从Excel提取数据生成知识图谱CSV文件
|
||||
保持原始数据表述不变
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def extract_batches(df):
|
||||
"""提取所有唯一的批次(保持原始表述)"""
|
||||
batches = {}
|
||||
batch_counter = {}
|
||||
|
||||
for batch_str in df['项目批次'].dropna().unique():
|
||||
if not batch_str or str(batch_str).strip() == '':
|
||||
continue
|
||||
|
||||
# 解析批次字符串(可能包含多个批次)
|
||||
batch_items = re.split(r'[,、,]', str(batch_str))
|
||||
|
||||
for item in batch_items:
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
|
||||
# 生成批次ID(使用原始表述的哈希)
|
||||
if item not in batch_counter:
|
||||
batch_counter[item] = 1
|
||||
else:
|
||||
batch_counter[item] += 1
|
||||
|
||||
batch_id = f"BATCH-{abs(hash(item)) % 100000:05d}"
|
||||
|
||||
if batch_id not in batches:
|
||||
batches[batch_id] = {
|
||||
'name': item, # 保持原始表述
|
||||
'original_string': item
|
||||
}
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def parse_inheritor_field(inheritor_str):
|
||||
"""解析传承人字段,保持原始表述(如"吴明新(国)")"""
|
||||
if not inheritor_str or str(inheritor_str).strip() in ['无', '']:
|
||||
return []
|
||||
|
||||
# 按顿号、逗号分割,保持原始表述
|
||||
inheritors = re.split(r'[、,,\n]', str(inheritor_str))
|
||||
inheritors = [inh.strip() for inh in inheritors if inh.strip() and inh.strip() != '无']
|
||||
return inheritors
|
||||
|
||||
|
||||
def extract_inheritors(df):
|
||||
"""提取所有唯一传承人(保持原始表述)"""
|
||||
inheritors = {}
|
||||
inheritor_counter = {}
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
inheritor_str = row.get('代表性传承人', '')
|
||||
if not inheritor_str or str(inheritor_str).strip() in ['无', '']:
|
||||
continue
|
||||
|
||||
# 解析传承人列表
|
||||
inheritor_names = parse_inheritor_field(inheritor_str)
|
||||
|
||||
for name in inheritor_names:
|
||||
# 使用原始名称(包括括号)作为key
|
||||
if name not in inheritor_counter:
|
||||
inheritor_counter[name] = 1
|
||||
else:
|
||||
inheritor_counter[name] += 1
|
||||
|
||||
# 生成传承人ID
|
||||
inheritor_id = f"INH-{abs(hash(name)) % 100000:05d}-{inheritor_counter[name]}"
|
||||
|
||||
if inheritor_id not in inheritors:
|
||||
inheritors[inheritor_id] = {
|
||||
'name': name # 保持原始表述,如"吴明新(国)"
|
||||
}
|
||||
|
||||
return inheritors
|
||||
|
||||
|
||||
def extract_institutions(df):
|
||||
"""提取所有唯一保护机构(保持原始表述)"""
|
||||
institutions = {}
|
||||
inst_counter = {}
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
inst_str = row.get('项目保护单位', '')
|
||||
if not inst_str or str(inst_str).strip() in ['', '无']:
|
||||
continue
|
||||
|
||||
inst_name = str(inst_str).strip()
|
||||
|
||||
# 使用原始机构名
|
||||
if inst_name not in inst_counter:
|
||||
inst_counter[inst_name] = 1
|
||||
else:
|
||||
inst_counter[inst_name] += 1
|
||||
|
||||
# 生成机构ID
|
||||
inst_id = f"INST-{abs(hash(inst_name)) % 100000:05d}-{inst_counter[inst_name]}"
|
||||
|
||||
if inst_id not in institutions:
|
||||
institutions[inst_id] = {
|
||||
'name': inst_name # 保持原始表述
|
||||
}
|
||||
|
||||
return institutions
|
||||
|
||||
|
||||
def parse_batch_field(batch_str, batches_dict):
|
||||
"""解析批次字段,返回批次ID列表"""
|
||||
if not batch_str or str(batch_str).strip() == '':
|
||||
return []
|
||||
|
||||
batches = re.split(r'[,、,]', str(batch_str))
|
||||
batch_ids = []
|
||||
|
||||
for batch in batches:
|
||||
batch = batch.strip()
|
||||
if batch:
|
||||
# 查找对应的批次ID
|
||||
for batch_id, batch_info in batches_dict.items():
|
||||
if batch_info['name'] == batch:
|
||||
batch_ids.append(batch_id)
|
||||
break
|
||||
|
||||
return batch_ids
|
||||
|
||||
|
||||
def get_institution_id(institution_name, institutions_dict):
|
||||
"""获取机构ID"""
|
||||
if not institution_name or str(institution_name).strip() in ['', '无']:
|
||||
return None
|
||||
|
||||
# 从已提取的机构中查找
|
||||
for inst_id, inst_info in institutions_dict.items():
|
||||
if inst_info['name'] == str(institution_name).strip():
|
||||
return inst_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_inheritor_id(inheritor_name, inheritors_dict):
|
||||
"""获取传承人ID"""
|
||||
if not inheritor_name or not inheritor_name.strip():
|
||||
return None
|
||||
|
||||
# 从已提取的传承人中查找
|
||||
for inheritor_id, inheritor_info in inheritors_dict.items():
|
||||
if inheritor_info['name'] == inheritor_name:
|
||||
return inheritor_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def clean_data(df):
|
||||
"""数据清洗"""
|
||||
# 去除完全空白的行
|
||||
df = df.dropna(how='all')
|
||||
|
||||
# 填充缺失值为空字符串
|
||||
for col in df.columns:
|
||||
if df[col].dtype == 'object':
|
||||
df[col] = df[col].fillna('')
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def build_nodes(df):
|
||||
"""构建节点数据(保持原始表述)"""
|
||||
nodes_list = []
|
||||
|
||||
print("正在构建节点...")
|
||||
|
||||
# 1. ICH_Project节点
|
||||
print(" - 构建ICH_Project节点...")
|
||||
for idx, row in df.iterrows():
|
||||
seq_num = row.get('总序号', idx + 1)
|
||||
project_name = row.get('项目名称', '')
|
||||
|
||||
# 确定级别(从批次字段推断)
|
||||
batch_str = str(row.get('项目批次', ''))
|
||||
level = '国家级' if '国家级' in batch_str else ('省级' if '省级' in batch_str else '')
|
||||
|
||||
nodes_list.append({
|
||||
'id': f"ICH-{int(seq_num)}",
|
||||
'label': str(project_name),
|
||||
'type': 'ICH_Project',
|
||||
'properties': json.dumps({
|
||||
'level': level,
|
||||
'category': row.get('类别', ''), # 保持原始表述,如"曲艺类"
|
||||
'batch': batch_str,
|
||||
'protection_unit': row.get('项目保护单位', '')
|
||||
}, ensure_ascii=False)
|
||||
})
|
||||
|
||||
# 2. Category节点(从数据中动态提取,保持原始表述)
|
||||
print(" - 构建Category节点...")
|
||||
unique_categories = df['类别'].dropna().unique()
|
||||
|
||||
for cat_name in unique_categories:
|
||||
# 使用类别名称作为ID(使用哈希避免特殊字符)
|
||||
cat_id = f"CAT-{abs(hash(cat_name)) % 100000:05d}"
|
||||
|
||||
nodes_list.append({
|
||||
'id': cat_id,
|
||||
'label': cat_name, # 保持原始表述,如"曲艺类"
|
||||
'type': 'Category',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
# 3. Batch节点(动态识别,保持原始表述)
|
||||
print(" - 构建Batch节点...")
|
||||
batches = extract_batches(df)
|
||||
for batch_id, batch_info in batches.items():
|
||||
nodes_list.append({
|
||||
'id': batch_id,
|
||||
'label': batch_info['name'], # 保持原始表述,如"国家级第1批"
|
||||
'type': 'Batch',
|
||||
'properties': json.dumps({
|
||||
'original_string': batch_info['original_string']
|
||||
}, ensure_ascii=False)
|
||||
})
|
||||
|
||||
# 4. Inheritor节点(动态识别,保持原始表述)
|
||||
print(" - 构建Inheritor节点...")
|
||||
inheritors = extract_inheritors(df)
|
||||
for inheritor_id, inheritor_info in inheritors.items():
|
||||
nodes_list.append({
|
||||
'id': inheritor_id,
|
||||
'label': inheritor_info['name'], # 保持原始表述,如"吴明新(国)"
|
||||
'type': 'Inheritor',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
# 5. Institution节点(动态识别,保持原始表述)
|
||||
print(" - 构建Institution节点...")
|
||||
institutions = extract_institutions(df)
|
||||
for inst_id, inst_info in institutions.items():
|
||||
nodes_list.append({
|
||||
'id': inst_id,
|
||||
'label': inst_info['name'], # 保持原始表述
|
||||
'type': 'Institution',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
return nodes_list, batches, inheritors, institutions
|
||||
|
||||
|
||||
def build_relations(df, batches, inheritors, institutions):
|
||||
"""构建关系数据"""
|
||||
relations_list = []
|
||||
|
||||
print("正在构建关系...")
|
||||
|
||||
# 建立名称到ID的快速查找映射
|
||||
category_map = {}
|
||||
for cat_name in df['类别'].dropna().unique():
|
||||
cat_id = f"CAT-{abs(hash(cat_name)) % 100000:05d}"
|
||||
category_map[cat_name] = cat_id
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
seq_num = row.get('总序号', idx + 1)
|
||||
project_id = f"ICH-{int(seq_num)}"
|
||||
|
||||
# 1. 项目 → 类别
|
||||
category_name = row.get('类别', '')
|
||||
if category_name and category_name in category_map:
|
||||
category_id = category_map[category_name]
|
||||
relations_list.append({
|
||||
'source': project_id,
|
||||
'target': category_id,
|
||||
'type': 'BELONGS_TO',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
# 2. 项目 → 批次(支持多个批次)
|
||||
batch_str = row.get('项目批次', '')
|
||||
if batch_str:
|
||||
batch_ids = parse_batch_field(batch_str, batches)
|
||||
for batch_id in batch_ids:
|
||||
relations_list.append({
|
||||
'source': project_id,
|
||||
'target': batch_id,
|
||||
'type': 'SELECTED_IN_BATCH',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
# 3. 项目 → 保护机构
|
||||
institution_name = row.get('项目保护单位', '')
|
||||
if institution_name and str(institution_name).strip() not in ['', '无']:
|
||||
institution_id = get_institution_id(institution_name, institutions)
|
||||
if institution_id:
|
||||
relations_list.append({
|
||||
'source': project_id,
|
||||
'target': institution_id,
|
||||
'type': 'PROTECTED_BY',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
# 4. 项目 → 传承人(支持多个传承人)
|
||||
inheritor_str = row.get('代表性传承人', '')
|
||||
if inheritor_str:
|
||||
inheritor_names = parse_inheritor_field(inheritor_str)
|
||||
for name in inheritor_names:
|
||||
inheritor_id = get_inheritor_id(name, inheritors)
|
||||
if inheritor_id:
|
||||
relations_list.append({
|
||||
'source': project_id,
|
||||
'target': inheritor_id,
|
||||
'type': 'HAS_INHERITOR',
|
||||
'properties': '{}'
|
||||
})
|
||||
|
||||
return relations_list
|
||||
|
||||
|
||||
def generate_report(nodes_df, relations_df, output_dir):
|
||||
"""生成统计报告"""
|
||||
report_lines = []
|
||||
report_lines.append("# 知识图谱CSV提取报告\n")
|
||||
report_lines.append(f"生成时间: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
report_lines.append("---\n\n")
|
||||
|
||||
# 节点统计
|
||||
report_lines.append("## 节点统计\n\n")
|
||||
report_lines.append(f"**节点总数**: {len(nodes_df)}\n\n")
|
||||
|
||||
node_type_counts = nodes_df['type'].value_counts().sort_index()
|
||||
report_lines.append("| 节点类型 | 数量 | 占比 |\n")
|
||||
report_lines.append("|---------|------|------|\n")
|
||||
for node_type, count in node_type_counts.items():
|
||||
percentage = (count / len(nodes_df) * 100)
|
||||
report_lines.append(f"| {node_type} | {count} | {percentage:.1f}% |\n")
|
||||
|
||||
# 关系统计
|
||||
report_lines.append("\n## 关系统计\n\n")
|
||||
report_lines.append(f"**关系总数**: {len(relations_df)}\n\n")
|
||||
|
||||
rel_type_counts = relations_df['type'].value_counts().sort_index()
|
||||
report_lines.append("| 关系类型 | 数量 | 占比 |\n")
|
||||
report_lines.append("|---------|------|------|\n")
|
||||
for rel_type, count in rel_type_counts.items():
|
||||
percentage = (count / len(relations_df) * 100)
|
||||
report_lines.append(f"| {rel_type} | {count} | {percentage:.1f}% |\n")
|
||||
|
||||
# 保存报告
|
||||
report_path = output_dir / 'extraction_report.md'
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(report_lines)
|
||||
|
||||
print(f"\n报告已生成: {report_path}")
|
||||
|
||||
# 打印统计信息
|
||||
print("\n" + "="*50)
|
||||
print("数据提取完成")
|
||||
print("="*50)
|
||||
print(f"\n节点总数: {len(nodes_df)}")
|
||||
for node_type, count in node_type_counts.items():
|
||||
print(f" - {node_type}: {count}")
|
||||
print(f"\n关系总数: {len(relations_df)}")
|
||||
for rel_type, count in rel_type_counts.items():
|
||||
print(f" - {rel_type}: {count}")
|
||||
print("="*50)
|
||||
|
||||
|
||||
def extract_data_from_excel():
|
||||
"""从Excel提取数据并生成知识图谱CSV文件"""
|
||||
|
||||
print("开始从Excel提取数据...")
|
||||
|
||||
# 1. 读取Excel
|
||||
excel_path = r'E:\Project\2026_KG_ICH\data\黑龙江国家级和省级非遗名单.xlsx'
|
||||
print(f"读取文件: {excel_path}")
|
||||
|
||||
df = pd.read_excel(excel_path)
|
||||
print(f"原始数据: {len(df)} 行 x {len(df.columns)} 列")
|
||||
|
||||
# 2. 提取所需列
|
||||
columns = ['总序号', '类别', '项目名称', '项目批次', '项目保护单位', '代表性传承人']
|
||||
print(f"\n提取列: {', '.join(columns)}")
|
||||
|
||||
# 检查列是否存在
|
||||
available_cols = [col for col in columns if col in df.columns]
|
||||
if len(available_cols) < len(columns):
|
||||
missing = set(columns) - set(available_cols)
|
||||
print(f"警告: 以下列不存在: {missing}")
|
||||
|
||||
df = df[available_cols]
|
||||
print(f"提取后数据: {len(df)} 行 x {len(df.columns)} 列")
|
||||
|
||||
# 3. 数据清洗
|
||||
print("\n数据清洗...")
|
||||
df = clean_data(df)
|
||||
print(f"清洗后数据: {len(df)} 行")
|
||||
|
||||
# 4. 构建节点
|
||||
nodes_list, batches, inheritors, institutions = build_nodes(df)
|
||||
nodes_df = pd.DataFrame(nodes_list)
|
||||
print(f"节点数据: {len(nodes_df)} 行")
|
||||
|
||||
# 5. 构建关系
|
||||
relations_list = build_relations(df, batches, inheritors, institutions)
|
||||
relations_df = pd.DataFrame(relations_list)
|
||||
print(f"关系数据: {len(relations_df)} 行")
|
||||
|
||||
# 6. 保存CSV
|
||||
output_dir = Path(r'E:\Project\2026_KG_ICH\dofile\kg_project\output')
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\n保存文件到: {output_dir}")
|
||||
|
||||
# 保存为UTF-8-BOM编码(Excel友好)
|
||||
nodes_path = output_dir / 'nodes.csv'
|
||||
relations_path = output_dir / 'rels.csv'
|
||||
|
||||
nodes_df.to_csv(nodes_path, index=False, encoding='utf-8-sig')
|
||||
relations_df.to_csv(relations_path, index=False, encoding='utf-8-sig')
|
||||
|
||||
print(f" - nodes.csv: {nodes_path}")
|
||||
print(f" - rels.csv: {relations_path}")
|
||||
|
||||
# 7. 生成统计报告
|
||||
generate_report(nodes_df, relations_df, output_dir)
|
||||
|
||||
return nodes_df, relations_df
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract_data_from_excel()
|
||||
@@ -0,0 +1,382 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱可视化工具
|
||||
使用networkx和matplotlib绘制知识图谱
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib import font_manager
|
||||
import matplotlib.patches as mpatches
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
|
||||
# 设置中文字体
|
||||
def setup_chinese_font():
|
||||
"""设置中文字体"""
|
||||
# 尝试多种中文字体
|
||||
chinese_fonts = [
|
||||
'Microsoft YaHei',
|
||||
'SimHei',
|
||||
'SimSun',
|
||||
'KaiTi',
|
||||
'FangSong',
|
||||
'STXihei',
|
||||
'STSong',
|
||||
'STKaiti',
|
||||
'STFangsong'
|
||||
]
|
||||
|
||||
for font in chinese_fonts:
|
||||
try:
|
||||
plt.rcParams['font.sans-serif'] = [font]
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
print(f"使用字体: {plt.rcParams['font.sans-serif'][0]}")
|
||||
|
||||
|
||||
# 节点类型颜色映射
|
||||
NODE_TYPE_COLORS = {
|
||||
'ICH_Project': '#FF6B6B', # 红色 - 项目
|
||||
'Category': '#4ECDC4', # 青色 - 类别
|
||||
'Batch': '#95E1D3', # 绿色 - 批次
|
||||
'Inheritor': '#FFD93D', # 黄色 - 传承人
|
||||
'Institution': '#6C5CE7' # 紫色 - 机构
|
||||
}
|
||||
|
||||
# 节点类型大小映射
|
||||
NODE_TYPE_SIZES = {
|
||||
'ICH_Project': 300,
|
||||
'Category': 500,
|
||||
'Batch': 350,
|
||||
'Inheritor': 200,
|
||||
'Institution': 250
|
||||
}
|
||||
|
||||
|
||||
def load_graph_data(nodes_csv, rels_csv):
|
||||
"""加载图谱数据"""
|
||||
print("正在加载数据...")
|
||||
|
||||
# 读取节点和关系
|
||||
nodes_df = pd.read_csv(nodes_csv, encoding='utf-8-sig')
|
||||
rels_df = pd.read_csv(rels_csv, encoding='utf-8-sig')
|
||||
|
||||
print(f" - 节点: {len(nodes_df)}")
|
||||
print(f" - 关系: {len(rels_df)}")
|
||||
|
||||
return nodes_df, rels_df
|
||||
|
||||
|
||||
def build_networkx_graph(nodes_df, rels_df):
|
||||
"""构建NetworkX图"""
|
||||
print("正在构建图...")
|
||||
|
||||
G = nx.DiGraph() # 有向图
|
||||
|
||||
# 添加节点
|
||||
for idx, row in nodes_df.iterrows():
|
||||
node_id = row['id']
|
||||
label = row['label']
|
||||
node_type = row['type']
|
||||
|
||||
# 截断过长的标签
|
||||
if len(label) > 10:
|
||||
display_label = label[:10] + '...'
|
||||
else:
|
||||
display_label = label
|
||||
|
||||
G.add_node(
|
||||
node_id,
|
||||
label=display_label,
|
||||
full_label=label,
|
||||
node_type=node_type,
|
||||
color=NODE_TYPE_COLORS.get(node_type, '#CCCCCC'),
|
||||
size=NODE_TYPE_SIZES.get(node_type, 200)
|
||||
)
|
||||
|
||||
# 添加边
|
||||
for idx, row in rels_df.iterrows():
|
||||
source = row['source']
|
||||
target = row['target']
|
||||
rel_type = row['type']
|
||||
|
||||
if source in G.nodes() and target in G.nodes():
|
||||
G.add_edge(source, target, rel_type=rel_type)
|
||||
|
||||
print(f" - 节点数: {G.number_of_nodes()}")
|
||||
print(f" - 边数: {G.number_of_edges()}")
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def filter_graph_by_type(G, include_types=None):
|
||||
"""按节点类型过滤图"""
|
||||
if include_types is None:
|
||||
return G
|
||||
|
||||
nodes_to_keep = [n for n, d in G.nodes(data=True)
|
||||
if d.get('node_type') in include_types]
|
||||
|
||||
return G.subgraph(nodes_to_keep).copy()
|
||||
|
||||
|
||||
def draw_graph(G, output_path, title="知识图谱", layout='spring'):
|
||||
"""绘制知识图谱"""
|
||||
print(f"正在绘制图谱: {title}")
|
||||
|
||||
plt.figure(figsize=(20, 16))
|
||||
|
||||
# 选择布局算法
|
||||
if layout == 'spring':
|
||||
pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
|
||||
elif layout == 'circular':
|
||||
pos = nx.circular_layout(G)
|
||||
elif layout == 'kamada_kawai':
|
||||
pos = nx.kamada_kawai_layout(G)
|
||||
elif layout == 'random':
|
||||
pos = nx.random_layout(G)
|
||||
else:
|
||||
pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
|
||||
|
||||
# 按节点类型分组
|
||||
node_types = {}
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get('node_type', 'Unknown')
|
||||
if node_type not in node_types:
|
||||
node_types[node_type] = []
|
||||
node_types[node_type].append(node)
|
||||
|
||||
# 绘制边
|
||||
nx.draw_networkx_edges(
|
||||
G, pos,
|
||||
alpha=0.3,
|
||||
width=0.5,
|
||||
edge_color='gray',
|
||||
arrows=True,
|
||||
arrowsize=10,
|
||||
arrowstyle='->,head_width=0.2,head_length=0.3'
|
||||
)
|
||||
|
||||
# 按类型绘制节点
|
||||
for node_type, nodes in node_types.items():
|
||||
color = NODE_TYPE_COLORS.get(node_type, '#CCCCCC')
|
||||
size = NODE_TYPE_SIZES.get(node_type, 200)
|
||||
|
||||
nx.draw_networkx_nodes(
|
||||
G, pos,
|
||||
nodelist=nodes,
|
||||
node_color=color,
|
||||
node_size=size,
|
||||
alpha=0.8,
|
||||
edgecolors='white',
|
||||
linewidths=1
|
||||
)
|
||||
|
||||
# 绘制标签(只对重要节点)
|
||||
important_nodes = []
|
||||
important_labels = {}
|
||||
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get('node_type')
|
||||
# 只显示类别、批次和部分重要节点的标签
|
||||
if node_type in ['Category', 'Batch'] or (
|
||||
node_type == 'ICH_Project' and data.get('size', 0) > 400
|
||||
):
|
||||
important_nodes.append(node)
|
||||
important_labels[node] = data.get('label', node)
|
||||
|
||||
if len(important_nodes) <= 100: # 节点不多时显示所有标签
|
||||
nx.draw_networkx_labels(
|
||||
G, pos,
|
||||
labels=important_labels,
|
||||
font_size=8,
|
||||
font_weight='bold',
|
||||
font_family='sans-serif'
|
||||
)
|
||||
else:
|
||||
# 节点太多时只显示类别标签
|
||||
category_labels = {n: d['label'] for n, d in G.nodes(data=True)
|
||||
if d.get('node_type') == 'Category'}
|
||||
nx.draw_networkx_labels(
|
||||
G, pos,
|
||||
labels=category_labels,
|
||||
font_size=10,
|
||||
font_weight='bold'
|
||||
)
|
||||
|
||||
# 图例
|
||||
legend_patches = []
|
||||
for node_type, color in NODE_TYPE_COLORS.items():
|
||||
if node_type in node_types:
|
||||
patch = mpatches.Patch(color=color, label=node_type)
|
||||
legend_patches.append(patch)
|
||||
|
||||
plt.legend(
|
||||
handles=legend_patches,
|
||||
loc='upper right',
|
||||
fontsize=12,
|
||||
framealpha=0.9
|
||||
)
|
||||
|
||||
plt.title(title, fontsize=16, fontweight='bold', pad=20)
|
||||
plt.axis('off')
|
||||
plt.tight_layout()
|
||||
|
||||
# 保存图片
|
||||
plt.savefig(output_path, dpi=150, bbox_inches='tight')
|
||||
print(f" - 保存到: {output_path}")
|
||||
plt.close()
|
||||
|
||||
|
||||
def draw_subgraphs(G, output_dir):
|
||||
"""绘制子图(按节点类型分组)"""
|
||||
print("\n正在绘制子图...")
|
||||
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 1. 只显示项目和类别
|
||||
print(" 1. 项目-类别关系图...")
|
||||
G1 = filter_graph_by_type(G, ['ICH_Project', 'Category'])
|
||||
draw_graph(
|
||||
G1,
|
||||
output_dir / 'kg_project_category.png',
|
||||
title='非遗项目与类别关系',
|
||||
layout='spring'
|
||||
)
|
||||
|
||||
# 2. 只显示项目和传承人
|
||||
print(" 2. 项目-传承人关系图...")
|
||||
G2 = filter_graph_by_type(G, ['ICH_Project', 'Inheritor'])
|
||||
if G2.number_of_nodes() > 0:
|
||||
draw_graph(
|
||||
G2,
|
||||
output_dir / 'kg_project_inheritor.png',
|
||||
title='非遗项目与传承人关系',
|
||||
layout='spring'
|
||||
)
|
||||
|
||||
# 3. 只显示项目、类别和批次
|
||||
print(" 3. 项目-类别-批次关系图...")
|
||||
G3 = filter_graph_by_type(G, ['ICH_Project', 'Category', 'Batch'])
|
||||
draw_graph(
|
||||
G3,
|
||||
output_dir / 'kg_project_category_batch.png',
|
||||
title='非遗项目、类别与批次关系',
|
||||
layout='kamada_kawai'
|
||||
)
|
||||
|
||||
# 4. 完整图谱(抽样显示)
|
||||
print(" 4. 完整知识图谱...")
|
||||
if G.number_of_nodes() > 500:
|
||||
# 节点太多时,只显示连接度高的节点
|
||||
degrees = dict(G.degree())
|
||||
high_degree_nodes = [n for n, d in degrees.items() if d >= 3]
|
||||
G_sample = G.subgraph(high_degree_nodes).copy()
|
||||
draw_graph(
|
||||
G_sample,
|
||||
output_dir / 'kg_full_sampled.png',
|
||||
title=f'完整知识图谱(抽样,显示{G_sample.number_of_nodes()}个节点)',
|
||||
layout='spring'
|
||||
)
|
||||
else:
|
||||
draw_graph(
|
||||
G,
|
||||
output_dir / 'kg_full.png',
|
||||
title='完整知识图谱',
|
||||
layout='spring'
|
||||
)
|
||||
|
||||
|
||||
def print_statistics(G):
|
||||
"""打印图统计信息"""
|
||||
print("\n" + "="*60)
|
||||
print("图谱统计信息")
|
||||
print("="*60)
|
||||
|
||||
print(f"\n节点总数: {G.number_of_nodes()}")
|
||||
print(f"边总数: {G.number_of_edges()}")
|
||||
|
||||
# 按类型统计节点
|
||||
print("\n节点类型分布:")
|
||||
node_types = {}
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get('node_type', 'Unknown')
|
||||
node_types[node_type] = node_types.get(node_type, 0) + 1
|
||||
|
||||
for node_type, count in sorted(node_types.items()):
|
||||
percentage = (count / G.number_of_nodes() * 100)
|
||||
print(f" - {node_type}: {count} ({percentage:.1f}%)")
|
||||
|
||||
# 按类型统计边
|
||||
print("\n关系类型分布:")
|
||||
rel_types = {}
|
||||
for u, v, data in G.edges(data=True):
|
||||
rel_type = data.get('rel_type', 'Unknown')
|
||||
rel_types[rel_type] = rel_types.get(rel_type, 0) + 1
|
||||
|
||||
for rel_type, count in sorted(rel_types.items()):
|
||||
percentage = (count / G.number_of_edges() * 100)
|
||||
print(f" - {rel_type}: {count} ({percentage:.1f}%)")
|
||||
|
||||
# 连接度统计
|
||||
degrees = [d for n, d in G.degree()]
|
||||
print(f"\n连接度统计:")
|
||||
print(f" - 平均连接度: {np.mean(degrees):.2f}")
|
||||
print(f" - 最大连接度: {max(degrees)}")
|
||||
print(f" - 最小连接度: {min(degrees)}")
|
||||
|
||||
# 找出连接度最高的节点
|
||||
top_nodes = sorted(G.degree(), key=lambda x: x[1], reverse=True)[:10]
|
||||
print(f"\n连接度最高的10个节点:")
|
||||
for node, degree in top_nodes:
|
||||
node_data = G.nodes[node]
|
||||
label = node_data.get('full_label', node)
|
||||
node_type = node_data.get('node_type', '')
|
||||
print(f" - [{node_type}] {label}: {degree}个连接")
|
||||
|
||||
print("="*60)
|
||||
|
||||
|
||||
def visualize_kg(nodes_csv, rels_csv, output_dir):
|
||||
"""可视化知识图谱"""
|
||||
print("="*60)
|
||||
print("知识图谱可视化工具")
|
||||
print("="*60)
|
||||
|
||||
# 设置中文字体
|
||||
setup_chinese_font()
|
||||
|
||||
# 加载数据
|
||||
nodes_df, rels_df = load_graph_data(nodes_csv, rels_csv)
|
||||
|
||||
# 构建图
|
||||
G = build_networkx_graph(nodes_df, rels_df)
|
||||
|
||||
# 打印统计信息
|
||||
print_statistics(G)
|
||||
|
||||
# 绘制子图
|
||||
draw_subgraphs(G, output_dir)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("可视化完成!")
|
||||
print("="*60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 输入文件
|
||||
nodes_csv = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\nodes.csv'
|
||||
rels_csv = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\rels.csv'
|
||||
|
||||
# 输出目录
|
||||
output_dir = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\visualizations'
|
||||
|
||||
# 执行可视化
|
||||
visualize_kg(nodes_csv, rels_csv, output_dir)
|
||||
@@ -0,0 +1,344 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
实体规范化器
|
||||
负责实体的去重、ID生成和规范化
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
|
||||
class EntityNormalizer:
|
||||
"""实体规范化器"""
|
||||
|
||||
def __init__(self, ontology_file: str):
|
||||
"""
|
||||
初始化规范化器
|
||||
|
||||
Args:
|
||||
ontology_file: 本体配置文件路径
|
||||
"""
|
||||
# 加载本体配置
|
||||
with open(ontology_file, 'r', encoding='utf-8') as f:
|
||||
self.ontology = yaml.safe_load(f)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 实体注册表:{entity_id: entity_data}
|
||||
self.entity_registry = {}
|
||||
|
||||
# 文本到ID的映射:{normalized_text: entity_id}
|
||||
self.text_to_id_map = {}
|
||||
|
||||
# 类型计数器:{entity_type: count}
|
||||
self.type_counters = {}
|
||||
|
||||
self.logger.info("EntityNormalizer初始化完成")
|
||||
|
||||
def normalize_text(self, text: str) -> str:
|
||||
"""
|
||||
标准化文本
|
||||
|
||||
Args:
|
||||
text: 原始文本
|
||||
|
||||
Returns:
|
||||
标准化后的文本
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# 去除首尾空格
|
||||
text = text.strip()
|
||||
|
||||
# 统一全角/半角字符
|
||||
text = text.replace(' ', ' ').replace(',', ',').replace('、', ',')
|
||||
|
||||
# 转换为小写进行比较(保持原文用于显示)
|
||||
return text
|
||||
|
||||
def calculate_similarity(self, text1: str, text2: str) -> float:
|
||||
"""
|
||||
计算两个文本的相似度(基于编辑距离)
|
||||
|
||||
Args:
|
||||
text1: 文本1
|
||||
text2: 文本2
|
||||
|
||||
Returns:
|
||||
相似度(0-1之间)
|
||||
"""
|
||||
import Levenshtein
|
||||
|
||||
norm1 = self.normalize_text(text1)
|
||||
norm2 = self.normalize_text(text2)
|
||||
|
||||
if not norm1 or not norm2:
|
||||
return 0.0
|
||||
|
||||
max_len = max(len(norm1), len(norm2))
|
||||
if max_len == 0:
|
||||
return 1.0
|
||||
|
||||
distance = Levenshtein.distance(norm1, norm2)
|
||||
similarity = 1.0 - (distance / max_len)
|
||||
|
||||
return similarity
|
||||
|
||||
def generate_entity_id(self, entity_type: str, entity_text: str) -> str:
|
||||
"""
|
||||
生成实体ID
|
||||
|
||||
Args:
|
||||
entity_type: 实体类型
|
||||
entity_text: 实体文本
|
||||
|
||||
Returns:
|
||||
实体ID
|
||||
"""
|
||||
# 获取类型配置
|
||||
type_config = self.ontology['entity_types'].get(entity_type, {})
|
||||
prefix = type_config.get('prefix', 'UNK')
|
||||
|
||||
# 生成哈希值
|
||||
text_hash = abs(hash(entity_text)) % 100000
|
||||
|
||||
# 检查是否需要计数器(某些类型可能需要)
|
||||
if entity_type not in self.type_counters:
|
||||
self.type_counters[entity_type] = 0
|
||||
|
||||
# 根据类型生成ID
|
||||
if entity_type in ['Ethnic_Group', 'Geographic_Location', 'Geographic_Environment', 'Time_Period']:
|
||||
# 这些类型使用唯一哈希,不需要计数器
|
||||
entity_id = f"{prefix}-{text_hash:05d}"
|
||||
else:
|
||||
# 其他类型使用计数器
|
||||
self.type_counters[entity_type] += 1
|
||||
counter = self.type_counters[entity_type]
|
||||
entity_id = f"{prefix}-{text_hash:05d}-{counter}"
|
||||
|
||||
return entity_id
|
||||
|
||||
def find_similar_entity(
|
||||
self,
|
||||
entity_text: str,
|
||||
entity_type: str,
|
||||
similarity_threshold: float = 0.85
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
查找相似实体
|
||||
|
||||
Args:
|
||||
entity_text: 实体文本
|
||||
entity_type: 实体类型
|
||||
similarity_threshold: 相似度阈值
|
||||
|
||||
Returns:
|
||||
相似实体的ID,如果不存在返回None
|
||||
"""
|
||||
normalized_text = self.normalize_text(entity_text)
|
||||
|
||||
# 只在相同类型的实体中查找
|
||||
for entity_id, entity_data in self.entity_registry.items():
|
||||
if entity_data['type'] != entity_type:
|
||||
continue
|
||||
|
||||
# 检查文本相似度
|
||||
similarity = self.calculate_similarity(normalized_text, entity_data['canonical_name'])
|
||||
|
||||
if similarity >= similarity_threshold:
|
||||
self.logger.info(f"发现相似实体: '{entity_text}' ~ '{entity_data['canonical_name']}' (相似度: {similarity:.2f})")
|
||||
return entity_id
|
||||
|
||||
return None
|
||||
|
||||
def is_generic_concept(self, entity_text: str, entity_type: str) -> bool:
|
||||
"""
|
||||
检查是否为泛指概念(应该被过滤)
|
||||
|
||||
Args:
|
||||
entity_text: 实体文本
|
||||
entity_type: 实体类型
|
||||
|
||||
Returns:
|
||||
True表示是泛指概念,应该过滤
|
||||
"""
|
||||
generic_patterns = [
|
||||
'东北少数民族',
|
||||
'本地土著民族',
|
||||
'当地民族',
|
||||
'少数民族',
|
||||
'土著',
|
||||
'东北民族',
|
||||
'本地民族',
|
||||
'地区民族',
|
||||
]
|
||||
|
||||
normalized_text = self.normalize_text(entity_text)
|
||||
|
||||
# 检查是否匹配泛指模式
|
||||
for pattern in generic_patterns:
|
||||
if pattern in normalized_text:
|
||||
self.logger.info(f"过滤泛指概念: '{entity_text}' (类型: {entity_type})")
|
||||
return True
|
||||
|
||||
# 对于Ethnic_Group类型,必须是具体的民族名称
|
||||
if entity_type == 'Ethnic_Group':
|
||||
specific_ethnic_groups = [
|
||||
'满族', '赫哲族', '鄂伦春族', '鄂温克族',
|
||||
'达斡尔族', '朝鲜族', '蒙古族', '回族',
|
||||
'汉族', '锡伯族', '柯尔克孜族'
|
||||
]
|
||||
# 如果不在具体民族列表中,可能是泛指概念
|
||||
is_specific = any(group in normalized_text for group in specific_ethnic_groups)
|
||||
if not is_specific:
|
||||
self.logger.info(f"过滤非具体民族: '{entity_text}'")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def normalize_entity(
|
||||
self,
|
||||
raw_entity: Dict[str, Any],
|
||||
similarity_threshold: float = 0.85,
|
||||
filter_generic: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
规范化单个实体
|
||||
|
||||
Args:
|
||||
raw_entity: 原始实体数据(从LLM返回)
|
||||
similarity_threshold: 相似度阈值
|
||||
filter_generic: 是否过滤泛指概念
|
||||
|
||||
Returns:
|
||||
实体ID
|
||||
"""
|
||||
entity_text = raw_entity.get('text', '')
|
||||
entity_type = raw_entity.get('type', '')
|
||||
attributes = raw_entity.get('attributes', {})
|
||||
|
||||
if not entity_text or not entity_type:
|
||||
self.logger.error(f"实体缺少text或type: {raw_entity}")
|
||||
return None
|
||||
|
||||
# 验证实体类型是否在本体中定义
|
||||
if entity_type not in self.ontology['entity_types']:
|
||||
self.logger.warning(f"未知实体类型 '{entity_type}',跳过: '{entity_text}'")
|
||||
return None
|
||||
|
||||
# 过滤泛指概念
|
||||
if filter_generic and self.is_generic_concept(entity_text, entity_type):
|
||||
return None
|
||||
|
||||
# 标准化文本
|
||||
normalized_text = self.normalize_text(entity_text)
|
||||
|
||||
# 检查是否已存在完全相同的实体
|
||||
if normalized_text in self.text_to_id_map:
|
||||
existing_id = self.text_to_id_map[normalized_text]
|
||||
self.logger.info(f"实体已存在: '{entity_text}' -> {existing_id}")
|
||||
return existing_id
|
||||
|
||||
# 查找相似实体
|
||||
similar_id = self.find_similar_entity(entity_text, entity_type, similarity_threshold)
|
||||
if similar_id:
|
||||
# 合并到相似实体
|
||||
self.logger.info(f"合并实体: '{entity_text}' -> {similar_id}")
|
||||
self.text_to_id_map[normalized_text] = similar_id
|
||||
return similar_id
|
||||
|
||||
# 生成新实体ID
|
||||
entity_id = self.generate_entity_id(entity_type, entity_text)
|
||||
|
||||
# 获取标准名称
|
||||
standard_name = attributes.get('name') or entity_text
|
||||
|
||||
# 保存到注册表
|
||||
self.entity_registry[entity_id] = {
|
||||
'canonical_name': normalized_text,
|
||||
'display_name': standard_name,
|
||||
'type': entity_type,
|
||||
'attributes': attributes,
|
||||
'source_projects': []
|
||||
}
|
||||
|
||||
# 保存文本映射
|
||||
self.text_to_id_map[normalized_text] = entity_id
|
||||
|
||||
self.logger.info(f"新建实体: {entity_id} - '{standard_name}' ({entity_type})")
|
||||
|
||||
return entity_id
|
||||
|
||||
def normalize_batch(
|
||||
self,
|
||||
extraction_results: List[Dict[str, Any]],
|
||||
similarity_threshold: float = 0.85
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
批量规范化实体
|
||||
|
||||
Args:
|
||||
extraction_results: LLM抽取结果列表
|
||||
similarity_threshold: 相似度阈值
|
||||
|
||||
Returns:
|
||||
文本到ID的映射字典(包含所有已处理的实体)
|
||||
"""
|
||||
self.logger.info(f"开始批量规范化,共{len(extraction_results)}个抽取结果")
|
||||
|
||||
for idx, result in enumerate(extraction_results):
|
||||
if not result or 'entities' not in result:
|
||||
continue
|
||||
|
||||
for entity in result['entities']:
|
||||
self.normalize_entity(entity, similarity_threshold)
|
||||
|
||||
self.logger.info(f"批量规范化完成,生成{len(self.entity_registry)}个唯一实体")
|
||||
|
||||
# 返回完整的文本到ID映射(包含所有已处理的实体,不仅仅是本批次)
|
||||
return self.text_to_id_map.copy()
|
||||
|
||||
def get_entity_nodes(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有实体节点(用于生成CSV)
|
||||
|
||||
Returns:
|
||||
节点列表
|
||||
"""
|
||||
nodes = []
|
||||
|
||||
for entity_id, entity_data in self.entity_registry.items():
|
||||
node = {
|
||||
'id': entity_id,
|
||||
'label': entity_data['display_name'],
|
||||
'type': entity_data['type'],
|
||||
'properties': json.dumps(entity_data['attributes'], ensure_ascii=False)
|
||||
}
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取统计信息
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
stats = {
|
||||
'total_entities': len(self.entity_registry),
|
||||
'entities_by_type': {},
|
||||
'type_counters': self.type_counters.copy()
|
||||
}
|
||||
|
||||
# 按类型统计
|
||||
for entity_data in self.entity_registry.values():
|
||||
entity_type = entity_data['type']
|
||||
stats['entities_by_type'][entity_type] = stats['entities_by_type'].get(entity_type, 0) + 1
|
||||
|
||||
return stats
|
||||
@@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
关系构建器
|
||||
负责构建非遗项目与深层实体之间的关系
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
|
||||
class RelationshipBuilder:
|
||||
"""关系构建器"""
|
||||
|
||||
def __init__(self, ontology_file: str):
|
||||
"""
|
||||
初始化关系构建器
|
||||
|
||||
Args:
|
||||
ontology_file: 本体配置文件路径
|
||||
"""
|
||||
# 加载本体配置
|
||||
with open(ontology_file, 'r', encoding='utf-8') as f:
|
||||
self.ontology = yaml.safe_load(f)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 关系注册表:用于去重
|
||||
self.relationship_registry = set()
|
||||
|
||||
self.logger.info("RelationshipBuilder初始化完成")
|
||||
|
||||
def build_relationship(
|
||||
self,
|
||||
source_id: str,
|
||||
target_id: str,
|
||||
rel_type: str,
|
||||
properties: Dict[str, Any] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
构建单个关系
|
||||
|
||||
Args:
|
||||
source_id: 源实体ID
|
||||
target_id: 目标实体ID
|
||||
rel_type: 关系类型
|
||||
properties: 关系属性
|
||||
|
||||
Returns:
|
||||
关系字典,如果验证失败返回None
|
||||
"""
|
||||
# 验证关系类型
|
||||
if rel_type not in self.ontology['relationship_types']:
|
||||
self.logger.warning(f"未知关系类型: {rel_type}")
|
||||
return None
|
||||
|
||||
# 验证ID不为空
|
||||
if not source_id or not target_id:
|
||||
self.logger.error(f"关系ID不能为空: source={source_id}, target={target_id}")
|
||||
return None
|
||||
|
||||
# 生成关系唯一键(用于去重)
|
||||
rel_key = f"{source_id}-{target_id}-{rel_type}"
|
||||
|
||||
# 检查是否重复
|
||||
if rel_key in self.relationship_registry:
|
||||
self.logger.debug(f"关系已存在,跳过: {rel_key}")
|
||||
return None
|
||||
|
||||
# 添加到注册表
|
||||
self.relationship_registry.add(rel_key)
|
||||
|
||||
# 构建关系
|
||||
relationship = {
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type,
|
||||
'properties': json.dumps(properties or {}, ensure_ascii=False)
|
||||
}
|
||||
|
||||
return relationship
|
||||
|
||||
def build_relationships_from_extraction(
|
||||
self,
|
||||
project_id: str,
|
||||
extraction_result: Dict[str, Any],
|
||||
entity_id_map: Dict[str, str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
从抽取结果构建关系
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
extraction_result: LLM抽取结果
|
||||
entity_id_map: 实体文本到ID的映射
|
||||
|
||||
Returns:
|
||||
关系列表
|
||||
"""
|
||||
relationships = []
|
||||
|
||||
if not extraction_result or 'relationships' not in extraction_result:
|
||||
return relationships
|
||||
|
||||
for rel in extraction_result['relationships']:
|
||||
# 获取源实体(应该是项目ID)
|
||||
source_entity = rel.get('source_entity', '')
|
||||
|
||||
# 验证源实体是否匹配当前项目
|
||||
if source_entity != project_id:
|
||||
self.logger.warning(f"关系源实体不匹配: 期望{project_id}, 实际{source_entity}")
|
||||
# 如果不匹配,尝试修正
|
||||
source_entity = project_id
|
||||
|
||||
# 获取目标实体文本
|
||||
target_text = rel.get('target_entity', '')
|
||||
|
||||
# 查找目标实体ID
|
||||
target_id = entity_id_map.get(target_text)
|
||||
|
||||
if not target_id:
|
||||
self.logger.warning(f"未找到目标实体ID: {target_text}")
|
||||
continue
|
||||
|
||||
# 获取关系类型
|
||||
rel_type = rel.get('type', '')
|
||||
rel_properties = rel.get('properties', {})
|
||||
|
||||
# 构建关系
|
||||
relationship = self.build_relationship(
|
||||
source_entity,
|
||||
target_id,
|
||||
rel_type,
|
||||
rel_properties
|
||||
)
|
||||
|
||||
if relationship:
|
||||
relationships.append(relationship)
|
||||
|
||||
return relationships
|
||||
|
||||
def build_batch_relationships(
|
||||
self,
|
||||
projects_data: List[Dict[str, Any]],
|
||||
extraction_results: List[Dict[str, Any]],
|
||||
entity_id_map: Dict[str, str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
批量构建关系
|
||||
|
||||
Args:
|
||||
projects_data: 项目数据列表
|
||||
extraction_results: 抽取结果列表
|
||||
entity_id_map: 实体文本到ID的映射
|
||||
|
||||
Returns:
|
||||
关系列表
|
||||
"""
|
||||
self.logger.info(f"开始批量构建关系,共{len(projects_data)}个项目")
|
||||
|
||||
all_relationships = []
|
||||
|
||||
for idx, (project_data, extraction_result) in enumerate(zip(projects_data, extraction_results)):
|
||||
if not extraction_result:
|
||||
continue
|
||||
|
||||
project_id = project_data.get('project_id', '')
|
||||
|
||||
if not project_id:
|
||||
self.logger.warning(f"项目{idx}缺少project_id")
|
||||
continue
|
||||
|
||||
# 构建该项目的关系
|
||||
relationships = self.build_relationships_from_extraction(
|
||||
project_id,
|
||||
extraction_result,
|
||||
entity_id_map
|
||||
)
|
||||
|
||||
all_relationships.extend(relationships)
|
||||
|
||||
self.logger.info(f"项目 {project_id} 构建了{len(relationships)}个关系")
|
||||
|
||||
self.logger.info(f"批量构建完成,共{len(all_relationships)}个关系")
|
||||
|
||||
return all_relationships
|
||||
|
||||
def validate_relationships(
|
||||
self,
|
||||
relationships: List[Dict[str, Any]],
|
||||
node_ids: set
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
验证关系完整性
|
||||
|
||||
Args:
|
||||
relationships: 关系列表
|
||||
node_ids: 节点ID集合
|
||||
|
||||
Returns:
|
||||
验证报告
|
||||
"""
|
||||
report = {
|
||||
'total_relationships': len(relationships),
|
||||
'valid_relationships': 0,
|
||||
'invalid_relationships': 0,
|
||||
'broken_links': [],
|
||||
'invalid_types': [],
|
||||
'relationships_by_type': {}
|
||||
}
|
||||
|
||||
for rel in relationships:
|
||||
source_id = rel.get('source', '')
|
||||
target_id = rel.get('target', '')
|
||||
rel_type = rel.get('type', '')
|
||||
|
||||
# 统计关系类型
|
||||
report['relationships_by_type'][rel_type] = \
|
||||
report['relationships_by_type'].get(rel_type, 0) + 1
|
||||
|
||||
is_valid = True
|
||||
|
||||
# 验证关系类型
|
||||
if rel_type not in self.ontology['relationship_types']:
|
||||
report['invalid_types'].append({
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type
|
||||
})
|
||||
is_valid = False
|
||||
|
||||
# 验证链接完整性
|
||||
if source_id not in node_ids:
|
||||
report['broken_links'].append({
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type,
|
||||
'issue': 'source_not_found'
|
||||
})
|
||||
is_valid = False
|
||||
|
||||
if target_id not in node_ids:
|
||||
report['broken_links'].append({
|
||||
'source': source_id,
|
||||
'target': target_id,
|
||||
'type': rel_type,
|
||||
'issue': 'target_not_found'
|
||||
})
|
||||
is_valid = False
|
||||
|
||||
if is_valid:
|
||||
report['valid_relationships'] += 1
|
||||
else:
|
||||
report['invalid_relationships'] += 1
|
||||
|
||||
return report
|
||||
|
||||
def get_statistics(self, relationships: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
获取关系统计信息
|
||||
|
||||
Args:
|
||||
relationships: 关系列表
|
||||
|
||||
Returns:
|
||||
统计信息
|
||||
"""
|
||||
stats = {
|
||||
'total_relationships': len(relationships),
|
||||
'relationships_by_type': {}
|
||||
}
|
||||
|
||||
for rel in relationships:
|
||||
rel_type = rel.get('type', 'Unknown')
|
||||
stats['relationships_by_type'][rel_type] = \
|
||||
stats['relationships_by_type'].get(rel_type, 0) + 1
|
||||
|
||||
return stats
|
||||
@@ -0,0 +1,406 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
深度文化实体抽取主流程
|
||||
从Excel备注字段抽取深层文化实体并生成知识图谱CSV文件
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# 添加模块路径
|
||||
import sys
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
|
||||
from knowledge_extraction.deep_entity_extractor import DeepEntityExtractor
|
||||
from data_processing.entity_normalizer import EntityNormalizer
|
||||
from data_processing.relationship_builder import RelationshipBuilder
|
||||
|
||||
|
||||
class DeepExtractionPipeline:
|
||||
"""深度文化实体抽取流程"""
|
||||
|
||||
def __init__(self, config_file: str):
|
||||
"""
|
||||
初始化流程
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
self.config_file = config_file
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化组件
|
||||
self.extractor = DeepEntityExtractor(config_file)
|
||||
self.normalizer = EntityNormalizer(
|
||||
str(Path(config_file).parent / 'entity_ontology.yaml')
|
||||
)
|
||||
self.builder = RelationshipBuilder(
|
||||
str(Path(config_file).parent / 'entity_ontology.yaml')
|
||||
)
|
||||
|
||||
self.logger.info("DeepExtractionPipeline初始化完成")
|
||||
|
||||
def load_excel_data(self, excel_file: str, max_rows: int = None) -> List[Dict[str, str]]:
|
||||
"""
|
||||
加载Excel数据
|
||||
|
||||
Args:
|
||||
excel_file: Excel文件路径
|
||||
max_rows: 最大读取行数(用于测试)
|
||||
|
||||
Returns:
|
||||
项目数据列表
|
||||
"""
|
||||
self.logger.info(f"读取Excel文件: {excel_file}")
|
||||
|
||||
df = pd.read_excel(excel_file)
|
||||
|
||||
# 限制行数
|
||||
if max_rows:
|
||||
df = df.head(max_rows)
|
||||
self.logger.info(f"限制读取行数: {max_rows}")
|
||||
|
||||
# 检查必需列
|
||||
required_columns = ['总序号', '项目名称', '备注']
|
||||
missing_columns = [col for col in required_columns if col not in df.columns]
|
||||
|
||||
if missing_columns:
|
||||
self.logger.error(f"Excel缺少必需列: {missing_columns}")
|
||||
raise ValueError(f"缺少必需列: {missing_columns}")
|
||||
|
||||
# 构建项目数据列表
|
||||
projects_data = []
|
||||
for idx, row in df.iterrows():
|
||||
seq_num = row.get('总序号', idx + 1)
|
||||
project_name = row.get('项目名称', '')
|
||||
remark_text = row.get('备注', '')
|
||||
|
||||
# 过滤空备注
|
||||
if not remark_text or len(str(remark_text).strip()) < 10:
|
||||
continue
|
||||
|
||||
projects_data.append({
|
||||
'project_id': f"ICH-{int(seq_num)}",
|
||||
'project_name': str(project_name),
|
||||
'remark_text': str(remark_text),
|
||||
'row_number': idx + 2 # Excel行号(1-based + header)
|
||||
})
|
||||
|
||||
self.logger.info(f"加载了{len(projects_data)}个项目的数据")
|
||||
|
||||
return projects_data
|
||||
|
||||
async def run_extraction(
|
||||
self,
|
||||
excel_file: str,
|
||||
output_prefix: str = "",
|
||||
max_rows: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
执行完整的抽取流程(支持增量保存)
|
||||
|
||||
Args:
|
||||
excel_file: Excel文件路径
|
||||
output_prefix: 输出文件前缀(用于测试)
|
||||
max_rows: 最大读取行数(用于测试)
|
||||
|
||||
Returns:
|
||||
处理结果统计
|
||||
"""
|
||||
self.logger.info("="*60)
|
||||
self.logger.info("开始深度文化实体抽取流程(增量保存模式)")
|
||||
self.logger.info("="*60)
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
# 1. 加载数据
|
||||
projects_data = self.load_excel_data(excel_file, max_rows)
|
||||
|
||||
if not projects_data:
|
||||
self.logger.error("没有可处理的数据")
|
||||
return {}
|
||||
|
||||
# 准备输出目录
|
||||
output_dir = Path('output')
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 确定输出文件名
|
||||
if output_prefix:
|
||||
nodes_file = output_dir / f"{output_prefix}_nodes.csv"
|
||||
rels_file = output_dir / f"{output_prefix}_rels.csv"
|
||||
report_file = output_dir / f"{output_prefix}_report.md"
|
||||
else:
|
||||
nodes_file = output_dir / Path(self.extractor.config['output']['nodes_file']).name
|
||||
rels_file = output_dir / Path(self.extractor.config['output']['relationships_file']).name
|
||||
report_file = output_dir / Path(self.extractor.config['output']['report_file']).name
|
||||
|
||||
# 2. 定义增量保存回调函数
|
||||
async def save_progress(batch_num, total_batches, extraction_results, processed_projects):
|
||||
"""每批次完成后保存进度(仅保存节点,不构建关系)"""
|
||||
try:
|
||||
self.logger.info(f"\n[增量保存] 批次 {batch_num}/{total_batches} 开始保存...")
|
||||
|
||||
# 实体规范化(累积到实体注册表)
|
||||
self.normalizer.normalize_batch(extraction_results)
|
||||
|
||||
# 生成节点(累积所有已处理的实体)
|
||||
entity_nodes = self.normalizer.get_entity_nodes()
|
||||
|
||||
# 添加项目节点(仅当前批次)
|
||||
project_nodes = [
|
||||
{
|
||||
'id': item['project_id'],
|
||||
'label': item['project_name'],
|
||||
'type': 'ICH_Project',
|
||||
'properties': '{}'
|
||||
}
|
||||
for item in processed_projects
|
||||
]
|
||||
|
||||
all_nodes = entity_nodes + project_nodes
|
||||
|
||||
# 保存节点
|
||||
nodes_df = pd.DataFrame(all_nodes)
|
||||
nodes_df.to_csv(nodes_file, index=False, encoding='utf-8-sig')
|
||||
self.logger.info(f"[增量保存] 节点已保存: {nodes_file} ({len(all_nodes)}行)")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[增量保存] 批次 {batch_num} 保存失败: {str(e)}", exc_info=True)
|
||||
|
||||
# 3. LLM批量抽取(带增量保存回调)
|
||||
self.logger.info("\n步骤1: LLM批量抽取(增量保存模式)")
|
||||
extraction_results = await self.extractor.batch_extract(
|
||||
projects_data,
|
||||
progress_callback=save_progress
|
||||
)
|
||||
|
||||
# 4. 最终数据验证和报告
|
||||
self.logger.info("\n步骤2: 最终数据验证")
|
||||
|
||||
# 重新规范化所有实体(确保一致性)
|
||||
entity_id_map = self.normalizer.normalize_batch(extraction_results)
|
||||
|
||||
# 重新构建所有关系
|
||||
relationships = self.builder.build_batch_relationships(
|
||||
projects_data,
|
||||
extraction_results,
|
||||
entity_id_map
|
||||
)
|
||||
|
||||
# 生成最终节点
|
||||
entity_nodes = self.normalizer.get_entity_nodes()
|
||||
project_nodes = [
|
||||
{
|
||||
'id': item['project_id'],
|
||||
'label': item['project_name'],
|
||||
'type': 'ICH_Project',
|
||||
'properties': '{}'
|
||||
}
|
||||
for item in projects_data
|
||||
]
|
||||
all_nodes = entity_nodes + project_nodes
|
||||
|
||||
# 数据验证
|
||||
node_ids = set(node['id'] for node in all_nodes)
|
||||
validation_report = self.builder.validate_relationships(relationships, node_ids)
|
||||
|
||||
# 保存最终结果
|
||||
self.logger.info("\n步骤3: 保存最终结果")
|
||||
nodes_df = pd.DataFrame(all_nodes)
|
||||
nodes_df.to_csv(nodes_file, index=False, encoding='utf-8-sig')
|
||||
self.logger.info(f"最终节点已保存: {nodes_file} ({len(all_nodes)}行)")
|
||||
|
||||
rels_df = pd.DataFrame(relationships)
|
||||
rels_df.to_csv(rels_file, index=False, encoding='utf-8-sig')
|
||||
self.logger.info(f"最终关系已保存: {rels_file} ({len(relationships)}行)")
|
||||
|
||||
# 生成报告
|
||||
self.logger.info("\n步骤4: 生成报告")
|
||||
self.generate_report(
|
||||
report_file,
|
||||
projects_data,
|
||||
all_nodes,
|
||||
relationships,
|
||||
validation_report,
|
||||
start_time
|
||||
)
|
||||
|
||||
end_time = datetime.now()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
# 返回统计信息
|
||||
result_stats = {
|
||||
'total_projects': len(projects_data),
|
||||
'total_nodes': len(all_nodes),
|
||||
'total_relationships': len(relationships),
|
||||
'entity_nodes': len(entity_nodes),
|
||||
'project_nodes': len(project_nodes),
|
||||
'duration_seconds': duration,
|
||||
'validation_report': validation_report
|
||||
}
|
||||
|
||||
self.logger.info("\n"+"="*60)
|
||||
self.logger.info(f"抽取完成!耗时: {duration:.2f}秒")
|
||||
self.logger.info(f"项目数: {result_stats['total_projects']}")
|
||||
self.logger.info(f"节点数: {result_stats['total_nodes']}")
|
||||
self.logger.info(f"关系数: {result_stats['total_relationships']}")
|
||||
self.logger.info("="*60)
|
||||
|
||||
return result_stats
|
||||
|
||||
def generate_report(
|
||||
self,
|
||||
report_file: Path,
|
||||
projects_data: List[Dict[str, str]],
|
||||
nodes: List[Dict[str, Any]],
|
||||
relationships: List[Dict[str, Any]],
|
||||
validation_report: Dict[str, Any],
|
||||
start_time: datetime
|
||||
):
|
||||
"""
|
||||
生成抽取报告
|
||||
|
||||
Args:
|
||||
report_file: 报告文件路径
|
||||
projects_data: 项目数据
|
||||
nodes: 节点列表
|
||||
relationships: 关系列表
|
||||
validation_report: 验证报告
|
||||
start_time: 开始时间
|
||||
"""
|
||||
report_lines = []
|
||||
|
||||
# 标题
|
||||
report_lines.append("# 深度文化实体抽取报告\n")
|
||||
report_lines.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
report_lines.append(f"**耗时**: {(datetime.now() - start_time).total_seconds():.2f}秒\n")
|
||||
report_lines.append("---\n\n")
|
||||
|
||||
# 1. 处理统计
|
||||
report_lines.append("## 1. 处理统计\n\n")
|
||||
report_lines.append(f"- **处理项目数**: {len(projects_data)}\n")
|
||||
report_lines.append(f"- **总节点数**: {len(nodes)}\n")
|
||||
report_lines.append(f"- **总关系数**: {len(relationships)}\n")
|
||||
report_lines.append(f"- **实体节点数**: {len([n for n in nodes if n['type'] != 'ICH_Project'])}\n")
|
||||
report_lines.append(f"- **项目节点数**: {len([n for n in nodes if n['type'] == 'ICH_Project'])}\n")
|
||||
|
||||
# 2. 节点类型分布
|
||||
report_lines.append("\n## 2. 节点类型分布\n\n")
|
||||
node_types = {}
|
||||
for node in nodes:
|
||||
node_type = node['type']
|
||||
node_types[node_type] = node_types.get(node_type, 0) + 1
|
||||
|
||||
report_lines.append("| 节点类型 | 数量 | 占比 |\n")
|
||||
report_lines.append("|---------|------|------|\n")
|
||||
for node_type, count in sorted(node_types.items()):
|
||||
percentage = (count / len(nodes) * 100) if len(nodes) > 0 else 0
|
||||
report_lines.append(f"| {node_type} | {count} | {percentage:.1f}% |\n")
|
||||
|
||||
# 3. 关系类型分布
|
||||
report_lines.append("\n## 3. 关系类型分布\n\n")
|
||||
rel_types = {}
|
||||
for rel in relationships:
|
||||
rel_type = rel['type']
|
||||
rel_types[rel_type] = rel_types.get(rel_type, 0) + 1
|
||||
|
||||
report_lines.append("| 关系类型 | 数量 | 占比 |\n")
|
||||
report_lines.append("|---------|------|------|\n")
|
||||
for rel_type, count in sorted(rel_types.items()):
|
||||
percentage = (count / len(relationships) * 100) if len(relationships) > 0 else 0
|
||||
report_lines.append(f"| {rel_type} | {count} | {percentage:.1f}% |\n")
|
||||
|
||||
# 4. 数据质量
|
||||
report_lines.append("\n## 4. 数据质量\n\n")
|
||||
report_lines.append(f"- **有效关系**: {validation_report['valid_relationships']}\n")
|
||||
report_lines.append(f"- **无效关系**: {validation_report['invalid_relationships']}\n")
|
||||
|
||||
if validation_report['broken_links']:
|
||||
report_lines.append(f"\n**断链警告**: {len(validation_report['broken_links'])}个\n")
|
||||
report_lines.append("```json\n")
|
||||
report_lines.append(json.dumps(validation_report['broken_links'][:10], ensure_ascii=False, indent=2))
|
||||
if len(validation_report['broken_links']) > 10:
|
||||
report_lines.append(f"\n... (还有{len(validation_report['broken_links'])-10}个)")
|
||||
report_lines.append("\n```\n")
|
||||
|
||||
# 5. 项目详情(前5个)
|
||||
report_lines.append("\n## 5. 项目抽取详情(前5个)\n\n")
|
||||
|
||||
for idx, (project_data, node) in enumerate(zip(projects_data[:5], nodes[:5])):
|
||||
if node['type'] != 'ICH_Project':
|
||||
continue
|
||||
|
||||
report_lines.append(f"### {idx+1}. {project_data['project_name']}\n\n")
|
||||
report_lines.append(f"**项目ID**: {project_data['project_id']}\n\n")
|
||||
report_lines.append(f"**备注**: {project_data['remark_text'][:200]}...\n\n")
|
||||
|
||||
# 查找相关关系
|
||||
related_rels = [r for r in relationships if r['source'] == project_data['project_id']]
|
||||
if related_rels:
|
||||
report_lines.append(f"**关系数**: {len(related_rels)}\n\n")
|
||||
report_lines.append("| 关系类型 | 目标实体 |\n")
|
||||
report_lines.append("|---------|---------|\n")
|
||||
for rel in related_rels[:10]:
|
||||
target_node = next((n for n in nodes if n['id'] == rel['target']), None)
|
||||
if target_node:
|
||||
report_lines.append(f"| {rel['type']} | {target_node['label']} |\n")
|
||||
if len(related_rels) > 10:
|
||||
report_lines.append(f"| ... | 还有{len(related_rels)-10}个关系 |\n")
|
||||
|
||||
report_lines.append("\n")
|
||||
|
||||
# 6. 输出文件
|
||||
report_lines.append("## 6. 输出文件\n\n")
|
||||
report_lines.append(f"- **节点文件**: `{report_file.parent / (report_file.stem.replace('_report', '') + '_nodes.csv')}`\n")
|
||||
report_lines.append(f"- **关系文件**: `{report_file.parent / (report_file.stem.replace('_report', '') + '_rels.csv')}`\n")
|
||||
|
||||
# 保存报告
|
||||
with open(report_file, 'w', encoding='utf-8') as f:
|
||||
f.writelines(report_lines)
|
||||
|
||||
self.logger.info(f"报告已保存: {report_file}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
import sys
|
||||
|
||||
# 配置文件
|
||||
config_file = r"E:\Project\2026_KG_ICH\dofile\kg_project\config\deep_extraction_config.yaml"
|
||||
|
||||
# 数据文件:从命令行参数获取,或使用默认完整数据文件
|
||||
if len(sys.argv) > 1:
|
||||
excel_file = sys.argv[1]
|
||||
else:
|
||||
excel_file = r"E:\Project\2026_KG_ICH\data\黑龙江国家级和省级非遗名单.xlsx"
|
||||
|
||||
# 创建流程
|
||||
pipeline = DeepExtractionPipeline(config_file)
|
||||
|
||||
# 执行抽取
|
||||
results = await pipeline.run_extraction(
|
||||
excel_file=excel_file,
|
||||
output_prefix="", # 不使用前缀(完整抽取)
|
||||
max_rows=None # 读取所有行
|
||||
)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("抽取完成!")
|
||||
print("="*60)
|
||||
print(f"\n结果统计:")
|
||||
print(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,420 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
深度文化实体抽取器
|
||||
利用LLM从非遗项目备注中抽取深层文化实体和关系
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
|
||||
class DeepEntityExtractor:
|
||||
"""深度文化实体抽取器"""
|
||||
|
||||
def __init__(self, config_file: str):
|
||||
"""
|
||||
初始化抽取器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
# 加载配置
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
# 设置日志
|
||||
log_config = self.config.get('logging', {})
|
||||
log_file = log_config.get('log_file', 'logs/deep_extraction.log')
|
||||
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建logger
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.setLevel(getattr(logging, log_config.get('level', 'INFO')))
|
||||
|
||||
# 清除已有的handlers
|
||||
self.logger.handlers.clear()
|
||||
|
||||
# 文件handler(实时刷新)
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(getattr(logging, log_config.get('level', 'INFO')))
|
||||
file_formatter = logging.Formatter(log_config.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||
file_handler.setFormatter(file_formatter)
|
||||
# 强制实时刷新
|
||||
file_handler.flush = lambda: file_handler.stream.flush()
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
# 控制台handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(getattr(logging, log_config.get('level', 'INFO')))
|
||||
console_formatter = logging.Formatter(log_config.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||
console_handler.setFormatter(console_formatter)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
# 加载实体本体配置
|
||||
ontology_file = Path(config_file).parent / 'entity_ontology.yaml'
|
||||
with open(ontology_file, 'r', encoding='utf-8') as f:
|
||||
self.ontology = yaml.safe_load(f)
|
||||
|
||||
# 初始化LLM
|
||||
self._init_llm()
|
||||
|
||||
# 构建提示词
|
||||
self.system_prompt = self._build_system_prompt()
|
||||
|
||||
self.logger.info("DeepEntityExtractor初始化完成")
|
||||
|
||||
def _init_llm(self):
|
||||
"""初始化LLM模型"""
|
||||
llm_config = self.config['llm']
|
||||
|
||||
# 读取API密钥(从现有的api_keys.yaml)
|
||||
api_keys_file = Path(__file__).parent.parent.parent / 'config' / 'api_keys.yaml'
|
||||
with open(api_keys_file, 'r', encoding='utf-8') as f:
|
||||
api_keys = yaml.safe_load(f)
|
||||
|
||||
# 适配API密钥格式
|
||||
api_key = api_keys.get('deepseek_api_key', '')
|
||||
if not api_key:
|
||||
# 尝试嵌套格式
|
||||
api_key = api_keys.get('deepseek', {}).get('api_key', '')
|
||||
|
||||
self.llm = ChatDeepSeek(
|
||||
model=llm_config['model'],
|
||||
temperature=llm_config['temperature'],
|
||||
max_tokens=llm_config['max_tokens'],
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
self.logger.info(f"LLM初始化完成: {llm_config['model']}")
|
||||
|
||||
def _build_system_prompt(self) -> str:
|
||||
"""构建系统提示词"""
|
||||
entity_prompts = self.config.get('entity_type_prompts', {})
|
||||
relationship_prompts = self.config.get('relationship_type_prompts', {})
|
||||
|
||||
prompt = """你是一位非物质文化遗产领域的专家,擅长从项目描述中识别深层文化实体和关系。
|
||||
|
||||
【任务】
|
||||
请从非遗项目描述中识别以下9类深层文化实体和9种深层关系:
|
||||
|
||||
=== 实体类型 ===
|
||||
"""
|
||||
|
||||
# 添加实体类型说明
|
||||
for entity_type, description in entity_prompts.items():
|
||||
prompt += f"{entity_type}:{description}\n"
|
||||
|
||||
prompt += "\n=== 关系类型 ===\n"
|
||||
|
||||
# 添加关系类型说明
|
||||
for rel_type, description in relationship_prompts.items():
|
||||
prompt += f"{rel_type}:{description}\n"
|
||||
|
||||
prompt += """
|
||||
【输出要求】
|
||||
1. 严格按JSON格式输出,不要包含任何其他文本
|
||||
2. 只输出明确的、文本中提到的实体和关系,不要臆测
|
||||
3. 实体text必须从原文中提取,不要自行改写
|
||||
4. 同一个实体只识别一次,避免重复
|
||||
5. 关系必须基于文本中的明确描述
|
||||
6. 如果某类实体或关系不存在,相应数组为空
|
||||
7. 确保JSON格式正确,可以被Python解析
|
||||
|
||||
【JSON输出格式】
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"text": "实体文本(如:鱼皮)",
|
||||
"type": "实体类型(如:Material)",
|
||||
"attributes": {
|
||||
"name": "标准名称",
|
||||
"category": "类别(如:动物材料)",
|
||||
"description": "详细描述"
|
||||
}
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"source_entity": "ICH-{项目ID}",
|
||||
"target_entity": "目标实体文本",
|
||||
"type": "关系类型",
|
||||
"properties": {
|
||||
"description": "关系描述",
|
||||
"context": "上下文信息"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_entities": 0,
|
||||
"total_relationships": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
async def extract_from_remark(
|
||||
self,
|
||||
project_id: str,
|
||||
project_name: str,
|
||||
remark_text: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
从单条备注抽取实体和关系
|
||||
|
||||
Args:
|
||||
project_id: 项目ID(如ICH-1)
|
||||
project_name: 项目名称
|
||||
remark_text: 备注文本
|
||||
|
||||
Returns:
|
||||
抽取结果(包含entities, relationships, summary)
|
||||
"""
|
||||
if not remark_text or len(remark_text.strip()) < 10:
|
||||
self.logger.warning(f"项目 {project_id} 备注文本过短,跳过抽取")
|
||||
return None
|
||||
|
||||
# 构建用户提示词
|
||||
user_prompt = f"""【项目信息】
|
||||
项目名称:{project_name}
|
||||
项目ID:{project_id}
|
||||
|
||||
【描述文本】
|
||||
{remark_text}
|
||||
|
||||
请从上述描述中识别深层文化实体和关系,严格按JSON格式输出。"""
|
||||
|
||||
try:
|
||||
# 调用LLM
|
||||
messages = [
|
||||
SystemMessage(content=self.system_prompt),
|
||||
HumanMessage(content=user_prompt)
|
||||
]
|
||||
|
||||
max_retries = self.config['llm']['max_retries']
|
||||
retry_delay = self.config['llm']['retry_delay']
|
||||
request_timeout = self.config['llm'].get('request_timeout', 120)
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
self.logger.info(f"项目 {project_id} 开始LLM调用(尝试{attempt+1}/{max_retries})")
|
||||
|
||||
# 添加超时控制
|
||||
response = await asyncio.wait_for(
|
||||
self.llm.ainvoke(messages),
|
||||
timeout=request_timeout
|
||||
)
|
||||
result_text = response.content
|
||||
|
||||
self.logger.info(f"项目 {project_id} LLM调用成功,开始提取JSON")
|
||||
|
||||
# 提取JSON
|
||||
extraction_result = self._extract_json(result_text)
|
||||
|
||||
if extraction_result:
|
||||
# 验证结果
|
||||
if self._validate_extraction(extraction_result):
|
||||
self.logger.info(f"项目 {project_id} 抽取成功:{extraction_result['summary']['total_entities']}个实体,{extraction_result['summary']['total_relationships']}个关系")
|
||||
return extraction_result
|
||||
else:
|
||||
self.logger.warning(f"项目 {project_id} 抽取结果验证失败")
|
||||
else:
|
||||
self.logger.warning(f"项目 {project_id} JSON提取失败")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self.logger.error(f"项目 {project_id} LLM调用超时({request_timeout}秒)(尝试{attempt+1}/{max_retries})")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
self.logger.error(f"项目 {project_id} 达到最大重试次数,放弃抽取")
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"项目 {project_id} LLM调用失败(尝试{attempt+1}/{max_retries}): {type(e).__name__}: {str(e)}")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"项目 {project_id} 抽取失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _extract_json(self, text: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
从文本中提取JSON
|
||||
|
||||
Args:
|
||||
text: LLM返回的文本
|
||||
|
||||
Returns:
|
||||
解析后的JSON对象,失败返回None
|
||||
"""
|
||||
# 尝试直接解析
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试提取JSON块
|
||||
import re
|
||||
json_pattern = r'```json\s*(.*?)\s*```'
|
||||
match = re.search(json_pattern, text, re.DOTALL)
|
||||
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(1))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 尝试提取花括号内容
|
||||
brace_pattern = r'\{.*\}'
|
||||
match = re.search(brace_pattern, text, re.DOTALL)
|
||||
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
self.logger.error(f"无法从文本中提取有效JSON: {text[:200]}...")
|
||||
return None
|
||||
|
||||
def _validate_extraction(self, result: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
验证抽取结果
|
||||
|
||||
Args:
|
||||
result: 抽取结果
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
# 检查必需字段
|
||||
required_fields = ['entities', 'relationships', 'summary']
|
||||
for field in required_fields:
|
||||
if field not in result:
|
||||
self.logger.error(f"缺少必需字段: {field}")
|
||||
return False
|
||||
|
||||
# 检查entities格式
|
||||
if not isinstance(result['entities'], list):
|
||||
self.logger.error("entities必须是列表")
|
||||
return False
|
||||
|
||||
for entity in result['entities']:
|
||||
if not all(k in entity for k in ['text', 'type', 'attributes']):
|
||||
self.logger.error(f"实体缺少必需字段: {entity}")
|
||||
return False
|
||||
|
||||
# 检查实体类型是否合法
|
||||
entity_type = entity['type']
|
||||
if entity_type not in self.ontology['entity_types']:
|
||||
self.logger.warning(f"未知实体类型: {entity_type}")
|
||||
|
||||
# 检查relationships格式
|
||||
if not isinstance(result['relationships'], list):
|
||||
self.logger.error("relationships必须是列表")
|
||||
return False
|
||||
|
||||
for rel in result['relationships']:
|
||||
if not all(k in rel for k in ['source_entity', 'target_entity', 'type', 'properties']):
|
||||
self.logger.error(f"关系缺少必需字段: {rel}")
|
||||
return False
|
||||
|
||||
# 检查关系类型是否合法
|
||||
rel_type = rel['type']
|
||||
if rel_type not in self.ontology['relationship_types']:
|
||||
self.logger.warning(f"未知关系类型: {rel_type}")
|
||||
|
||||
return True
|
||||
|
||||
async def batch_extract(
|
||||
self,
|
||||
projects_data: List[Dict[str, str]],
|
||||
batch_size: int = None,
|
||||
progress_callback=None
|
||||
) -> List[Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
批量抽取实体和关系
|
||||
|
||||
Args:
|
||||
projects_data: 项目数据列表,每项包含project_id, project_name, remark_text
|
||||
batch_size: 批次大小(从配置文件读取)
|
||||
progress_callback: 进度回调函数,每批次完成后调用
|
||||
|
||||
Returns:
|
||||
抽取结果列表
|
||||
"""
|
||||
if batch_size is None:
|
||||
batch_size = self.config['batch_processing']['batch_size']
|
||||
|
||||
batch_timeout = self.config['llm'].get('batch_timeout', 300)
|
||||
incremental_save = self.config['batch_processing'].get('incremental_save', False)
|
||||
|
||||
results = []
|
||||
total = len(projects_data)
|
||||
|
||||
self.logger.info(f"="*60)
|
||||
self.logger.info(f"开始批量抽取,共{total}个项目,批次大小{batch_size}")
|
||||
self.logger.info(f"增量保存: {'启用' if incremental_save else '禁用'}")
|
||||
self.logger.info(f"请求超时: {self.config['llm'].get('request_timeout', 120)}秒")
|
||||
self.logger.info(f"批次超时: {batch_timeout}秒")
|
||||
self.logger.info(f"="*60)
|
||||
|
||||
for i in range(0, total, batch_size):
|
||||
batch = projects_data[i:i+batch_size]
|
||||
batch_num = i // batch_size + 1
|
||||
total_batches = (total + batch_size - 1) // batch_size
|
||||
|
||||
self.logger.info(f"-"*60)
|
||||
self.logger.info(f"开始处理批次 {batch_num}/{total_batches},包含{len(batch)}个项目")
|
||||
for item in batch:
|
||||
self.logger.info(f" - {item['project_id']}: {item['project_name']}")
|
||||
|
||||
# 并发处理当前批次
|
||||
batch_tasks = [
|
||||
self.extract_from_remark(
|
||||
item['project_id'],
|
||||
item['project_name'],
|
||||
item['remark_text']
|
||||
)
|
||||
for item in batch
|
||||
]
|
||||
|
||||
try:
|
||||
# 添加批次级别的超时控制
|
||||
batch_results = await asyncio.wait_for(
|
||||
asyncio.gather(*batch_tasks, return_exceptions=True),
|
||||
timeout=batch_timeout
|
||||
)
|
||||
results.extend(batch_results)
|
||||
|
||||
# 统计本批次结果
|
||||
success_count = sum(1 for r in batch_results if r is not None and not isinstance(r, Exception))
|
||||
error_count = len(batch_results) - success_count
|
||||
|
||||
self.logger.info(f"批次 {batch_num}/{total_batches} 完成 - 成功: {success_count}, 失败: {error_count}")
|
||||
|
||||
# 调用进度回调(用于增量保存)
|
||||
if progress_callback:
|
||||
await progress_callback(batch_num, total_batches, results, projects_data[:i+len(batch)])
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self.logger.error(f"批次 {batch_num}/{total_batches} 超时({batch_timeout}秒),部分请求失败")
|
||||
# 将未完成的结果标记为None
|
||||
for item in batch[len(results):]:
|
||||
results.append(None)
|
||||
|
||||
self.logger.info(f"="*60)
|
||||
self.logger.info(f"批量抽取完成,共处理{len(results)}个项目")
|
||||
self.logger.info(f"="*60)
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,486 @@
|
||||
"""
|
||||
DeepSeek实体识别模块
|
||||
使用DeepSeek API进行非遗知识抽取
|
||||
"""
|
||||
|
||||
import yaml
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
except ImportError:
|
||||
print("错误: 请先安装依赖包")
|
||||
print("运行: pip install langchain-deepseek langchain-core")
|
||||
raise
|
||||
|
||||
|
||||
class DeepSeekExtractor:
|
||||
"""基于DeepSeek的知识抽取器"""
|
||||
|
||||
def __init__(self, config_file: str = 'config/ich_config.yaml'):
|
||||
"""
|
||||
初始化抽取器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
# 加载配置
|
||||
self.config = self._load_config(config_file)
|
||||
self.api_key = self._load_api_key()
|
||||
|
||||
# 初始化模型
|
||||
self.model = ChatDeepSeek(
|
||||
model=self.config['api']['model'],
|
||||
api_key=self.api_key,
|
||||
temperature=self.config['api']['temperature'],
|
||||
max_tokens=self.config['api']['max_tokens']
|
||||
)
|
||||
|
||||
# 设置日志
|
||||
self.logger = self._setup_logger()
|
||||
|
||||
# 加载本体配置
|
||||
self.entity_types = self.config['extraction']['entity_types']
|
||||
self.relation_types = self.config['extraction']['relation_types']
|
||||
|
||||
def _setup_logger(self):
|
||||
"""设置日志"""
|
||||
log_dir = Path(self.config['paths']['logs_dir'])
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log_file = log_dir / f"extraction_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, self.config['logging']['level']),
|
||||
format=self.config['logging']['format'],
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
def _load_config(self, config_file: str) -> Dict:
|
||||
"""加载配置"""
|
||||
config_path = Path(config_file)
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_file}")
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def _load_api_key(self) -> str:
|
||||
"""加载API密钥"""
|
||||
api_config_file = Path(self.config['api']['config_file'])
|
||||
if not api_config_file.exists():
|
||||
raise FileNotFoundError(f"API配置文件不存在: {api_config_file}")
|
||||
|
||||
with open(api_config_file, 'r', encoding='utf-8') as f:
|
||||
api_config = yaml.safe_load(f)
|
||||
|
||||
return api_config['deepseek_api_key']
|
||||
|
||||
def extract_json_from_response(self, response_text: str) -> str:
|
||||
"""
|
||||
从API响应中提取JSON内容
|
||||
|
||||
Args:
|
||||
response_text: API响应文本
|
||||
|
||||
Returns:
|
||||
str: 提取的JSON字符串
|
||||
"""
|
||||
# 如果响应包含markdown代码块,提取其中的JSON
|
||||
if '```json' in response_text:
|
||||
start = response_text.find('```json') + 7
|
||||
end = response_text.find('```', start)
|
||||
if start > 6 and end > start:
|
||||
return response_text[start:end].strip()
|
||||
elif '```' in response_text:
|
||||
start = response_text.find('```') + 3
|
||||
end = response_text.find('```', start)
|
||||
if start > 2 and end > start:
|
||||
content = response_text[start:end].strip()
|
||||
if not content.startswith('json'):
|
||||
return content
|
||||
return content[5:].strip() if content.startswith('json') else content.strip()
|
||||
|
||||
# 否则直接返回原始文本
|
||||
return response_text.strip()
|
||||
|
||||
async def extract_entities_from_text(
|
||||
self,
|
||||
text: str,
|
||||
project_name: str = "",
|
||||
max_retries: int = 3
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
从文本中抽取实体
|
||||
|
||||
Args:
|
||||
text: 输入文本
|
||||
project_name: 项目名称(可选)
|
||||
max_retries: 最大重试次数
|
||||
|
||||
Returns:
|
||||
Dict: 抽取结果
|
||||
"""
|
||||
# 构建提示词
|
||||
entity_types_str = "、".join(self.entity_types)
|
||||
|
||||
prompt = f"""
|
||||
你是一位非物质文化遗产领域的专家。请从以下文本中识别非遗相关实体,并提取属性。
|
||||
|
||||
项目名称:{project_name}
|
||||
|
||||
文本内容:
|
||||
{text}
|
||||
|
||||
请识别以下类型的实体:
|
||||
{entity_types_str}
|
||||
|
||||
对于每个实体,请提取以下信息:
|
||||
1. 实体文本
|
||||
2. 实体类型
|
||||
3. 相关属性(如:民族、地点、时间、技艺特点等)
|
||||
|
||||
输出格式(JSON):
|
||||
{{
|
||||
"entities": [
|
||||
{{
|
||||
"text": "实体文本",
|
||||
"type": "实体类型",
|
||||
"attributes": {{
|
||||
"ethnic_group": "民族(如果适用)",
|
||||
"location": "地点(如果适用)",
|
||||
"time_period": "时期(如果适用)",
|
||||
"skill_feature": "技艺特点(如果适用)",
|
||||
"cultural_value": "文化价值(如果适用)"
|
||||
}}
|
||||
}}
|
||||
],
|
||||
"relationships": [
|
||||
{{
|
||||
"from": "实体1",
|
||||
"to": "实体2",
|
||||
"type": "关系类型",
|
||||
"description": "关系描述"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
请确保输出是有效的JSON格式。
|
||||
"""
|
||||
|
||||
# 调用API
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
self.logger.info(f"开始抽取实体 (尝试 {attempt + 1}/{max_retries})")
|
||||
|
||||
messages = [HumanMessage(content=prompt)]
|
||||
response = await self.model.ainvoke(messages)
|
||||
result_text = response.content
|
||||
|
||||
# 提取JSON
|
||||
json_text = self.extract_json_from_response(result_text)
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
result = json.loads(json_text)
|
||||
|
||||
# 添加元数据
|
||||
result['metadata'] = {
|
||||
'project_name': project_name,
|
||||
'extraction_time': datetime.now().isoformat(),
|
||||
'text_length': len(text),
|
||||
'model': self.config['api']['model']
|
||||
}
|
||||
|
||||
self.logger.info(f"成功抽取 {len(result.get('entities', []))} 个实体")
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.warning(f"JSON解析失败: {str(e)}")
|
||||
self.logger.debug(f"响应文本: {result_text[:500]}")
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
|
||||
if attempt == max_retries - 1:
|
||||
return None
|
||||
|
||||
# 等待后重试
|
||||
await asyncio.sleep(self.config['processing']['retry_delay'])
|
||||
|
||||
return None
|
||||
|
||||
async def extract_relationships(
|
||||
self,
|
||||
entities: List[Dict],
|
||||
context: str = "",
|
||||
max_retries: int = 3
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
抽取实体间的关系
|
||||
|
||||
Args:
|
||||
entities: 实体列表
|
||||
context: 上下文文本
|
||||
max_retries: 最大重试次数
|
||||
|
||||
Returns:
|
||||
List[Dict]: 关系列表
|
||||
"""
|
||||
if not entities:
|
||||
return []
|
||||
|
||||
# 构建实体描述
|
||||
entity_descriptions = []
|
||||
for entity in entities:
|
||||
desc = f"- {entity.get('text', '')} ({entity.get('type', '')})"
|
||||
if entity.get('attributes'):
|
||||
attrs = ", ".join([f"{k}={v}" for k, v in entity['attributes'].items() if v])
|
||||
if attrs:
|
||||
desc += f" [{attrs}]"
|
||||
entity_descriptions.append(desc)
|
||||
|
||||
entities_str = "\n".join(entity_descriptions)
|
||||
relations_str = "、".join(self.relation_types)
|
||||
|
||||
prompt = f"""
|
||||
基于以下实体和上下文,识别实体间的关系:
|
||||
|
||||
实体列表:
|
||||
{entities_str}
|
||||
|
||||
上下文:
|
||||
{context}
|
||||
|
||||
可能的关系类型:
|
||||
{relations_str}
|
||||
|
||||
对于每个关系,请提供:
|
||||
1. 头实体(from)
|
||||
2. 尾实体(to)
|
||||
3. 关系类型
|
||||
4. 关系描述
|
||||
|
||||
输出格式(JSON):
|
||||
{{
|
||||
"relationships": [
|
||||
{{
|
||||
"from": "实体1文本",
|
||||
"to": "实体2文本",
|
||||
"type": "关系类型",
|
||||
"description": "关系描述",
|
||||
"confidence": 0.9
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
请确保输出是有效的JSON格式。
|
||||
"""
|
||||
|
||||
# 调用API
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
messages = [HumanMessage(content=prompt)]
|
||||
response = await self.model.ainvoke(messages)
|
||||
result_text = response.content
|
||||
|
||||
# 提取JSON
|
||||
json_text = self.extract_json_from_response(result_text)
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
result = json.loads(json_text)
|
||||
relationships = result.get('relationships', [])
|
||||
|
||||
self.logger.info(f"成功抽取 {len(relationships)} 个关系")
|
||||
return relationships
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.warning(f"JSON解析失败: {str(e)}")
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
|
||||
if attempt == max_retries - 1:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(self.config['processing']['retry_delay'])
|
||||
|
||||
return None
|
||||
|
||||
async def enrich_project_entity(
|
||||
self,
|
||||
project_data: Dict[str, Any],
|
||||
max_retries: int = 3
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
丰富非遗项目实体信息
|
||||
|
||||
Args:
|
||||
project_data: 项目数据
|
||||
max_retries: 最大重试次数
|
||||
|
||||
Returns:
|
||||
Dict: 丰富后的实体信息
|
||||
"""
|
||||
project_name = project_data.get('properties', {}).get('name', '')
|
||||
description = project_data.get('properties', {}).get('description', '')
|
||||
|
||||
prompt = f"""
|
||||
你是一位非物质文化遗产领域的专家。请分析以下非遗项目,提取和丰富实体信息。
|
||||
|
||||
项目名称:{project_name}
|
||||
|
||||
项目描述:
|
||||
{description}
|
||||
|
||||
请提取以下信息:
|
||||
|
||||
1. **民族特色**:是否与特定少数民族相关?(满族、赫哲族、鄂伦春族等)
|
||||
2. **地域特征**:体现哪些黑龙江地域特征?(寒地、冰雪、森林、江河等)
|
||||
3. **技艺特点**:核心技艺特点是什么?
|
||||
4. **文化价值**:有哪些重要的文化价值?
|
||||
5. **传承方式**:如何传承?(家族传承、师徒制度、口传心授等)
|
||||
6. **濒危状况**:是否濒危?原因是什么?
|
||||
7. **保护措施**:有哪些保护措施?
|
||||
|
||||
输出格式(JSON):
|
||||
{{
|
||||
"ethnic_features": ["民族1", "民族2"],
|
||||
"regional_characteristics": ["特征1", "特征2"],
|
||||
"skill_features": ["技艺特点1", "技艺特点2"],
|
||||
"cultural_values": ["价值1", "价值2"],
|
||||
"transmission_methods": ["方式1", "方式2"],
|
||||
"endangerment_status": "濒危状况描述",
|
||||
"protection_measures": ["措施1", "措施2"],
|
||||
"summary": "项目总结(50字以内)"
|
||||
}}
|
||||
|
||||
请确保输出是有效的JSON格式。
|
||||
"""
|
||||
|
||||
# 调用API
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
messages = [HumanMessage(content=prompt)]
|
||||
response = await self.model.ainvoke(messages)
|
||||
result_text = response.content
|
||||
|
||||
# 提取JSON
|
||||
json_text = self.extract_json_from_response(result_text)
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
result = json.loads(json_text)
|
||||
|
||||
self.logger.info(f"成功丰富项目实体信息: {project_name}")
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.warning(f"JSON解析失败: {str(e)}")
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
|
||||
if attempt == max_retries - 1:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(self.config['processing']['retry_delay'])
|
||||
|
||||
return None
|
||||
|
||||
async def batch_extract(
|
||||
self,
|
||||
items: List[Dict[str, Any]],
|
||||
concurrent: int = 5
|
||||
) -> List[Optional[Dict[str, Any]]]:
|
||||
"""
|
||||
批量抽取
|
||||
|
||||
Args:
|
||||
items: 待处理项目列表
|
||||
concurrent: 并发数
|
||||
|
||||
Returns:
|
||||
List[Dict]: 抽取结果列表
|
||||
"""
|
||||
results = []
|
||||
batch_size = concurrent
|
||||
|
||||
for i in range(0, len(items), batch_size):
|
||||
batch = items[i:i + batch_size]
|
||||
self.logger.info(f"处理批次 {i//batch_size + 1}: {len(batch)} 个项目")
|
||||
|
||||
# 并发处理
|
||||
tasks = []
|
||||
for item in batch:
|
||||
project_name = item.get('properties', {}).get('name', '')
|
||||
description = item.get('properties', {}).get('description', '')
|
||||
|
||||
if description:
|
||||
task = self.enrich_project_entity(item)
|
||||
tasks.append(task)
|
||||
else:
|
||||
tasks.append(asyncio.sleep(0)) # 占位任务
|
||||
|
||||
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
results.extend(batch_results)
|
||||
|
||||
# 显示进度
|
||||
completed = min(i + batch_size, len(items))
|
||||
self.logger.info(f"进度: {completed}/{len(items)}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 测试实体抽取"""
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
# 创建抽取器
|
||||
extractor = DeepSeekExtractor('config/ich_config.yaml')
|
||||
|
||||
# 测试文本
|
||||
test_text = """
|
||||
桦树皮制作技艺是鄂伦春族的传统手工艺,利用桦树皮制作各种生活用品。
|
||||
传承人莫桂树2008年被评为国家级传承人。这项技艺体现了鄂伦春族对自然资源的
|
||||
巧妙利用,具有鲜明的渔猎文化特色。
|
||||
"""
|
||||
|
||||
# 测试实体抽取
|
||||
print("="*60)
|
||||
print("测试实体抽取")
|
||||
print("="*60)
|
||||
|
||||
async def test():
|
||||
result = await extractor.extract_entities_from_text(
|
||||
text=test_text,
|
||||
project_name="桦树皮制作技艺"
|
||||
)
|
||||
|
||||
if result:
|
||||
print("\n抽取结果:")
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print("抽取失败")
|
||||
|
||||
asyncio.run(test())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
黑龙江省非物质文化遗产知识图谱构建 - 主处理脚本
|
||||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from knowledge_extraction.llm_extractor import DeepSeekExtractor
|
||||
|
||||
|
||||
class ICKnowledgeGraphBuilder:
|
||||
"""非遗知识图谱构建器"""
|
||||
|
||||
def __init__(self, config_file: str = 'config/ich_config.yaml'):
|
||||
"""初始化构建器"""
|
||||
# 加载配置
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
# 设置路径
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
self.data_dir = self.project_root / self.config['paths']['data_dir']
|
||||
self.output_dir = self.project_root / self.config['paths']['output_dir']
|
||||
self.logs_dir = self.project_root / self.config['paths']['logs_dir']
|
||||
|
||||
# 创建目录
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置日志
|
||||
self.logger = self._setup_logger()
|
||||
|
||||
# 初始化DeepSeek抽取器
|
||||
self.extractor = DeepSeekExtractor(config_file)
|
||||
|
||||
# 加载进度
|
||||
self.progress_file = self.output_dir / 'progress.json'
|
||||
self.progress = self._load_progress()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""设置日志"""
|
||||
log_file = self.logs_dir / f"build_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, self.config['logging']['level']),
|
||||
format=self.config['logging']['format'],
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
def _load_progress(self) -> Dict:
|
||||
"""加载进度"""
|
||||
if self.progress_file.exists():
|
||||
with open(self.progress_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {
|
||||
'total': 0,
|
||||
'processed': [],
|
||||
'successful': [],
|
||||
'failed': [],
|
||||
'last_update': None
|
||||
}
|
||||
|
||||
def _save_progress(self):
|
||||
"""保存进度"""
|
||||
self.progress['last_update'] = datetime.now().isoformat()
|
||||
with open(self.progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.progress, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def load_nodes(self) -> List[Dict[str, Any]]:
|
||||
"""加载节点数据"""
|
||||
nodes_file = self.output_dir / 'nodes.json'
|
||||
if not nodes_file.exists():
|
||||
self.logger.error(f"节点文件不存在: {nodes_file}")
|
||||
return []
|
||||
|
||||
with open(nodes_file, 'r', encoding='utf-8') as f:
|
||||
nodes = json.load(f)
|
||||
|
||||
self.logger.info(f"加载了 {len(nodes)} 个节点")
|
||||
self.progress['total'] = len(nodes)
|
||||
|
||||
return nodes
|
||||
|
||||
async def enrich_single_node(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""丰富单个节点"""
|
||||
node_id = node.get('id')
|
||||
project_name = node.get('properties', {}).get('name', '')
|
||||
|
||||
self.logger.info(f"开始处理节点: {node_id} - {project_name}")
|
||||
|
||||
try:
|
||||
# 调用DeepSeek丰富信息
|
||||
enriched_data = await self.extractor.enrich_project_entity(node)
|
||||
|
||||
if enriched_data:
|
||||
# 合并到原节点
|
||||
node['properties']['enriched_data'] = enriched_data
|
||||
node['properties']['enriched_at'] = datetime.now().isoformat()
|
||||
|
||||
self.logger.info(f"成功丰富节点: {node_id}")
|
||||
return {
|
||||
'node_id': node_id,
|
||||
'status': 'success',
|
||||
'enriched_data': enriched_data
|
||||
}
|
||||
else:
|
||||
self.logger.warning(f"丰富节点失败(无返回数据): {node_id}")
|
||||
return {
|
||||
'node_id': node_id,
|
||||
'status': 'failed',
|
||||
'error': 'No data returned'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"丰富节点失败: {node_id} - {str(e)}")
|
||||
return {
|
||||
'node_id': node_id,
|
||||
'status': 'failed',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def batch_enrich_nodes(
|
||||
self,
|
||||
nodes: List[Dict[str, Any]],
|
||||
max_nodes: int = None,
|
||||
concurrent: int = 5
|
||||
):
|
||||
"""批量丰富节点"""
|
||||
# 过滤已处理的节点
|
||||
pending_nodes = [
|
||||
node for node in nodes
|
||||
if node.get('id') not in self.progress['processed']
|
||||
]
|
||||
|
||||
# 限制处理数量(用于测试)
|
||||
if max_nodes:
|
||||
pending_nodes = pending_nodes[:max_nodes]
|
||||
|
||||
self.logger.info(f"开始批量处理: {len(pending_nodes)} 个节点待处理")
|
||||
|
||||
# 分批处理
|
||||
batch_size = concurrent
|
||||
for i in range(0, len(pending_nodes), batch_size):
|
||||
batch = pending_nodes[i:i + batch_size]
|
||||
batch_num = i // batch_size + 1
|
||||
total_batches = (len(pending_nodes) + batch_size - 1) // batch_size
|
||||
|
||||
self.logger.info(f"处理批次 {batch_num}/{total_batches}: {len(batch)} 个节点")
|
||||
|
||||
# 并发处理
|
||||
tasks = [self.enrich_single_node(node) for node in batch]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 更新进度
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
self.logger.error(f"处理异常: {str(result)}")
|
||||
continue
|
||||
|
||||
node_id = result.get('node_id')
|
||||
status = result.get('status')
|
||||
|
||||
if status == 'success':
|
||||
self.progress['successful'].append(node_id)
|
||||
else:
|
||||
self.progress['failed'].append(node_id)
|
||||
|
||||
self.progress['processed'].append(node_id)
|
||||
|
||||
# 保存进度
|
||||
self._save_progress()
|
||||
|
||||
# 显示进度
|
||||
self._print_progress()
|
||||
|
||||
def _print_progress(self):
|
||||
"""打印进度"""
|
||||
total = self.progress['total']
|
||||
processed = len(self.progress['processed'])
|
||||
successful = len(self.progress['successful'])
|
||||
failed = len(self.progress['failed'])
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("处理进度")
|
||||
print("="*60)
|
||||
print(f"总节点数: {total}")
|
||||
print(f"已处理: {processed} ({processed/total*100:.1f}%)")
|
||||
print(f"成功: {successful}")
|
||||
print(f"失败: {failed}")
|
||||
print(f"成功率: {successful/processed*100:.1f}%" if processed > 0 else "成功率: N/A")
|
||||
print(f"最后更新: {self.progress['last_update']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
def save_enriched_nodes(self, nodes: List[Dict[str, Any]]):
|
||||
"""保存丰富后的节点"""
|
||||
output_file = self.output_dir / 'nodes_enriched.json'
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(nodes, f, indent=2, ensure_ascii=False)
|
||||
|
||||
self.logger.info(f"丰富后的节点已保存到: {output_file}")
|
||||
|
||||
# 统计
|
||||
enriched_count = sum(
|
||||
1 for node in nodes
|
||||
if 'enriched_data' in node.get('properties', {})
|
||||
)
|
||||
|
||||
print(f"\n统计信息:")
|
||||
print(f"总节点数: {len(nodes)}")
|
||||
print(f"已丰富节点数: {enriched_count}")
|
||||
print(f"丰富率: {enriched_count/len(nodes)*100:.1f}%")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='黑龙江非遗知识图谱构建')
|
||||
parser.add_argument('--config', default='config/ich_config.yaml', help='配置文件路径')
|
||||
parser.add_argument('--max-nodes', type=int, help='最大处理节点数(用于测试)')
|
||||
parser.add_argument('--status', action='store_true', help='查看进度')
|
||||
parser.add_argument('--resume', action='store_true', help='断点续传')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建构建器
|
||||
builder = ICKnowledgeGraphBuilder(args.config)
|
||||
|
||||
if args.status:
|
||||
# 显示进度
|
||||
builder._print_progress()
|
||||
return
|
||||
|
||||
# 加载节点数据
|
||||
print("加载节点数据...")
|
||||
nodes = builder.load_nodes()
|
||||
|
||||
if not nodes:
|
||||
print("没有找到节点数据")
|
||||
return
|
||||
|
||||
# 批量处理
|
||||
print("开始批量处理节点...")
|
||||
asyncio.run(builder.batch_enrich_nodes(
|
||||
nodes,
|
||||
max_nodes=args.max_nodes,
|
||||
concurrent=builder.config['processing']['concurrent_requests']
|
||||
))
|
||||
|
||||
# 加载并保存丰富后的节点
|
||||
print("保存处理结果...")
|
||||
nodes_file = builder.output_dir / 'nodes.json'
|
||||
with open(nodes_file, 'r', encoding='utf-8') as f:
|
||||
all_nodes = json.load(f)
|
||||
|
||||
builder.save_enriched_nodes(all_nodes)
|
||||
|
||||
print("\n处理完成!")
|
||||
builder._print_progress()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user