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
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()