avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
  • 首页
主页 18 文档解析
文章

18 文档解析

发表于 4天前 更新于 4天前
作者 Administrator
32~42 分钟 阅读

文档解析

文档解析的核心是把 PDF、Word、Markdown、TXT 等不同格式统一转换成可处理的文本,同时尽量保留页码、标题、来源等元数据。

[TOC]

1. 学完本节要达到什么程度

前面已经完成上传。

上传后,我们有:

documentId  
originalName  
storagePath  
contentType  
fileSize  
status=UPLOADED  

本节要做的是:

读取原始文件  
 -> 根据文件类型选择解析器  
 -> 抽取文本  
 -> 保留元数据  
 -> 写入 rag_document_text -> 更新文档状态为 PARSED  

先不要清洗。

先把“能解析出来”做好。

2. 解析和清洗的区别

解析是把文件格式转成文本。

比如:

PDF -> 文本  
DOCX -> 文本  
Markdown -> 文本  
TXT -> 文本  

清洗是处理文本质量。

比如:

去空行  
去页眉页脚  
修复换行  
保留标题层级  
删除重复页码  

两者不要混在一起。

本节做解析。

下一节做清洗。

3. 支持格式

第一版支持:

.pdf  
.docx  
.md  
.txt  

每种格式的特点:

PDF:可能有页码,可能有复杂排版  
DOCX:段落结构较清楚  
Markdown:标题结构清楚  
TXT:最简单,但编码可能有问题  

扫描版 PDF 暂时不支持。

扫描版需要 OCR,复杂度会高很多。

4. 解析结果结构

建议统一成这样的结构:

from pydantic import BaseModel  
  
  
class ParsedDocument(BaseModel):  
 document_id: str text: str metadata: dict```  
  
metadata 可以放:  
  
```text  
parser  
source_path  
file_type  
page_count  
encoding  
title  

如果能按页解析,可以保留页面信息:

class ParsedPage(BaseModel):  
 page_number: int text: str metadata: dict```  
  
第一版可以先存全文。  
  
后面切片时再进一步保留页码。  
  
## 5. Python 解析服务  
  
文档解析更适合放 Python。  
  
原因是 Python 生态里解析库更多:  
  
```text  
pypdf  
pymupdf  
python-docx  
docx2txt  
markdown  
langchain-community loaders  
unstructured  

在本路线里,可以由 Java 上传文件,Python 负责解析:

Java 上传并保存文件  
 -> Java 调 Python parse 接口  
 -> Python 读取 storagePath -> Python 返回解析文本  
 -> Java 写入数据库  

Java 调 Python 解析时序图:

sequenceDiagram  
 participant J as Spring Boot participant DB as PostgreSQL participant P as FastAPI 解析服务  
 participant FS as 文件存储  
  
 J->>DB: 读取 documentId 对应的 storagePath J->>P: POST /api/parse,传 documentId 和 storagePath P->>FS: 读取原始文件  
 P->>P: 按文件类型选择解析器  
 P-->>J: 返回 text 和 metadata J->>DB: 写入 rag_document_text J->>DB: 更新文档状态为 PARSED  

也可以 Python 直接写数据库。

学习阶段建议先让 Java 控制入库,Python 只做解析能力。

6. FastAPI 解析接口

Python 接口:

from pathlib import Path  
  
from fastapi import FastAPI, HTTPException  
from pydantic import BaseModel, Field  
  
app = FastAPI(title="RAG Parser Service")  
  
  
class ParseRequest(BaseModel):  
 document_id: str = Field(min_length=1) storage_path: str = Field(min_length=1) file_type: str = Field(min_length=1)  
  
class ParseResponse(BaseModel):  
 document_id: str text: str metadata: dict  
  
@app.post("/api/rag/parse", response_model=ParseResponse)  
def parse_document(request: ParseRequest):  
 path = Path(request.storage_path) if not path.exists(): raise HTTPException(status_code=404, detail="文件不存在")  
  
 text, metadata = parse_by_type(path, request.file_type)  
 return ParseResponse( document_id=request.document_id, text=text, metadata=metadata, )```  
  
解析函数:  
  
```python  
def parse_by_type(path: Path, file_type: str) -> tuple[str, dict]:  
 normalized = file_type.lower().lstrip(".")  
 if normalized == "pdf": return parse_pdf(path) if normalized == "docx": return parse_docx(path) if normalized == "md": return parse_markdown(path) if normalized == "txt": return parse_txt(path)  
 raise HTTPException(status_code=400, detail=f"不支持的文件类型:{file_type}")  

7. 解析 TXT

TXT 最简单。

但要注意编码。

def parse_txt(path: Path) -> tuple[str, dict]:  
 for encoding in ("utf-8", "utf-8-sig", "gb18030"): try: text = path.read_text(encoding=encoding) return text, { "parser": "plain_text", "encoding": encoding, "source_path": str(path), } except UnicodeDecodeError: continue  
 raise HTTPException(status_code=400, detail="无法识别 TXT 文件编码")  

为什么尝试 gb18030?

因为中文 Windows 环境下有些文本不是 UTF-8。

8. 解析 Markdown

Markdown 可以先按纯文本读取。

def parse_markdown(path: Path) -> tuple[str, dict]:  
 text = path.read_text(encoding="utf-8") title = ""  
 for line in text.splitlines(): if line.startswith("# "): title = line.removeprefix("# ").strip() break  
 return text, { "parser": "markdown_text", "title": title, "source_path": str(path), }```  
  
Markdown 的标题结构很重要。  
  
后面可以按标题切片。  
  
所以解析阶段不要把 `#`、`##` 全删掉。  
  
## 9. 解析 DOCX  
  
可以先用 `python-docx`。  
  
安装:  
  
```bash  
python -m pip install python-docx```  
  
代码:  
  
```python  
from docx import Document  
  
  
def parse_docx(path: Path) -> tuple[str, dict]:  
 document = Document(path) paragraphs = []  
 for paragraph in document.paragraphs: text = paragraph.text.strip() if text: paragraphs.append(text)  
 return "\n\n".join(paragraphs), { "parser": "python-docx", "paragraph_count": len(paragraphs), "source_path": str(path), }```  
  
第一版先处理段落。  
  
表格可以后面再补。  
  
如果文档里很多表格,解析质量会受影响。  
  
## 10. 解析 PDF  
  
PDF 可以先用 PyPDF。  
  
安装:  
  
```bash  
python -m pip install pypdf```  
  
代码:  
  
```python  
from pypdf import PdfReader  
  
  
def parse_pdf(path: Path) -> tuple[str, dict]:  
 reader = PdfReader(str(path)) pages = []  
 for index, page in enumerate(reader.pages, start=1): text = page.extract_text() or "" if text.strip(): pages.append(f"\n\n[PAGE {index}]\n{text.strip()}")  
 return "\n".join(pages), { "parser": "pypdf", "page_count": len(reader.pages), "source_path": str(path), }```  
  
这里保留 `[PAGE 1]`。  
  
后面切片时可以根据它推断页码。  
  
PDF 解析常见问题:  
  
```text  
换行很乱  
表格顺序错  
页眉页脚重复  
扫描版没有文本  

这些是正常的。

后续再处理一部分。

11. 使用 LangChain Loader

如果想和 LangChain RAG 流程衔接,也可以使用 Document Loader。

示例:

from langchain_community.document_loaders import PyPDFLoader  
  
  
def parse_pdf_with_loader(path: Path):  
 loader = PyPDFLoader(str(path)) docs = loader.load()  
 text = "\n\n".join(doc.page_content for doc in docs) metadata = { "parser": "PyPDFLoader", "page_count": len(docs), "source_path": str(path), }  
 return text, metadata```  
  
LangChain 的 Document 通常包含:  
  
```text  
page_content  
metadata  

这和 RAG 后续流程很匹配。

但学习阶段不要被 Loader 种类绕进去。

先保证自己的解析接口稳定。

12. Java 调 Python 解析

Java 可以在上传后或手动接口里调用 Python:

public record ParseRequest(  
 String documentId, String storagePath, String fileType) {  
}  
public record ParseResponse(  
 String documentId, String text, Map<String, Object> metadata) {  
}  

调用:

ParseResponse response = restClient.post()  
 .uri("/api/rag/parse") .body(new ParseRequest(documentId.toString(), storagePath, fileType)) .retrieve() .body(ParseResponse.class);  

拿到结果后:

写入 rag_document_text.raw_text更新 rag_document.status = PARSED  

如果失败:

更新 rag_document.status = FAILED写入 error_message  

13. 解析质量检查

解析完不要直接相信结果。

至少检查:

text 是否为空  
text 长度是否合理  
是否包含大量乱码  
PDF 页数是否正确  
DOCX 段落数是否合理  
metadata 是否包含 source_path  

简单判断:

def validate_parsed_text(text: str):  
 if not text.strip(): raise HTTPException(status_code=400, detail="未解析到有效文本")  
  
 if len(text.strip()) < 20: raise HTTPException(status_code=400, detail="解析文本过短,请检查文档内容")  

不要把空文本送去切片。

否则后面每一步都会出奇怪问题。

14. 解析服务的工程化设计

解析器不是一个简单的“文件转字符串”函数。它需要稳定的输入输出协议、明确的版本和可追踪的质量信息,否则同一份文件在不同时间可能得到无法解释的结果。

14.1 解析契约

Java 服务与 Python 解析服务之间应约定统一返回结构,至少包含正文、页数、段落或页面信息、解析器名称、解析器版本、警告列表和错误类型。

解析失败要区分可重试错误和永久错误。例如服务超时、临时磁盘不足可以重试;文件损坏、密码保护、格式不支持通常应直接标记失败,避免无意义地重复执行。

14.2 版本和可复现性

升级 PDF、DOCX 或 Markdown 解析库后,文本顺序和空白处理可能变化。建议把 parserName、parserVersion 和解析配置写入 metadata,并保留原始文件与 raw_text。

当解析结果发生变化时,应能够回答三个问题:使用了哪个解析器、配置是否改变、旧结果是否需要重新切片和向量化。

14.3 隔离、超时和质量门槛

复杂文件可能消耗大量 CPU 和内存。解析任务应设置文件大小限制、执行超时、并发上限和临时目录配额。对不可信文件,最好在独立进程或容器中运行解析器。

解析完成后不要立刻进入清洗。可以先检查文本长度、空白占比、乱码比例、页数是否合理以及关键页是否为空。质量检查不通过时进入人工排查或专用 OCR 流程。

15. 常见问题

15.1 扫描版 PDF 为什么解析为空

因为扫描版 PDF 本质上是图片。

普通文本解析器读不到文字。

需要 OCR。

第一版可以返回:

未解析到有效文本,可能是扫描版 PDF  

15.2 PDF 表格乱怎么办

第一版先接受。

后面可以考虑:

pymupdf  
pdfplumber  
unstructured  
Docling  
专用表格抽取  

不要一开始就把表格解析作为主线。

15.3 Markdown 要不要转 HTML

不用。

RAG 更需要语义结构。

保留 Markdown 标题通常更有价值。

15.4 Java 解析还是 Python 解析

都可以。

但 Python 的文档解析生态更丰富。

本路线建议 Java 负责业务和入库,Python 负责 AI 和解析能力。

16. 练习清单

完成几件事情:

实现 FastAPI /api/rag/parse支持 txt 解析  
支持 md 解析  
支持 docx 解析  
支持 pdf 解析  
解析结果包含 metadata空文本返回明确错误  
Java 能调用 Python parse 接口  
解析结果写入 rag_document_text更新 rag_document.status = PARSED解析失败写入 FAILED 和 error_message  

建议目录:

ai-agent-study  
├── python  
│   └── rag-service  
│       ├── main.py  
│       ├── parsers.py  
│       └── schemas.py  
├── java  
│   └── rag-api  
│       └── PythonParseClient.java  
└── docs  
 └── document-parsing.md  

17. 小结

本节的结论:

文档解析要把不同文件格式统一变成文本,并保留足够的 metadata,方便后面清洗、切片和定位来源。

最小链路:

storagePath  
 -> parse_by_type -> raw_text -> metadata -> rag_document_text -> status=PARSED  

解析质量越清楚,后面的 RAG 调试越轻松。

参考资料

  • LangChain Document Loader Integrations
  • PyPDFLoader
  • python-docx Documentation
  • PyPDF Documentation
AI自学路线
AI
许可协议: 
分享

相关文章

6月 16, 2026

27 对话历史:让多轮 RAG 正确理解追问

对话历史:让多轮 RAG 正确理解追问 对话历史的核心不是把所有聊天记录不断塞给模型,而是保存完&#

6月 16, 2026

26 权限隔离:让无权内容无法进入 RAG 上下文

权限隔离:让无权内容无法进入 RAG 上下文 RAG 权限隔离的底线不是“最终答案不展示秘密”,而

6月 16, 2026

25 引用来源:让 RAG 答案可以核对和追溯

引用来源:让 RAG 答案可以核对和追溯 引用来源的核心是让答案中的事实能够回到具体文档、页码&#

下一篇

19 文本清洗

上一篇

17 文档上传

最近更新

  • 27 对话历史:让多轮 RAG 正确理解追问
  • 26 权限隔离:让无权内容无法进入 RAG 上下文
  • 25 引用来源:让 RAG 答案可以核对和追溯
  • 24 基础 RAG 问答
  • 23 向量检索

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy