读文件没那么简单
假设你正在做一个毕业设计:构建一个基于大模型的智能问答系统,用户可以上传公司内部文档,然后针对这些文档进行提问。
你可能觉得"解析文档内容"这一步很简单------不就是读文件吗?
java
// 你以为的代码
String content = Files.readString(Path.of("report.pdf"));
然后你会发现:这行代码根本跑不通。
1、 PDF:同一个后缀,完全不同的内心
你收到三份 PDF 文件:
可能有word导出来的,有扫描仪扫的(复制不了文字),某老系统生成(复制出来全是代码)。
也就是说。同样是 .pdf 后缀,内部结构可能完全不同:
-
文字型 PDF:内部存储的是文字编码,可以直接提取。
-
扫描型 PDF:内部存储的是图片,文字只是"画"上去的。
-
混合型 PDF:部分页是文字,部分页是扫描图。
你写的 readString() 根本不知道怎么处理这些差异。
2、Word 文档:远比你想象的复杂
一份 .docx 文件,打开看起来就是几段文字。但你用代码读出来可能是这样:
公司简介 成立时间 2015年 员工人数 500人 第 1 页,共 3 页 本文档为内部资料,请勿外传
问题在哪?
表格被拆成了莫名其妙的换行。页眉页脚混进了正文。多余的空行和空格;文档属性(作者、创建时间)你根本没拿到。
3、 文件后缀:会骗人的
java
File file = new File("data.txt");
// 你以为是文本文件,其实...
真实场景:
-
有人把
.xlsx改成了.txt(怕被邮件系统拦截)。 -
有人把
.exe改成了.docx(别问为什么)。 -
有人上传的
.pdf其实是 HTML 改的后缀。
只看后缀判断文件类型 = 100% 会翻车。
4、编码问题:中文乱码重灾区
鎮ㄥソ锛屾杩庝娇鐢? // 这是什么鬼?
原因:文件是 GBK 编码,你用 UTF-8 读取。
或者反过来:文件是 UTF-8,你用 GBK 读取。
更惨的情况:一份文档里混合了多种编码(别笑,真的有)。
构建rag系统的文档解析方案
RAG 的核心思路是:
不要让大模型凭空回答,而是先从你的文档库里检索相关内容,把检索结果喂给大模型,让它基于这些内容来回答。
而文档解析的质量,直接决定了你整个 RAG 系统的上限。
到这里,你应该理解了:解析文档不是一个 readFile() 能搞定的事。
你需要一个工具:
能自动识别文件的真实类型(不靠后缀)
能处理几十种文档格式(PDF、Word、PPT、Excel、HTML、邮件...)
能提取文本内容。
能提取元数据(作者、创建时间、标题...)
能处理编码问题。
能对接 OCR(处理扫描件)
最好是开源免费的。
这个工具就是 Apache Tika。
GitHub 地址:https://github.com/apache/tika
认识 Apache Tika
Apache Tika 是 Apache 基金会的开源项目,专门用于内容检测和内容提取。
用一句话概括:给 Tika 一个文件(不管什么格式),它还你干净的文本和元数据。
Tika 支持 1000+ 种 MIME 类型,常见的包括:
| 类别 | 格式 |
|---|---|
| 文档 | PDF, DOC, DOCX, ODT, RTF, TXT |
| 表格 | XLS, XLSX, CSV, ODS |
| 演示 | PPT, PPTX, ODP |
| 图片 | JPG, PNG, GIF, TIFF, BMP |
| 网页 | HTML, XML, XHTML |
| 压缩 | ZIP, TAR, GZIP, 7Z |
| 邮件 | EML, MSG, MBOX |
| 电子书 | EPUB, MOBI |
| 音视频 | MP3, MP4, AVI(提取元数据) |
解析方式
它不是一个单一的 PDF 解析器,而是一套可组合的解析框架。核心可以概括为:
输入文件:PDF / DOCX / XLSX / HTML / EML ...
Detector:文件类型检测:判断 MIME 类型,而非只看后缀
Parser:格式解析器:根据文件类型调用相应的解析逻辑
==>ContentHandler:文本、HTML、Markdown 等
==>Metadata标题、作者、日期等
结构化解析结果
两种使用方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 作为 Java 依赖库 | 直接在项目中引入 tika-core 和 tika-parsers,代码里调用 |
单体应用、解析量不大、对延迟敏感 |
| 作为独立服务(Tika Server) | 用 Docker 跑一个 Tika 服务,通过 HTTP 接口调用 | 微服务架构、多语言、需要隔离、解析量大 |
Tika与OCR
Tika 本身不包含 OCR 引擎,但可以对接外部 OCR 工具:
-
Tesseract(开源,最常用)
-
Adobe Acrobat(商业)
-
ABBYY(商业,效果最好)
当 Tika 发现 PDF 里某一页是图片时,会调用配置好的 OCR 引擎。
Tika 如何检测真实 MIME 类型
Tika 使用魔数检测(Magic Number Detection):
PDF 文件的前几个字节是:%PDF- ZIP 文件的前几个字节是:PK PNG 图片的前几个字节是:‰PNG
Tika 读取文件的头部字节,与已知的签名库比对,从而判断真实类型。
java
// Tika 检测 MIME 类型
Tika tika = new Tika();
String mimeType = tika.detect(new File("unknown_file"));
// 返回 application/pdf,不管文件后缀是什么
拓展
什么是 MIME 类型
MIME(Multipurpose Internet Mail Extensions)类型是互联网标准,用于标识文件的真实格式。
格式是 类型/子类型,比如:
| MIME 类型 | 对应格式 |
|---|---|
text/plain |
纯文本 |
text/html |
HTML |
application/pdf |
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
DOCX |
image/png |
PNG 图片 |
application/zip |
ZIP 压缩包 |
文本抽取
文本抽取就是从各种格式的文件中,提取出纯文本内容。
输入:一份带格式的 Word 文档(有字体、颜色、表格、图片)
输出:纯文字字符串(没有格式信息)
但为什么不能直接读字节
java
byte[] bytes = Files.readAllBytes(Path.of("report.docx"));
String content = new String(bytes, StandardCharsets.UTF_8);
// 结果:一堆乱码 + 不可见字符
因为 .docx 本质是一个 ZIP 压缩包,里面是一堆 XML 文件。你直接读字节,读到的是 ZIP 的二进制结构。
怪不得以前我们写demo时用函数读字节,演示的都是txt文档; .txt 能直接读,是因为它本身就是文本;.docx、.pdf、.xlsx 不能直接读,是因为它们是结构化容器或二进制格式。
元数据抽取
元数据(Metadata)是"关于数据的数据",描述文档本身的属性。
常见的元数据:
| 字段 | 说明 | 示例 |
|---|---|---|
Content-Type |
MIME 类型 | application/pdf |
title |
文档标题 | 2024年度报告 |
creator |
作者/创建者 | 张三 |
created |
创建时间 | 2024-01-15T10:30:00 |
modified |
修改时间 | 2024-03-20T14:00:00 |
pageCount |
页数 | 15 |
wordCount |
字数 | 5000 |
元数据在 RAG 中的价值可以是溯源,过滤(如果你有 created 字段,可以做时间过滤),以及权限控制(某些文档有 department: 财务部 的元数据,只有财务部员工能查)
OCR
OCR(Optical Character Recognition,光学字符识别)是一种技术,能把图片中的文字识别成可编辑的文本。
输入:一张包含文字的图片
输出:图片中文字的字符串
对于word导出的pdf,我们可以直接提取文字,但是扫描仪扫描的pdf(内部是图片),手机拍的照片,PNG/JPG 图片,截图,这些就需要用到OCR了。
OCR也是有一定局限性的,譬如 比直接文本提取慢 10-100 倍,手写字、模糊图片、特殊字体可能识别错误, 要安装 Tesseract 等, CPU 和内存占用高。
所以我们建议:先尝试直接文本提取,只有提取为空/内容过少时再走 OCR。
把 Tika 当 Java 依赖库
Gitee 地址:https://gitee.com/nageoffer/springboot-ladder
项目介绍:从零到一学习 SpringBoot 各种组件框架实战的项目,让 Demo 变得简单。
主要函数介绍
1、参数 MultipartFile file 来自 Spring Web 的文件上传对象,包含文件名、文件大小、文件流等信息。
2、返回值
返回类型是 ParseResult,从代码看有三种情况:
-
解析成功 :返回
ParseResult.success(mimeType, content, metadataMap),包含 MIME 类型、清洗后的正文、元数据 Map; -
解析失败 :返回
ParseResult.failure("错误原因"),比如文件为空、内容为空、读取失败、解析异常; -
异常兜底 :捕获
IOException、TikaException、SAXException和普通Exception,统一转成失败结果。
java
3、实现思路
1)空文件校验
如果 file 为 null 或 isEmpty(),直接返回失败,避免后续空指针。
2)MIME 类型检测
使用 tika.detect(inputStream, originalFilename) 识别文件真实类型。
这里重新获取了一次输入流,因为 detect 会消费流,不能复用同一个流。
3)准备 Tika 解析组件
BodyContentHandler:接收解析出的正文,并通过 MAX_TEXT_LENGTH 限制长度,防止大文件导致内存溢出;
Metadata:保存和传递元数据;
ParseContext:解析上下文,后续可扩展 OCR、PDF 配置等;
parser:通常是 AutoDetectParser 或已配置好的 Tika 解析器。
4)执行解析
调用:parser.parse(parseStream, handler, metadata, context);
Tika 会把文档流解析成 XHTML SAX 事件,正文写入 handler,元数据写入 metadata
5)获取正文
通过 handler.toString() 拿到提取出的文本内容。
6)清洗文本
调用 cleanText(content) 去除多余空白、不可见字符或解析残留噪音。
7)提取元数据
通过 extractMetadata(metadata) 把 Tika 的 Metadata 转成 Map<String, String>,方便后续序列化或入库。
8)质量检查与异常处理
如果正文为空,认为可能是扫描件、加密文档或解析失败;最后通过多层 catch 分别处理 IO、Tika、SAX 和未知异常,并记录日志。
public ParseResult parseFile(MultipartFile file) {
}
清洗文档内容
java
/**
* 清洗文本内容
* - 将多个连续空白字符替换为单个空格
* - 将多个连续换行替换为最多两个换行(保留段落)
* - 去除首尾空白
*/
private String cleanText(String text) {
if (text == null) {
return "";
}
return text
// 将 \r\n 统一为 \n
.replaceAll("\\r\\n", "\n")
// 将 \r 统一为 \n
.replaceAll("\\r", "\n")
// 去除每行首尾的空格
.replaceAll("(?m)^[ \\t]+|[ \\t]+$", "")
// 将 3 个及以上连续换行替换为 2 个换行
.replaceAll("\\n{3,}", "\n\n")
// 将多个连续空格/制表符替换为单个空格
.replaceAll("[ \\t]+", " ")
// 去除首尾空白
.trim();
}
Tika 选择嵌入式还是独立部署运行?
更适合嵌入式的场景:
-
课程/作业/小项目:就一个 Spring Boot 服务,上传一些 doc/pdf,规模不大
-
部署环境简单:不想多维护一个容器服务
-
解析量不高:偶尔解析、文件不大、并发低
更适合 Tika Server 独立部署的场景:
-
你在做 知识库入库/RAG 数据准备:大量文件、批处理、并发高
-
文件来源更"野":各种 PDF(含扫描件)、各种 Office、各种编码
-
你希望解析失败/卡死不影响主业务:把解析从业务进程剥离
-
你有多种语言/多个服务要复用同一套解析能力