ForcePilot/backend/package/yuxi/knowledge/parser/pp_structure_v3.py
2026-05-29 22:19:58 +08:00

275 lines
10 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.

"""
PP-Structure-V3 文档解析器
使用 PP-Structure-V3 进行文档版面解析和内容提取
"""
import base64
import os
import time
from pathlib import Path
from typing import Any
import requests
from yuxi.knowledge.parser.base import BaseDocumentProcessor, DocumentParserException
from yuxi.utils import logger
class PPStructureV3Parser(BaseDocumentProcessor):
"""PP-Structure-V3 文档解析器 - 使用 PP-Structure-V3 进行版面解析"""
def __init__(self, server_url: str | None = None):
self.server_url = server_url or os.getenv("PADDLEX_URI") or "http://localhost:8080"
self.base_url = self.server_url.rstrip("/")
self.endpoint = f"{self.base_url}/layout-parsing"
def get_service_name(self) -> str:
return "pp_structure_v3_ocr"
def get_supported_extensions(self) -> list[str]:
"""PP-Structure-V3 支持 PDF 和多种图像格式"""
return [".pdf", ".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif"]
def _encode_file_to_base64(self, file_path: str) -> str:
"""将文件编码为Base64"""
with open(file_path, "rb") as file:
encoded = base64.b64encode(file.read()).decode("utf-8")
return encoded
def _process_file_input(self, file_input: str) -> str:
"""处理文件输入本地文件路径、URL或Base64内容"""
# 检查是否为本地文件路径
if os.path.exists(file_input):
logger.info(f"📁 检测到本地文件: {file_input}")
logger.info(f"📏 文件大小: {os.path.getsize(file_input) / 1024 / 1024:.2f} MB")
return self._encode_file_to_base64(file_input)
# 检查是否为URL
elif file_input.startswith(("http://", "https://")):
logger.info(f"🌐 检测到URL: {file_input}")
return file_input
# 否则假设为Base64编码内容
else:
logger.info(f"📝 假设为Base64编码内容长度: {len(file_input)} 字符")
return file_input
def _call_layout_api(
self,
file_input: str,
file_type: int | None = None,
use_table_recognition: bool = True,
use_formula_recognition: bool = True,
use_seal_recognition: bool = False,
**kwargs,
) -> dict[str, Any]:
"""调用PP-Structure-V3版面解析API"""
# 处理文件输入
processed_file_input = self._process_file_input(file_input)
payload = {"file": processed_file_input}
# 添加核心参数
optional_params = {
"fileType": file_type,
"useTableRecognition": use_table_recognition,
"useFormulaRecognition": use_formula_recognition,
"useSealRecognition": use_seal_recognition,
}
# 添加非空参数
for key, value in optional_params.items():
if value is not None:
payload[key] = value
# 添加其他kwargs参数
for key, value in kwargs.items():
if value is not None:
payload[key] = value
response = requests.post(self.endpoint, json=payload, headers={"Content-Type": "application/json"}, timeout=300)
if response.status_code == 200:
return response.json()
else:
error_msg = f"PP-Structure-V3 API请求失败: {response.status_code}"
try:
error_result = response.json()
raise DocumentParserException(f"{error_msg}: {error_result}", self.get_service_name(), "api_error")
except Exception:
raise DocumentParserException(f"{error_msg}: {response.text}", self.get_service_name(), "api_error")
def _parse_api_result(self, api_result: dict[str, Any], file_path: str) -> dict[str, Any]:
"""解析API返回结果"""
# 基本信息
parsed_result = {
"success": True,
"file_path": file_path,
"file_name": os.path.basename(file_path),
"log_id": api_result.get("logId"),
"total_pages": 0,
"pages": [],
"full_text": "",
"summary": {},
}
result_data = api_result.get("result", {})
layout_results = result_data.get("layoutParsingResults", [])
# 数据信息
parsed_result["total_pages"] = len(layout_results)
# 统计信息
total_tables = 0
total_formulas = 0
all_text_content = []
# 解析每页结果
for page_result in layout_results:
# Markdown内容
if "markdown" in page_result:
markdown = page_result["markdown"]
if markdown.get("text"):
all_text_content.append(markdown["text"])
# 详细识别结果
if "prunedResult" in page_result:
pruned = page_result["prunedResult"]
# 表格识别
table_result = pruned.get("table_result", [])
total_tables += len(table_result)
# 公式识别
formula_result = pruned.get("formula_result", [])
total_formulas += len(formula_result)
# 汇总全文内容
parsed_result["full_text"] = "\n\n".join(all_text_content)
# 汇总统计信息
parsed_result["summary"] = {
"total_tables": total_tables,
"total_formulas": total_formulas,
"total_characters": len(parsed_result["full_text"]),
}
return parsed_result
def check_health(self) -> dict:
"""检查 PP-Structure-V3 服务健康状态"""
try:
response = requests.get(f"{self.base_url}/health", timeout=5)
if response.status_code == 200:
return {
"status": "healthy",
"message": "PP-Structure-V3 服务运行正常",
"details": {"server_url": self.server_url},
}
else:
return {
"status": "unhealthy",
"message": f"PP-Structure-V3 服务响应异常: {response.status_code}",
"details": {"server_url": self.server_url},
}
except requests.exceptions.ConnectionError:
return {
"status": "unavailable",
"message": "PP-Structure-V3 服务无法连接,请检查服务是否启动",
"details": {"server_url": self.server_url},
}
except requests.exceptions.Timeout:
return {
"status": "timeout",
"message": "PP-Structure-V3 服务连接超时",
"details": {"server_url": self.server_url},
}
except Exception as e:
return {
"status": "error",
"message": f"PP-Structure-V3 健康检查失败: {str(e)}",
"details": {"server_url": self.server_url, "error": str(e)},
}
def process_file(self, file_path: str, params: dict | None = None) -> str:
"""
使用 PP-Structure-V3 处理文档
Args:
file_path: 文件路径
params: 处理参数
- use_table_recognition: 启用表格识别 (默认: True)
- use_formula_recognition: 启用公式识别 (默认: True)
- use_seal_recognition: 启用印章识别 (默认: False)
Returns:
str: 提取的 Markdown 文本
"""
if not os.path.exists(file_path):
raise DocumentParserException(f"文件不存在: {file_path}", self.get_service_name(), "file_not_found")
file_ext = Path(file_path).suffix.lower()
if not self.supports_file_type(file_ext):
raise DocumentParserException(
f"不支持的文件类型: {file_ext}", self.get_service_name(), "unsupported_file_type"
)
# 先检查服务健康状态
health = self.check_health()
if health["status"] != "healthy":
raise DocumentParserException(
f"PP-Structure-V3 服务不可用: {health['message']}", self.get_service_name(), health["status"]
)
try:
start_time = time.time()
params = params or {}
# 判断文件类型
file_type = 0 if file_ext == ".pdf" else 1
logger.info(f"PP-Structure-V3 开始处理: {os.path.basename(file_path)}")
# 调用API
api_result = self._call_layout_api(
file_input=file_path,
file_type=file_type,
use_table_recognition=params.get("use_table_recognition", True),
use_formula_recognition=params.get("use_formula_recognition", True),
use_seal_recognition=params.get("use_seal_recognition", False),
)
# 检查API调用是否成功
if api_result.get("errorCode") != 0:
raise DocumentParserException(
f"PP-Structure-V3 API错误: {api_result.get('errorMsg', '未知错误')}",
self.get_service_name(),
"api_error",
)
# 解析结果
result = self._parse_api_result(api_result, file_path)
text = result.get("full_text", "")
processing_time = time.time() - start_time
logger.info(
f"PP-Structure-V3 处理成功: {os.path.basename(file_path)} - {len(text)} 字符 ({processing_time:.2f}s)"
)
# 记录统计信息
summary = result.get("summary", {})
if summary:
logger.info(f" 统计: {summary.get('total_tables', 0)} 表格, {summary.get('total_formulas', 0)} 公式")
return text
except DocumentParserException:
raise
except Exception as e:
processing_time = time.time() - start_time
error_msg = f"PP-Structure-V3 处理失败: {str(e)}"
logger.error(f"{error_msg} ({processing_time:.2f}s)")
raise DocumentParserException(error_msg, self.get_service_name(), "processing_failed")