init: KG_ICH 项目初始化

- data/: 非遗地理编码数据(GIS shapefile + CSV)
- dofile/kg_project/: 知识图谱构建代码(纳入主仓库)
- dofile/visulization/: 可视化数据与路线图
- officefile/: 文献、草稿、bib 文档
- officefile/latex/: Overleaf 同步目录(独立管理,不纳入)
- output/: 输出目录
- logs/: 日志目录
This commit is contained in:
2026-05-30 00:52:36 +08:00
commit 834cad729f
44 changed files with 8586 additions and 0 deletions
+57
View File
@@ -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/
+155
View File
@@ -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()
+203
View File
@@ -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()
+38
View File
@@ -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())
+241
View File
@@ -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())
+375
View File
@@ -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()
+438
View File
@@ -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()
+382
View File
@@ -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)
View File
@@ -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()
+276
View File
@@ -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()