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
@@ -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