248 lines
9.0 KiB
Python
248 lines
9.0 KiB
Python
import json
|
||
import fitz # PyMuPDF
|
||
import os
|
||
import fitz
|
||
from collections import Counter
|
||
import uuid
|
||
from openai import OpenAI
|
||
from kg_build.Prompt import Prompt_compose
|
||
from extract_util import safe_json_loads,highlight_pdf_file,get_content_bbox
|
||
import re
|
||
from typing import List, Dict, Any, Tuple
|
||
from modelsAPI.model_api import OpenaiAPI
|
||
import asyncio # <-- 新增导入
|
||
|
||
import re
|
||
|
||
def is_compose_table_by_first_tr(table_body: str) -> bool:
|
||
if not isinstance(table_body, str):
|
||
return False
|
||
|
||
# 非贪婪匹配第一个 <tr> ... </tr>
|
||
match = re.search(r'<tr[^>]*>(.*?)</tr>', table_body, re.IGNORECASE | re.DOTALL)
|
||
if not match:
|
||
return False
|
||
|
||
first_tr_html = match.group(1)
|
||
|
||
# 去除所有 HTML 标签,只保留纯文本
|
||
text_in_first_tr = re.sub(r'<[^>]+>', '', first_tr_html)
|
||
|
||
# 统一转为小写便于匹配
|
||
text_lower = text_in_first_tr.lower()
|
||
|
||
# 定义多组关键词组合(任意一组满足即可)
|
||
keyword_groups = [
|
||
["序号", "组成部分", "功能", "数量"],
|
||
["序号", "组成部分", "功能"]
|
||
]
|
||
|
||
# 检查是否存在至少一组关键词全部出现在首行中
|
||
for group in keyword_groups:
|
||
if all(kw in text_lower for kw in group):
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
async def get_compose_chunk_bbox_and_removed_data(data):
|
||
"""
|
||
返回:
|
||
- 符合条件的组成表分组(每组 [前前, 前, 当前])
|
||
- 从原始 data 中移除了所有相关记录后的新 data 列表
|
||
"""
|
||
record_map = {item["id"]: item for item in data}
|
||
results = []
|
||
ids_to_remove = set()
|
||
|
||
for ins in data:
|
||
if ins.get("type") == "table":
|
||
table_body = ins.get("table_body", "")
|
||
|
||
# 使用关键词组合判断是否为“组成表”
|
||
if is_compose_table_by_first_tr(table_body):
|
||
current_id = ins["id"]
|
||
prev_ids = [current_id - 2, current_id - 1]
|
||
|
||
# 收集要移除的 ID(前两条 + 当前)
|
||
group_ids = [pid for pid in prev_ids if pid >= 1] + [current_id]
|
||
ids_to_remove.update(group_ids)
|
||
|
||
# 构建分组
|
||
group = [record_map.get(pid) for pid in prev_ids if pid >= 1] + [ins]
|
||
results.append(group)
|
||
|
||
# 过滤原始 data
|
||
filtered_data = [item for item in data if item["id"] not in ids_to_remove]
|
||
|
||
return results, filtered_data
|
||
|
||
|
||
# def openai_chat(query: str, model="Qwen3-32B"):
|
||
# """调用OpenAI API"""
|
||
# try:
|
||
# client = OpenAI(
|
||
# api_key="none",
|
||
# base_url="http://192.168.0.46:59800/v1"
|
||
# )
|
||
|
||
# response = client.chat.completions.create(
|
||
# model=model,
|
||
# messages=[
|
||
# {"role": "user", "content": query},
|
||
# ],
|
||
# temperature=0.1,
|
||
# stream=False,
|
||
# response_format={"type": "json_object"},
|
||
# extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
||
# )
|
||
# return response.choices[0].message.content
|
||
# except Exception as e:
|
||
# print(f"调用OpenAI API时出错: {e}")
|
||
# return None
|
||
|
||
async def extract_compose_table_entity(chunk):
|
||
"""
|
||
提取组成表的设备、零部件实体以及关系
|
||
"""
|
||
print("提取组成表实体和关系")
|
||
|
||
# 建议:将 all_results 改为复数形式以符合习惯
|
||
|
||
|
||
final_prompt = Prompt_compose.format(text=chunk)
|
||
raw_response = await OpenaiAPI.openai_chat_aysnc(final_prompt,timeout=480)
|
||
|
||
if raw_response is None:
|
||
print(f" ⚠️ 切片 API 调用失败")
|
||
|
||
# 尝试解析 JSON
|
||
try:
|
||
cleaned = raw_response.strip()
|
||
if cleaned.startswith("```json"):
|
||
cleaned = cleaned[7:].lstrip()
|
||
if cleaned.endswith("```"):
|
||
cleaned = cleaned[:-3].rstrip()
|
||
|
||
json_result = safe_json_loads(cleaned)
|
||
|
||
# 打印美化版
|
||
# print(json.dumps(json_result, ensure_ascii=False, indent=2))
|
||
|
||
except Exception as e:
|
||
print(f" ⚠️切片 JSON 解析失败: {e}")
|
||
print(f" 原始响应: {repr(raw_response[:500])}")
|
||
json_result = []
|
||
|
||
|
||
# 返回按切片分组的结果列表
|
||
return json_result
|
||
|
||
|
||
async def get_compose_node_relation(input_pdf_path,data,prefix_url = "http://192.168.0.46:59085"):
|
||
for idx, item in enumerate(data, start=1):
|
||
item["id"] = idx
|
||
results, filtered_data =await get_compose_chunk_bbox_and_removed_data(data=data)
|
||
print(f"抽取的组成表总数量为:{len(results)}")
|
||
|
||
|
||
if not results:
|
||
print("未找到符合条件的组成表格,跳过处理。")
|
||
all_entity_results,all_relation_results = [],[]
|
||
else:
|
||
all_entity_results,all_relation_results = [],[] # 用于收集所有成功提取的结果
|
||
|
||
for i, compose in enumerate(results):
|
||
try:
|
||
# 获取内容和高亮信息
|
||
print(f"开始处理第{i+1}个组成表")
|
||
|
||
content, highlight_list = get_content_bbox(compose)
|
||
print("组成表切片内容为:",content)
|
||
print(len(content))
|
||
# 提取实体
|
||
result =await extract_compose_table_entity(content)
|
||
if result is None:
|
||
print(f"第 {i+1} 组:实体提取失败,跳过")
|
||
continue
|
||
|
||
# 安全访问 entities 字段
|
||
entities = result.get("entities", [])
|
||
if not isinstance(entities, list):
|
||
print(f"第 {i+1} 组:entities 字段格式异常,跳过")
|
||
continue
|
||
relationships = result.get("relationships", [])
|
||
|
||
# 为每个 entity 添加 knowledge_source
|
||
highlight_filename, most_common_page,filename = highlight_pdf_file(input_pdf_path,highlight_list)
|
||
url = f"{prefix_url}/upload/{highlight_filename}#page={most_common_page}"
|
||
# for entity in entities:
|
||
# knowledge_source = [{
|
||
# "filename": filename,
|
||
# "info": content,
|
||
# "url": url
|
||
# }]
|
||
# # 确保 properties 存在
|
||
# if "properties" not in entity or not isinstance(entity["properties"], dict):
|
||
# entity["properties"] = {}
|
||
# entity["properties"]["knowledge_source"] = knowledge_source
|
||
for entity in entities:
|
||
# 构造原始数据对象 (List of Dict)
|
||
source_data_list = [{
|
||
"filename": filename,
|
||
"info": content,
|
||
"url": url
|
||
}]
|
||
|
||
# 2. 【关键修改】将列表序列化为 JSON 字符串
|
||
# ensure_ascii=False 确保中文内容不会被转义成 \uXXXX
|
||
knowledge_source_str = json.dumps(source_data_list, ensure_ascii=False)
|
||
|
||
# 确保 properties 存在且是字典
|
||
if "properties" not in entity or not isinstance(entity["properties"], dict):
|
||
entity["properties"] = {}
|
||
|
||
# 3. 赋值字符串而不是对象列表
|
||
entity["properties"]["knowledge_source"] = knowledge_source_str
|
||
# 更新 result 中的 entities
|
||
all_entity_results.extend(entities)
|
||
all_relation_results.extend(relationships)
|
||
|
||
except Exception as e:
|
||
print(f"处理第 {i+1} 组时发生未预期错误: {e}")
|
||
continue # 跳过当前组,继续下一轮
|
||
return all_entity_results,all_relation_results
|
||
|
||
# if __name__ == "__main__":
|
||
# file_path = '/storage01/home/hdf/project/wxkgrag/kgextract/output/output1/122-06A0014-B01001_发动机-维修手册_content_list.json'
|
||
# input_pdf_path = "/storage01/home/hdf/project/agent/测试.pdf"
|
||
# output_json_path = '/storage01/home/hdf/project/wxkgrag/kgextract/output/output2/组成表抽取结果.json' # 你可以自定义路径
|
||
|
||
# try:
|
||
# with open(file_path, 'r', encoding='utf-8') as file:
|
||
# data = json.load(file)
|
||
# except Exception as e:
|
||
# print(f"读取 JSON 文件时出错:{e}")
|
||
# data = []
|
||
# all_entity_results, all_relation_results = asyncio.run(
|
||
# get_compose_node_relation(input_pdf_path, data=data)
|
||
# )
|
||
|
||
# length_entity = len(all_entity_results)
|
||
# length_relation = len(all_relation_results)
|
||
|
||
# print(f"抽取{length_entity}个实体,抽取{length_relation}个关系")
|
||
# result = {
|
||
# "entities": all_entity_results,
|
||
# "relations": all_relation_results
|
||
# }
|
||
|
||
# # 保存为 JSON 文件
|
||
# try:
|
||
# with open(output_json_path, 'w', encoding='utf-8') as f:
|
||
# json.dump(result, f, ensure_ascii=False, indent=4)
|
||
# print(f"结果已保存至:{output_json_path}")
|
||
# except Exception as e:
|
||
# print(f"保存 JSON 文件时出错:{e}")
|
||
|