kgrag/kgrag_migration/split_by_ship.py
2026-07-29 18:10:19 +08:00

332 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
按舰船号拆分 kg_snapshot.json、kg_output/、files/ 等目录
输出结果放在 OUTPUT_BASE_DIR 下,每艘舰一个子目录
文件名规则与舰船的对应关系只需修改下方「变量配置区」,逻辑不用动。
"""
import json
import os
import re
import shutil
import sys
from collections import defaultdict
from typing import Dict, List, Optional, Set
# 从统一配置文件读取路径
sys.path.insert(0, os.path.dirname(__file__))
from migration_config import DST_CODE_DIR, DST_FILES_DIR
# ===================== 变量配置区(路径已从 migration_config.py 读取) ========================
# 输入路径
SNAPSHOT_PATH = os.path.join(DST_CODE_DIR, "kg_snapshot.json") # 全量快照
KG_OUTPUT_DIR = os.path.join(DST_CODE_DIR, "kg_output") # 抽取结果 JSON 目录
FILES_DIR = DST_FILES_DIR # 原始文档目录
UPLOADS_V1_DIR = os.path.join(DST_FILES_DIR, "uploads_v1") # 高亮 PDF 目录
# 输出根目录(每艘舰一个子目录)
OUTPUT_BASE_DIR = os.path.join(DST_CODE_DIR, "migration_split")
# --------------------------------------------------------------------------
# 舰船规则表:定义「哪些文件名属于哪艘舰」
#
# 每条规则是一个 dict支持以下匹配方式可以组合任意一条命中即匹配
# prefix : 文件名以此开头(不含路径)
# suffix : 文件名以此结尾(含扩展名)
# contains : 文件名包含此字符串
# regex : 正则表达式,匹配整个文件名(不含路径)
#
# 示例:
# {"prefix": "122-"} → 以 "122-" 开头
# {"prefix": "163-"} → 以 "163-" 开头
# {"contains": "发动机"} → 包含"发动机"
# {"regex": r"^JZ\d+_.+"} → 正则匹配
# --------------------------------------------------------------------------
SHIP_RULES: Dict[str, List[Dict]] = {
"101舰": [
{"prefix": "101-"},
],
"122舰": [
{"prefix": "122-"},
],
"163舰": [
{"prefix": "163-"},
],
# 新增舰船示例(取消注释并修改):
# "200舰": [
# {"prefix": "200-"},
# {"contains": "某特殊前缀"},
# ],
}
# 共享节点(同时属于多艘舰)的处理策略:
# "all" : 每艘涉及的舰都保留这个节点knowledge_source 保持完整)
# "majority" : 归属来源文件更多的那艘舰,其他舰不保留
SHARED_NODE_POLICY = "all"
# ==========================================================================
def match_ship(filename: str) -> Optional[str]:
"""
根据 SHIP_RULES 判断文件名属于哪艘舰。
返回舰船名,或 None没有匹配
第一条命中的规则生效(规则之间互斥时注意顺序)。
"""
bare = os.path.basename(filename)
for ship, rules in SHIP_RULES.items():
for rule in rules:
if "prefix" in rule and bare.startswith(rule["prefix"]):
return ship
if "suffix" in rule and bare.endswith(rule["suffix"]):
return ship
if "contains" in rule and rule["contains"] in bare:
return ship
if "regex" in rule and re.match(rule["regex"], bare):
return ship
return None
# --------------------------------------------------------------------------
# 快照拆分
# --------------------------------------------------------------------------
def _get_node_ships(entities: List[Dict]) -> Dict[str, Set[str]]:
"""返回 {节点名: {归属舰集合}}"""
node_ships: Dict[str, Set[str]] = defaultdict(set)
for e in entities:
name = e.get("properties", {}).get("名称", "")
if not name:
continue
ks_raw = e.get("properties", {}).get("knowledge_source", "")
try:
ks_list = json.loads(ks_raw) if isinstance(ks_raw, str) else (ks_raw or [])
except Exception:
ks_list = []
for ks in ks_list:
ship = match_ship(ks.get("filename", ""))
if ship:
node_ships[name].add(ship)
return node_ships
def split_snapshot(snapshot_path: str, output_base: str):
print(f"\n[快照拆分] 读取 {snapshot_path} ...")
with open(snapshot_path, "r", encoding="utf-8") as f:
data = json.load(f)
entities = data.get("entities", [])
relationships = data.get("relationships", [])
print(f" 总节点: {len(entities)} 总关系: {len(relationships)}")
node_ships = _get_node_ships(entities)
# 统计共享情况
all_ships = set(SHIP_RULES.keys())
stats = defaultdict(int)
for name, ships in node_ships.items():
if len(ships) == 0:
stats["无来源"] += 1
elif len(ships) == 1:
stats[next(iter(ships))] += 1
else:
stats["共享"] += 1
print(f" 节点归属统计: { dict(stats) }")
# 为每艘舰生成节点/关系子集
for ship in SHIP_RULES:
ship_entities = []
for e in entities:
name = e.get("properties", {}).get("名称", "")
ships = node_ships.get(name, set())
if ship in ships:
ship_entities.append(e)
elif not ships and SHARED_NODE_POLICY == "all":
# 无来源文件的节点(如顶层舰艇/系统节点)每艘舰都保留
ship_entities.append(e)
# 只保留两端节点都在本舰集合里的关系
ship_node_names = {e.get("properties", {}).get("名称", "") for e in ship_entities}
ship_rels = [
r for r in relationships
if r.get("from_entity") in ship_node_names
and r.get("to_entity") in ship_node_names
]
out_dir = os.path.join(output_base, ship)
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, "kg_snapshot.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump({"entities": ship_entities, "relationships": ship_rels},
f, ensure_ascii=False, indent=2)
print(f" [{ship}] 节点:{len(ship_entities)} 关系:{len(ship_rels)}{out_path}")
# --------------------------------------------------------------------------
# kg_output 拆分(每个 .json 文件对应一个原始文档)
# --------------------------------------------------------------------------
def split_kg_output(kg_output_dir: str, output_base: str):
if not os.path.isdir(kg_output_dir):
print(f"\n[kg_output 拆分] 目录不存在,跳过: {kg_output_dir}")
return
print(f"\n[kg_output 拆分] 扫描 {kg_output_dir} ...")
files = [f for f in os.listdir(kg_output_dir) if f.endswith(".json")]
print(f"{len(files)} 个文件")
counts = defaultdict(int)
unmatched = []
for fname in files:
# kg_output 文件名格式:原始文件名.json含扩展名如 122-xxx.pdf.json
# 去掉最后的 .json 拿到原始文件名
original = fname[:-5] if fname.endswith(".json") else fname
ship = match_ship(original)
if ship:
dst_dir = os.path.join(output_base, ship, "kg_output")
os.makedirs(dst_dir, exist_ok=True)
shutil.copy2(os.path.join(kg_output_dir, fname),
os.path.join(dst_dir, fname))
counts[ship] += 1
else:
unmatched.append(fname)
for ship, cnt in counts.items():
print(f" [{ship}] {cnt} 个 JSON")
if unmatched:
print(f" [未匹配] {len(unmatched)} 个文件: {unmatched[:5]}")
# --------------------------------------------------------------------------
# files/ 原始文档拆分
# --------------------------------------------------------------------------
def split_files(files_dir: str, output_base: str):
if not os.path.isdir(files_dir):
print(f"\n[files 拆分] 目录不存在,跳过: {files_dir}")
return
print(f"\n[files 拆分] 扫描 {files_dir} ...")
all_files = [f for f in os.listdir(files_dir) if os.path.isfile(os.path.join(files_dir, f))]
print(f"{len(all_files)} 个文件")
counts = defaultdict(int)
unmatched = []
for fname in all_files:
# files/ 里有两种格式:
# UUID_原文件名.pdf → 去掉 UUID 前缀再匹配
# 原文件名.pdf → 直接匹配
bare = fname
# 尝试去掉 UUID 前缀格式8-4-4-4-12 字符的十六进制)
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_(.+)$'
m = re.match(uuid_pattern, fname, re.IGNORECASE)
if m:
bare = m.group(1)
ship = match_ship(bare)
if ship:
dst_dir = os.path.join(output_base, ship, "files")
os.makedirs(dst_dir, exist_ok=True)
shutil.copy2(os.path.join(files_dir, fname),
os.path.join(dst_dir, fname))
counts[ship] += 1
else:
unmatched.append(fname)
for ship, cnt in counts.items():
print(f" [{ship}] {cnt} 个文件")
if unmatched:
print(f" [未匹配] {len(unmatched)} 个: {unmatched[:5]}")
# --------------------------------------------------------------------------
# uploads_v1/ 高亮 PDF 拆分(通过快照里的 knowledge_source 反查归属)
# --------------------------------------------------------------------------
def split_uploads(snapshot_path: str, uploads_dir: str, output_base: str):
if not os.path.isdir(uploads_dir):
print(f"\n[uploads 拆分] 目录不存在,跳过: {uploads_dir}")
return
print(f"\n[uploads 拆分] 通过快照反查高亮 PDF 归属 ...")
with open(snapshot_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 从快照里收集高亮PDF文件名 → 归属舰船
highlight_ship: Dict[str, str] = {}
for e in data.get("entities", []):
ks_raw = e.get("properties", {}).get("knowledge_source", "")
try:
ks_list = json.loads(ks_raw) if isinstance(ks_raw, str) else (ks_raw or [])
except Exception:
continue
for ks in ks_list:
url = ks.get("url", "")
src_file = ks.get("filename", "")
ship = match_ship(src_file)
if not ship or not url:
continue
# URL 格式http://host/upload/<highlight_filename>#page=N
if "/upload/" in url:
highlight_fname = url.split("/upload/")[-1].split("#")[0]
# 共享节点下同一个高亮文件只记录第一个匹配舰船
if highlight_fname not in highlight_ship:
highlight_ship[highlight_fname] = ship
counts = defaultdict(int)
missing = []
for fname, ship in highlight_ship.items():
src = os.path.join(uploads_dir, fname)
if os.path.exists(src):
dst_dir = os.path.join(output_base, ship, "files", "uploads_v1")
os.makedirs(dst_dir, exist_ok=True)
shutil.copy2(src, os.path.join(dst_dir, fname))
counts[ship] += 1
else:
missing.append(fname)
for ship, cnt in counts.items():
print(f" [{ship}] {cnt} 个高亮 PDF")
if missing:
print(f" [本地缺失] {len(missing)} 个(需从服务器 uploads_v1 补全): {missing[:3]}")
# --------------------------------------------------------------------------
# 入口
# --------------------------------------------------------------------------
def main():
print("=" * 50)
print(" kgrag 按舰船号拆分工具")
print(f" 舰船规则: { {k: v for k, v in SHIP_RULES.items()} }")
print(f" 共享节点策略: {SHARED_NODE_POLICY}")
print("=" * 50)
os.makedirs(OUTPUT_BASE_DIR, exist_ok=True)
split_snapshot(SNAPSHOT_PATH, OUTPUT_BASE_DIR)
split_kg_output(KG_OUTPUT_DIR, OUTPUT_BASE_DIR)
split_files(FILES_DIR, OUTPUT_BASE_DIR)
split_uploads(SNAPSHOT_PATH, UPLOADS_V1_DIR, OUTPUT_BASE_DIR)
print("\n" + "=" * 50)
print(f" 完成!输出目录: {OUTPUT_BASE_DIR}")
print(" 每艘舰的子目录结构:")
print(" <舰名>/kg_snapshot.json ← 导入新 Neo4j 用")
print(" <舰名>/kg_output/ ← 抽取结果 JSON")
print(" <舰名>/files/ ← 原始文档")
print(" <舰名>/files/uploads_v1/ ← 高亮 PDF")
print("=" * 50)
if __name__ == "__main__":
main()