feat(文档解析): 新增Word文档图片解析支持并上传至MinIO
新增对Word文档中图片的解析支持,能够提取文档中的图片并上传至MinIO存储,同时生成包含图片链接的Markdown格式文本。已支持Docx格式的Word文档处理,完善了文档解析功能。
This commit is contained in:
parent
0ff771dc19
commit
18d46ca04a
@ -11,7 +11,7 @@
|
||||
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
|
||||
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
|
||||
- 集成 neo4j mcp (或者自己构建工具)
|
||||
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接
|
||||
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接(还有word等)
|
||||
|
||||
### Bugs
|
||||
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
|
||||
@ -28,6 +28,7 @@
|
||||
- 新增基于知识库文件生成示例问题功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425))
|
||||
- 新增知识库支持文件夹/压缩包上传的功能([#335](https://github.com/xerrors/Yuxi-Know/pull/335#issuecomment-3530976425))
|
||||
- 新增自定义模型支持、新增 dashscope rerank/embeddings 模型的支持
|
||||
- 新增文档解析的图片支持,已支持 MinerU Officical、Docs、Markdown Zip格式
|
||||
|
||||
### 修复
|
||||
- 修复重排序模型实际未生效的问题
|
||||
|
||||
@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from langchain_community.document_loaders import (
|
||||
@ -72,6 +73,85 @@ def _extract_word_text(file_path: Path) -> str:
|
||||
raise ValueError(f"无法解析 Word 文档: {file_path.name}") from unstructured_error
|
||||
|
||||
|
||||
def _extract_docx_markdown_with_images(file_path: Path, params: dict | None = None) -> str:
|
||||
params = params or {}
|
||||
db_id = params.get("db_id") or "word-docs"
|
||||
minio_client = get_minio_client()
|
||||
bucket_name = "kb-images"
|
||||
minio_client.ensure_bucket_exists(bucket_name)
|
||||
file_id = hashstr(file_path.name, length=16)
|
||||
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
rels_path = "word/_rels/document.xml.rels"
|
||||
rid_to_target: dict[str, str] = {}
|
||||
try:
|
||||
rels_xml = zf.read(rels_path).decode("utf-8")
|
||||
rels_root = ET.fromstring(rels_xml)
|
||||
for rel in list(rels_root):
|
||||
rid = rel.attrib.get("Id")
|
||||
target = rel.attrib.get("Target")
|
||||
rtype = rel.attrib.get("Type")
|
||||
if rid and target and rtype and rtype.endswith("/image"):
|
||||
rid_to_target[rid] = target
|
||||
except Exception:
|
||||
rid_to_target = {}
|
||||
|
||||
doc_xml = zf.read("word/document.xml").decode("utf-8")
|
||||
ns = {
|
||||
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
}
|
||||
root = ET.fromstring(doc_xml)
|
||||
md_lines: list[str] = []
|
||||
|
||||
for p in root.findall(".//w:p", ns):
|
||||
texts = [t.text or "" for t in p.findall(".//w:t", ns)]
|
||||
para_text = "".join(texts).strip()
|
||||
image_urls: list[tuple[str, str]] = []
|
||||
|
||||
for blip in p.findall(".//a:blip", ns):
|
||||
rid = blip.attrib.get(f"{{{ns['r']}}}embed")
|
||||
if not rid:
|
||||
continue
|
||||
target = rid_to_target.get(rid)
|
||||
if not target:
|
||||
continue
|
||||
media_path = target if target.startswith("word/") else f"word/{target}"
|
||||
try:
|
||||
data = zf.read(media_path)
|
||||
object_name = f"{db_id}/{file_id}/images/{Path(target).name}"
|
||||
suffix = Path(target).suffix.lower()
|
||||
content_type = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
}.get(suffix, "image/jpeg")
|
||||
result = minio_client.upload_file(
|
||||
bucket_name=bucket_name,
|
||||
object_name=object_name,
|
||||
data=data,
|
||||
content_type=content_type,
|
||||
)
|
||||
image_urls.append((Path(target).name, result.url))
|
||||
except Exception as e:
|
||||
logger.error(f"上传图片失败 {Path(target).name}: {e}")
|
||||
continue
|
||||
|
||||
line = para_text
|
||||
for name, url in image_urls:
|
||||
line = f"{line}\n"
|
||||
if line:
|
||||
md_lines.append(line)
|
||||
|
||||
return "\n\n".join(md_lines)
|
||||
|
||||
|
||||
def chunk_with_parser(file_path, params=None):
|
||||
"""
|
||||
使用文件解析器将文件切分成固定大小的块
|
||||
@ -294,8 +374,11 @@ async def process_file_to_markdown(file_path: str, params: dict | None = None) -
|
||||
content = f.read()
|
||||
return f"# {file_path_obj.name}\n\n{content}"
|
||||
|
||||
elif file_ext in [".doc", ".docx"]:
|
||||
# 处理 Word 文档
|
||||
elif file_ext == ".docx":
|
||||
text = _extract_docx_markdown_with_images(file_path_obj, params=params)
|
||||
return f"# {file_path_obj.name}\n\n" + text
|
||||
|
||||
elif file_ext == ".doc":
|
||||
text = _extract_word_text(file_path_obj)
|
||||
return f"# {file_path_obj.name}\n\n{text}"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user