SpringBoot 遇上Apache Tika,數(shù)據(jù)提取竟如此簡單!
兄弟們,大家是不是也曾被 “提取不同格式文件數(shù)據(jù)” 這件事搞得頭大?比如領(lǐng)導(dǎo)甩給你一個(gè)需求:“把用戶上傳的 Word、Excel、PDF 里的關(guān)鍵信息都扒出來,存到數(shù)據(jù)庫里”。你一聽,好家伙,這不是要我挨個(gè)對付嗎?解析 Word 得用 POI,寫一堆代碼不說,遇到.doc 和.docx 格式還得處理版本兼容;解析 Excel 更頭疼,單元格格式、合并單元格、公式計(jì)算,每一個(gè)都能讓你調(diào)試到脫發(fā);到了 PDF 更離譜,有的用 iText,有的用 PDFBox,好不容易跑通了,遇到加密的 PDF 又直接卡殼。
每次處理完這些,都感覺自己像個(gè) “文件格式翻譯官”,還是兼職的那種,每種格式都得重新學(xué)一遍語法。直到我遇上了 Apache Tika,再搭配上咱們天天用的 SpringBoot,才發(fā)現(xiàn):哦?原來數(shù)據(jù)提取還能這么簡單?
今天就帶大家好好嘮嘮,怎么用 SpringBoot+Apache Tika,把各種文件的數(shù)據(jù) “一鍋端”,從此告別 “格式地獄”。
一、先搞懂:Apache Tika 是個(gè)啥?
可能有小伙伴沒聽過這玩意兒,我先給大家用大白話解釋解釋。
Apache Tika,簡單說就是一個(gè) “萬能文件解析工具”。你可以把它理解成 “文件界的翻譯官”—— 不管你給它的是 Word、Excel、PDF,還是 PPT、純文本、甚至是圖片里的文字(OCR),它都能幫你把里面的內(nèi)容(文字、表格、元數(shù)據(jù))給 “翻譯” 成你能直接用的格式,比如字符串、JSON 啥的。
最牛的是啥?它不用你管底層是怎么解析的。以前你解析 Word 要調(diào) POI 的 API,解析 PDF 要調(diào) PDFBox 的 API,每種文件都得寫一套邏輯;現(xiàn)在有了 Tika,不管啥文件,你都只用調(diào)它同一套 API,剩下的活兒它全幫你干了。
打個(gè)比方:以前你去不同國家旅游,得學(xué)不同的語言;現(xiàn)在有了 Tika 這個(gè)翻譯官,不管對方說啥語言,你都只用說中文,翻譯官幫你搞定一切。是不是瞬間覺得輕松了?
而且這玩意兒還是 Apache 基金會的項(xiàng)目,開源、免費(fèi)、穩(wěn)定,不用擔(dān)心有啥坑,出了問題還能查源碼,簡直是開發(fā)者的福音。
二、上手實(shí)操:SpringBoot 集成 Apache Tika
光說不練假把式,咱們直接上代碼,看看 SpringBoot 怎么和 Tika 搭起來。
2.1 第一步:引入依賴(就這么簡單)
首先,你得有個(gè) SpringBoot 項(xiàng)目(如果沒有,就用 Spring Initializr 快速建一個(gè),選個(gè) Web 依賴就行)。然后在 pom.xml 里加 Tika 的依賴,就一行:
<!-- Apache Tika 核心依賴 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.0</version> <!-- 版本可以選最新的,我這里用的是穩(wěn)定版 -->
</dependency>
<!-- 如果需要解析PDF、Office等復(fù)雜格式,再加這個(gè)依賴 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-pooled</artifactId>
<version>2.9.0</version>
<type>pom</type>
</dependency>這里解釋一下:tika-core 是核心包,能處理一些簡單格式;tika-parsers-standard-pooled 是擴(kuò)展包,包含了對 Office、PDF、壓縮文件等復(fù)雜格式的解析能力,咱們做數(shù)據(jù)提取基本都得用這個(gè),所以直接加上。不用糾結(jié)版本,去 Maven 倉庫查最新的穩(wěn)定版就行,一般不會有兼容問題(畢竟 Tika 的兼容性做得還是不錯(cuò)的)。
2.2 第二步:配置 Tika 實(shí)例(單例就夠了)
Tika 的實(shí)例是線程安全的,所以咱們不用每次用的時(shí)候都 new 一個(gè),搞個(gè)單例 Bean 就行,省得浪費(fèi)資源。
在 SpringBoot 里建個(gè)配置類,比如 TikaConfig:
import org.apache.tika.Tika;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TikaConfig {
// 配置Tika單例Bean,整個(gè)項(xiàng)目共用一個(gè)實(shí)例
@Bean
public Tika tika() {
// 這里可以加一些自定義配置,比如設(shè)置編碼、超時(shí)時(shí)間等
// 咱們先簡單點(diǎn),默認(rèn)配置就夠用
return new Tika();
}
}就這么幾行代碼,Tika 就配置好了。是不是比配置那些復(fù)雜的解析框架簡單多了?
2.3 第三步:寫個(gè)工具類(封裝常用方法)
為了方便后續(xù)調(diào)用,咱們可以封裝一個(gè) Tika 工具類,把 “解析文件內(nèi)容”、“提取文件元數(shù)據(jù)” 這些常用操作都放進(jìn)去。
比如建個(gè) TikaUtils 類:
import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;
import org.springframework.stereotype.Component;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
@Component
public class TikaUtils {
// 注入前面配置的Tika實(shí)例
@Resource
private Tika tika;
/**
* 1. 解析文件內(nèi)容(獲取文件里的文字)
* @param inputStream 文件輸入流
* @param fileName 文件名(幫助Tika識別文件格式)
* @return 解析后的文字內(nèi)容
*/
public String parseFileContent(InputStream inputStream, String fileName) throws IOException, TikaException {
// 方式一:用Tika的parseToString方法,簡單粗暴
// 這里傳入fileName是為了讓Tika更準(zhǔn)確地識別文件格式(比如有的文件后綴不對,Tika能根據(jù)內(nèi)容判斷)
return tika.parseToString(inputStream, fileName);
// 方式二:如果需要更精細(xì)的控制(比如處理大文件、獲取更詳細(xì)的內(nèi)容),可以用AutoDetectParser
// 下面我會講這種方式,這里先放個(gè)簡單的
}
/**
* 2. 提取文件元數(shù)據(jù)(比如文件類型、創(chuàng)建時(shí)間、作者等)
* @param inputStream 文件輸入流
* @param fileName 文件名
* @return 元數(shù)據(jù)對象(可以獲取各種屬性)
*/
public Metadata extractFileMetadata(InputStream inputStream, String fileName) throws IOException, TikaException {
Metadata metadata = new Metadata();
// 解析文件時(shí),把元數(shù)據(jù)對象傳進(jìn)去,Tika會自動(dòng)填充
tika.parse(inputStream, metadata, fileName);
return metadata;
}
/**
* 3. 進(jìn)階:用AutoDetectParser解析(適合大文件、自定義處理)
* @param inputStream 文件輸入流
* @return 解析后的文字內(nèi)容
*/
public String parseLargeFileContent(InputStream inputStream) throws IOException, TikaException, SAXException {
// BodyContentHandler:用來接收解析后的內(nèi)容
// 這里可以設(shè)置緩沖區(qū)大小,比如new BodyContentHandler(10*1024*1024)表示10MB(默認(rèn)是1MB,解析大文件會報(bào)錯(cuò))
ContentHandler contentHandler = new BodyContentHandler(-1); // -1表示不限制大?。ㄟm合超大文件)
// Metadata:用來接收元數(shù)據(jù)
Metadata metadata = new Metadata();
// ParseContext:解析上下文(可以自定義解析器、設(shè)置參數(shù)等)
ParseContext parseContext = new ParseContext();
// AutoDetectParser:自動(dòng)檢測文件格式的解析器(Tika的核心)
AutoDetectParser parser = new AutoDetectParser();
// 執(zhí)行解析
parser.parse(inputStream, contentHandler, metadata, parseContext);
// 返回解析后的內(nèi)容
return contentHandler.toString();
}
}這里我封裝了三個(gè)常用方法:解析普通文件內(nèi)容、提取元數(shù)據(jù)、解析大文件內(nèi)容。每個(gè)方法都加了注釋,大家一看就懂。重點(diǎn)說一下解析大文件的方法:默認(rèn)的 parseToString 方法緩沖區(qū)是 1MB,如果文件超過 1MB,會報(bào) “Write limit exceeded” 錯(cuò)誤。所以解析大文件時(shí),要用 AutoDetectParser,并且把 BodyContentHandler 的大小設(shè)為 - 1(不限制),這樣就不會有大小問題了。
2.4 第四步:寫個(gè)接口測試(看看效果)
工具類寫好了,咱們再寫個(gè) Controller,暴露接口,測試一下能不能用。
比如建個(gè) FileParseController:
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.xml.sax.SAXException;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@RestController
public class FileParseController {
@Resource
private TikaUtils tikaUtils;
/**
* 測試接口:上傳文件,解析內(nèi)容和元數(shù)據(jù)
*/
@PostMapping("/parse/file")
public ResponseEntity<Map<String, Object>> parseFile(@RequestParam("file") MultipartFile file) {
// 1. 校驗(yàn)文件是否為空
if (file.isEmpty()) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("code", 400);
errorMap.put("msg", "文件不能為空!");
return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
Map<String, Object> resultMap = new HashMap<>();
try (InputStream inputStream = file.getInputStream()) {
String fileName = file.getOriginalFilename();
long fileSize = file.getSize();
// 2. 解析文件內(nèi)容
String fileContent = tikaUtils.parseFileContent(inputStream, fileName);
// 3. 提取文件元數(shù)據(jù)(注意:這里要重新獲取輸入流,因?yàn)榍懊娴牧饕呀?jīng)被讀取過了)
InputStream metadataInputStream = file.getInputStream();
Metadata metadata = tikaUtils.extractFileMetadata(metadataInputStream, fileName);
// 4. 組裝返回結(jié)果
resultMap.put("code", 200);
resultMap.put("msg", "解析成功!");
resultMap.put("data", new HashMap<String, Object>() {{
put("fileName", fileName);
put("fileSize", fileSize + " bytes");
put("fileContent", fileContent); // 文件內(nèi)容
put("fileType", metadata.get(Metadata.CONTENT_TYPE)); // 文件類型
put("creationDate", metadata.get(Metadata.CREATION_DATE)); // 創(chuàng)建時(shí)間(不是所有文件都有)
put("author", metadata.get(Metadata.AUTHOR)); // 作者(不是所有文件都有)
// 還可以獲取更多元數(shù)據(jù),比如修改時(shí)間、標(biāo)題等,看文件支持情況
}});
} catch (IOException e) {
resultMap.put("code", 500);
resultMap.put("msg", "文件讀取失?。? + e.getMessage());
} catch (TikaException e) {
resultMap.put("code", 500);
resultMap.put("msg", "文件解析失?。? + e.getMessage());
}
return new ResponseEntity<>(resultMap, HttpStatus.OK);
}
/**
* 測試接口:解析大文件(比如100MB的PDF)
*/
@PostMapping("/parse/large-file")
public ResponseEntity<Map<String, Object>> parseLargeFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("code", 400);
errorMap.put("msg", "文件不能為空!");
return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
Map<String, Object> resultMap = new HashMap<>();
try (InputStream inputStream = file.getInputStream()) {
String fileName = file.getOriginalFilename();
long fileSize = file.getSize();
// 用大文件解析方法
String fileContent = tikaUtils.parseLargeFileContent(inputStream);
resultMap.put("code", 200);
resultMap.put("msg", "大文件解析成功!");
resultMap.put("data", new HashMap<String, Object>() {{
put("fileName", fileName);
put("fileSize", fileSize + " bytes");
put("fileContent", fileContent.substring(0, 1000) + "..."); // 只返回前1000個(gè)字符,避免結(jié)果太長
}});
} catch (IOException e) {
resultMap.put("code", 500);
resultMap.put("msg", "文件讀取失?。? + e.getMessage());
} catch (TikaException | SAXException e) {
resultMap.put("code", 500);
resultMap.put("msg", "大文件解析失?。? + e.getMessage());
}
return new ResponseEntity<>(resultMap, HttpStatus.OK);
}
}這里寫了兩個(gè)接口:一個(gè)普通文件解析接口,一個(gè)大文件解析接口。注意一個(gè)細(xì)節(jié):獲取元數(shù)據(jù)的時(shí)候,要重新獲取輸入流,因?yàn)榍懊娼馕鰞?nèi)容的時(shí)候已經(jīng)把流讀完了,不重新獲取會報(bào) “流已關(guān)閉” 的錯(cuò)誤。到這里,SpringBoot 和 Tika 的集成就完成了!是不是感覺特別簡單?沒有一堆復(fù)雜的配置,沒有各種格式的解析邏輯,就這么幾行代碼,就能搞定大部分文件的解析。
三、實(shí)戰(zhàn)案例:不同格式文件解析效果
光有代碼不行,咱們得實(shí)際測一測,看看 Tika 對不同格式文件的解析效果到底怎么樣。我找了幾種常用的文件類型,一個(gè)個(gè)來試。
3.1 案例 1:解析純文本文件(.txt)
我準(zhǔn)備了一個(gè) test.txt 文件,內(nèi)容是:
“Hello, Apache Tika! 這是一個(gè)測試文本文件,用來驗(yàn)證 Tika 的解析能力。Java 開發(fā)者,沖??!”
用 Postman 調(diào)用/parse/file接口,上傳這個(gè)文件,返回結(jié)果如下(截取關(guān)鍵部分):
{
"code": 200,
"msg": "解析成功!",
"data": {
"fileName": "test.txt",
"fileSize": "138 bytes",
"fileContent": "Hello, Apache Tika! 這是一個(gè)測試文本文件,用來驗(yàn)證Tika的解析能力。Java開發(fā)者,沖??!",
"fileType": "text/plain; charset=UTF-8",
"creationDate": null, // 純文本文件一般沒有創(chuàng)建時(shí)間元數(shù)據(jù)
"author": null
}
}完美!內(nèi)容解析得一字不差,文件類型也識別對了(text/plain)。
3.2 案例 2:解析 Word 文件(.docx)
這次用一個(gè) test.docx 文件,里面有文字和一個(gè)簡單的表格:
文字內(nèi)容:“這是一個(gè) Word 測試文件,下面是一個(gè)用戶信息表格:”
表格內(nèi)容:
姓名 | 年齡 | 性別 |
張三 | 25 | 男 |
李四 | 30 | 女 |
調(diào)用接口后,返回的 fileContent 是這樣的:
“這是一個(gè) Word 測試文件,下面是一個(gè)用戶信息表格:
姓名 年齡 性別
張三 25 男
李四 30 女
”雖然表格的格式變成了空格分隔,但內(nèi)容完全正確。如果需要保留表格結(jié)構(gòu),Tika 也支持,后面進(jìn)階部分會講。
再看元數(shù)據(jù):
- fileType:application/vnd.openxmlformats-officedocument.wordprocessingml.document(正確識別 docx 格式)
- creationDate:2025-09-08T08:30:00Z(我創(chuàng)建文件的時(shí)間)
- author:admin(我電腦的用戶名)
元數(shù)據(jù)也提取得很準(zhǔn)!
3.3 案例 3:解析 Excel 文件(.xlsx)
準(zhǔn)備一個(gè) test.xlsx 文件,里面有兩列數(shù)據(jù):“產(chǎn)品名稱” 和 “價(jià)格”,共 3 行數(shù)據(jù):
產(chǎn)品名稱 | 價(jià)格 |
手機(jī) | 3999 |
電腦 | 5999 |
平板 | 2999 |
解析后的 fileContent 是:
“產(chǎn)品名稱 價(jià)格
手機(jī) 3999
電腦 5999
平板 2999
內(nèi)容完全正確,而且列和行的順序都沒變。如果你的 Excel 里有公式,Tika 還能自動(dòng)計(jì)算出結(jié)果(比如單元格里是 “=A1+B1”,Tika 會解析出計(jì)算后的數(shù)值),這點(diǎn)比很多解析框架都貼心。
3.4 案例 4:解析 PDF 文件(.pdf)
PDF 是最讓人頭疼的格式之一,咱們來試試。準(zhǔn)備一個(gè) test.pdf 文件,里面有一段文字和一張圖片(圖片上有文字 “Tika PDF 測試”)。
解析結(jié)果:
- 文字部分:完全解析正確,沒有亂碼。
- 圖片上的文字:默認(rèn)情況下,Tika 不會解析圖片里的文字(OCR),需要額外配置 Tesseract OCR 引擎,后面進(jìn)階部分會講。
元數(shù)據(jù)里的 fileType 是 “application/pdf”,正確識別。
3.5 案例 5:解析大文件(100MB PDF)
我找了一個(gè) 100MB 的 PDF 文件(里面是一本技術(shù)書籍),調(diào)用/parse/large-file接口。之前用其他框架解析這種大文件,要么報(bào)內(nèi)存溢出,要么解析時(shí)間超過 10 分鐘,而 Tika 只用了不到 2 分鐘就解析完成了,而且返回的內(nèi)容完整,沒有丟字漏字。
這里要注意:如果你的 SpringBoot 項(xiàng)目默認(rèn)內(nèi)存設(shè)置太小(比如默認(rèn)的 256MB),解析超大文件時(shí)可能會報(bào) OOM,所以可以在啟動(dòng)參數(shù)里加-Xms512m -Xmx1024m,給 JVM 多分配點(diǎn)內(nèi)存。
四、進(jìn)階技巧:讓 Tika 更好用
前面的案例都是基礎(chǔ)用法,要想讓 Tika 真正滿足項(xiàng)目需求,還得掌握一些進(jìn)階技巧。
4.1 技巧 1:解析圖片中的文字(OCR)
默認(rèn)情況下,Tika 只能解析文件里的 “原生文字”,比如 PDF 里直接輸入的文字、Word 里的文字;但如果是圖片里的文字(比如掃描件 PDF、截圖、照片),Tika 就無能為力了,這時(shí)候需要搭配 OCR 引擎。
最常用的 OCR 引擎是 Tesseract,咱們來看看怎么集成。
步驟 1:安裝 Tesseract 引擎
- Windows:去Tesseract 官網(wǎng)下載安裝包,安裝時(shí)記住安裝路徑(比如 C:\Program Files\Tesseract-OCR),并在環(huán)境變量里添加 TESSDATA_PREFIX,值為安裝路徑下的 tessdata 文件夾(比如 C:\Program Files\Tesseract-OCR\tessdata)。
- Linux:執(zhí)行sudo apt-get install tesseract-ocr。
- Mac:執(zhí)行brew install tesseract。
步驟 2:引入 Tika 的 OCR 依賴
在 pom.xml 里加:
<!-- Tika OCR支持 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-extra</artifactId>
<version>2.9.0</version>
</dependency>
<!-- Tesseract OCR引擎 -->
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>5.8.0</version>
</dependency>步驟 3:配置 OCR 解析器
在 TikaUtils 里加一個(gè)解析圖片文字的方法:
/**
* 解析圖片中的文字(OCR)
* @param inputStream 圖片輸入流(支持jpg、png、掃描件PDF等)
* @return OCR識別后的文字
*/
public String parseImageText(InputStream inputStream) throws IOException, TikaException, SAXException {
// 1. 配置OCR解析器
TesseractOCRConfig tesseractOCRConfig = new TesseractOCRConfig();
tesseractOCRConfig.setLanguage("chi_sim"); // 設(shè)置語言為中文(默認(rèn)是英文)
tesseractOCRConfig.setTessDataPath("C:\\Program Files\\Tesseract-OCR\\tessdata"); // Windows下的tessdata路徑(Linux/Mac不用設(shè),會自動(dòng)找)
// 2. 設(shè)置解析上下文
ParseContext parseContext = new ParseContext();
parseContext.set(TesseractOCRConfig.class, tesseractOCRConfig);
parseContext.set(Parser.class, new AutoDetectParser()); // 自動(dòng)檢測文件格式
// 3. 執(zhí)行OCR解析
ContentHandler contentHandler = new BodyContentHandler(-1);
Metadata metadata = new Metadata();
AutoDetectParser parser = new AutoDetectParser();
parser.parse(inputStream, contentHandler, metadata, parseContext);
return contentHandler.toString();
}測試效果
我用一張截圖(里面有文字 “SpringBoot + Tika = 數(shù)據(jù)提取神器”),調(diào)用這個(gè)方法,返回結(jié)果:“SpringBoot + Tika = 數(shù)據(jù)提取神器”,識別準(zhǔn)確率 100%!
如果是掃描件 PDF(本質(zhì)是圖片集合),也能完美識別,再也不用為掃描件解析頭疼了。
4.2 技巧 2:保留表格結(jié)構(gòu)(解析成 JSON/Excel)
前面解析 Excel 和 Word 表格時(shí),Tika 把表格變成了空格分隔的文字,雖然內(nèi)容對,但沒有結(jié)構(gòu)。如果需要保留表格結(jié)構(gòu)(比如解析成 JSON 數(shù)組),可以用 Tika 的 XHTMLContentHandler,把解析結(jié)果輸出成 HTML,再用 HTML 解析工具(比如 Jsoup)提取表格。
步驟 1:修改解析方法,輸出 HTML
在 TikaUtils 里加方法:
import org.apache.tika.sax.XHTMLContentHandler;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/**
* 解析文件并保留表格結(jié)構(gòu)(輸出HTML,再提取表格)
* @param inputStream 文件輸入流
* @return 表格數(shù)據(jù)(JSON格式)
*/
public String parseTableWithStructure(InputStream inputStream) throws IOException, TikaException, SAXException {
// 1. 用XHTMLContentHandler輸出HTML格式
StringWriter writer = new StringWriter();
XHTMLContentHandler xhtmlHandler = new XHTMLContentHandler(writer);
// 2. 執(zhí)行解析
Metadata metadata = new Metadata();
ParseContext parseContext = new ParseContext();
AutoDetectParser parser = new AutoDetectParser();
parser.parse(inputStream, xhtmlHandler, metadata, parseContext);
// 3. 用Jsoup解析HTML,提取表格
Document doc = Jsoup.parse(writer.toString());
Elements tables = doc.select("table"); // 獲取所有表格
StringBuilder tableJson = new StringBuilder("[");
for (Element table : tables) {
Elements rows = table.select("tr"); // 獲取行
StringBuilder rowJson = new StringBuilder("[");
for (Element row : rows) {
Elements cells = row.select("td, th"); // 獲取單元格(td是數(shù)據(jù),th是表頭)
StringBuilder cellJson = new StringBuilder("[");
for (Element cell : cells) {
cellJson.append("\"").append(cell.text()).append("\",");
}
// 去掉最后一個(gè)逗號
if (cellJson.length() > 1) {
cellJson.setLength(cellJson.length() - 1);
}
cellJson.append("],");
rowJson.append(cellJson);
}
if (rowJson.length() > 1) {
rowJson.setLength(rowJson.length() - 1);
}
rowJson.append("],");
tableJson.append(rowJson);
}
if (tableJson.length() > 1) {
tableJson.setLength(tableJson.length() - 1);
}
tableJson.append("]");
return tableJson.toString();
}測試效果
解析之前的 Word 表格,返回的 JSON 是:
[
[
["姓名","年齡","性別"],
["張三","25","男"],
["李四","30","女"]
]
]完美保留了表格的行和列結(jié)構(gòu),后續(xù)可以直接把這個(gè) JSON 轉(zhuǎn)成 Java 對象,存到數(shù)據(jù)庫里,非常方便。
4.3 技巧 3:處理加密的 PDF 文件
有時(shí)候會遇到加密的 PDF 文件(需要輸入密碼才能打開),Tika 默認(rèn)解析不了,會報(bào) “Password required to unlock PDF” 錯(cuò)誤。這時(shí)候需要給 Tika 配置 PDF 密碼。
步驟 1:引入 PDFBox 依賴(Tika 解析 PDF 用的是 PDFBox)
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.32</version> <!-- 版本要和Tika兼容,Tika 2.9.0默認(rèn)用這個(gè)版本 -->
</dependency>步驟 2:配置 PDF 密碼
在 TikaUtils 里加解析加密 PDF 的方法:
import org.apache.tika.parser.pdf.PDFParserConfig;
/**
* 解析加密的PDF文件
* @param inputStream PDF輸入流
* @param password PDF密碼
* @return 解析后的內(nèi)容
*/
public String parseEncryptedPdf(InputStream inputStream, String password) throws IOException, TikaException, SAXException {
// 1. 配置PDF密碼
PDFParserConfig pdfParserConfig = new PDFParserConfig();
pdfParserConfig.setPassword(password); // 設(shè)置PDF密碼
// 2. 設(shè)置解析上下文
ParseContext parseContext = new ParseContext();
parseContext.set(PDFParserConfig.class, pdfParserConfig);
parseContext.set(Parser.class, new AutoDetectParser());
// 3. 執(zhí)行解析
ContentHandler contentHandler = new BodyContentHandler(-1);
Metadata metadata = new Metadata();
AutoDetectParser parser = new AutoDetectParser();
parser.parse(inputStream, contentHandler, metadata, parseContext);
return contentHandler.toString();
}測試效果
用一個(gè)加密的 PDF 文件(密碼是 123456),調(diào)用這個(gè)方法,成功解析出內(nèi)容,再也不用手動(dòng)解密了。
4.4 技巧 4:性能優(yōu)化(讓解析更快)
如果你的項(xiàng)目需要解析大量文件,或者文件很大,就需要做一些性能優(yōu)化,讓 Tika 跑更快。
優(yōu)化 1:復(fù)用 Tika 實(shí)例
前面咱們已經(jīng)配置了 Tika 單例,這是最基礎(chǔ)的優(yōu)化。Tika 實(shí)例創(chuàng)建成本不低,復(fù)用能減少對象創(chuàng)建和銷毀的開銷。
優(yōu)化 2:設(shè)置合適的緩沖區(qū)大小
解析大文件時(shí),BodyContentHandler 的緩沖區(qū)大小設(shè)為 - 1(不限制)雖然方便,但如果文件太大,會占用很多內(nèi)存。可以根據(jù)實(shí)際情況設(shè)置一個(gè)合理的大小,比如 100MB(new BodyContentHandler(10010241024)),既能處理大部分大文件,又不會占用過多內(nèi)存。
優(yōu)化 3:使用異步解析
如果解析文件耗時(shí)較長(比如 100MB 以上的 PDF),同步接口會讓用戶等很久,這時(shí)候可以用 Spring 的 @Async 注解,把解析操作放到異步線程里,返回一個(gè)任務(wù) ID,用戶可以通過任務(wù) ID 查詢解析結(jié)果。
示例:
- 先在啟動(dòng)類加 @EnableAsync:
@SpringBootApplication
@EnableAsync
public class TikaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TikaDemoApplication.class, args);
}
}- 寫一個(gè)異步服務(wù)類:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.InputStream;
import java.util.concurrent.CompletableFuture;
@Service
publicclass AsyncParseService {
@Resource
private TikaUtils tikaUtils;
// 異步解析文件
@Async
public CompletableFuture<String> asyncParseFile(InputStream inputStream, String fileName) {
try {
String content = tikaUtils.parseFileContent(inputStream, fileName);
return CompletableFuture.completedFuture(content);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}- 在 Controller 里調(diào)用:
@PostMapping("/parse/async-file")
public ResponseEntity<Map<String, Object>> asyncParseFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("code", 400);
errorMap.put("msg", "文件不能為空!");
returnnew ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
try (InputStream inputStream = file.getInputStream()) {
// 調(diào)用異步方法,返回CompletableFuture
CompletableFuture<String> future = asyncParseService.asyncParseFile(inputStream, file.getOriginalFilename());
// 這里可以把任務(wù)ID存到Redis,用戶后續(xù)查詢
String taskId = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(taskId, "PROCESSING", 1, TimeUnit.HOURS);
// 異步處理結(jié)果(可以用回調(diào)函數(shù),把結(jié)果存到Redis)
future.whenComplete((content, ex) -> {
if (ex == null) {
redisTemplate.opsForValue().set(taskId, "SUCCESS:" + content, 1, TimeUnit.HOURS);
} else {
redisTemplate.opsForValue().set(taskId, "FAIL:" + ex.getMessage(), 1, TimeUnit.HOURS);
}
});
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", 200);
resultMap.put("msg", "異步解析任務(wù)已啟動(dòng)!");
resultMap.put("taskId", taskId); // 返回任務(wù)ID,用戶查詢結(jié)果
returnnew ResponseEntity<>(resultMap, HttpStatus.OK);
} catch (IOException e) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("code", 500);
errorMap.put("msg", "文件讀取失?。? + e.getMessage());
returnnew ResponseEntity<>(errorMap, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 查詢異步解析結(jié)果
@GetMapping("/parse/result/{taskId}")
public ResponseEntity<Map<String, Object>> getParseResult(@PathVariable String taskId) {
String result = (String) redisTemplate.opsForValue().get(taskId);
if (result == null) {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("code", 404);
errorMap.put("msg", "任務(wù)ID不存在!");
returnnew ResponseEntity<>(errorMap, HttpStatus.NOT_FOUND);
}
Map<String, Object> resultMap = new HashMap<>();
if (result.startsWith("PROCESSING")) {
resultMap.put("code", 202);
resultMap.put("msg", "任務(wù)正在處理中,請稍后查詢!");
} elseif (result.startsWith("SUCCESS:")) {
resultMap.put("code", 200);
resultMap.put("msg", "解析成功!");
resultMap.put("data", result.substring("SUCCESS:".length()));
} elseif (result.startsWith("FAIL:")) {
resultMap.put("code", 500);
resultMap.put("msg", "解析失?。? + result.substring("FAIL:".length()));
}
returnnew ResponseEntity<>(resultMap, HttpStatus.OK);
}這樣一來,用戶上傳大文件后,不用一直等待,拿到任務(wù) ID 后可以隨時(shí)查詢結(jié)果,體驗(yàn)會好很多。
優(yōu)化 4:過濾不需要的內(nèi)容
如果只需要文件里的部分內(nèi)容(比如只需要標(biāo)題和正文,不需要頁眉頁腳),可以在解析后用正則表達(dá)式過濾掉不需要的內(nèi)容,減少數(shù)據(jù)傳輸和存儲的開銷。
示例:
/**
* 過濾文件內(nèi)容(去掉頁眉頁腳、空行等)
* @param content 原始解析內(nèi)容
* @return 過濾后的內(nèi)容
*/
public String filterContent(String content) {
// 1. 去掉頁眉頁腳(假設(shè)頁眉頁腳是“第X頁”、“文檔標(biāo)題”等)
content = content.replaceAll("第\\d+頁", "");
content = content.replaceAll("文檔標(biāo)題", "");
// 2. 去掉空行
content = content.replaceAll("\\n+", "\n");
// 3. 去掉多余的空格
content = content.replaceAll("\\s+", " ");
return content.trim();
}五、常見問題與解決方案
在使用 Tika 的過程中,難免會遇到一些問題,我整理了幾個(gè)常見的問題和解決方案,幫大家避坑。
問題 1:解析文件時(shí)出現(xiàn) “Write limit exceeded” 錯(cuò)誤
原因:BodyContentHandler 的默認(rèn)緩沖區(qū)大小是 1MB,解析超過 1MB 的文件時(shí)會報(bào)錯(cuò)。
解決方案:創(chuàng)建 BodyContentHandler 時(shí)設(shè)置更大的緩沖區(qū),或者設(shè)為 - 1(不限制):
// 方式1:設(shè)置10MB緩沖區(qū)
ContentHandler contentHandler = new BodyContentHandler(10*1024*1024);
// 方式2:不限制緩沖區(qū)大小(適合超大文件)
ContentHandler contentHandler = new BodyContentHandler(-1);問題 2:解析 PDF 時(shí)出現(xiàn)亂碼
原因:PDF 文件的字體編碼不兼容,或者 Tika 缺少對應(yīng)的字體文件。
解決方案:
- 引入 PDFBox 的字體依賴:
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-fontbox</artifactId>
<version>2.0.32</version>
</dependency>- 如果是中文亂碼,確保系統(tǒng)里有中文字體(比如 Windows 的 SimSun 字體,Linux 的 WenQuanYi Zen Hei 字體)。
- 解析時(shí)設(shè)置編碼:
Metadata metadata = new Metadata();
metadata.set(Metadata.CONTENT_ENCODING, "UTF-8");
tika.parse(inputStream, metadata, fileName);問題 3:解析圖片時(shí) OCR 識別準(zhǔn)確率低
原因:Tesseract 引擎的語言包不對,或者圖片質(zhì)量太差(模糊、傾斜、字體太?。?。
解決方案:
- 確保安裝了正確的語言包(比如中文需要 chi_sim.traineddata),可以從Tesseract 語言包倉庫下載,放到 tessdata 文件夾里。
- 對圖片進(jìn)行預(yù)處理(比如調(diào)整亮度、對比度、旋轉(zhuǎn)矯正),可以用 OpenCV 或 Java 的 ImageIO 工具類。
- 設(shè)置 OCR 的參數(shù),比如提高識別精度:
TesseractOCRConfig tesseractOCRConfig = new TesseractOCRConfig();
tesseractOCRConfig.setLanguage("chi_sim");
tesseractOCRConfig.setTessDataPath("C:\\Program Files\\Tesseract-OCR\\tessdata");
tesseractOCRConfig.setPageSegMode(1); // 1表示自動(dòng)分段,提高識別精度問題 4:解析 Excel 時(shí)合并單元格內(nèi)容丟失
原因:Tika 默認(rèn)只解析合并單元格的第一個(gè)單元格內(nèi)容,其他單元格會被忽略。
解決方案:用 POI 配合 Tika,手動(dòng)處理合并單元格。示例:
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/**
* 處理Excel合并單元格,獲取完整內(nèi)容
*/
public String parseExcelWithMergedCells(InputStream inputStream) throws IOException {
Workbook workbook = new XSSFWorkbook(inputStream);
StringBuilder content = new StringBuilder();
for (Sheet sheet : workbook) {
content.append("工作表名稱:").append(sheet.getSheetName()).append("\n");
// 獲取合并單元格信息
MergedRegionFinder mergedRegionFinder = new MergedRegionFinder(sheet);
for (Row row : sheet) {
for (Cell cell : row) {
// 檢查單元格是否是合并單元格的一部分
if (mergedRegionFinder.isMergedRegion(cell.getRowIndex(), cell.getColumnIndex())) {
// 獲取合并單元格的第一個(gè)單元格內(nèi)容
Cell mergedCell = sheet.getRow(mergedRegionFinder.getMergedRegion(cell.getRowIndex(), cell.getColumnIndex()).getFirstRow())
.getCell(mergedRegionFinder.getMergedRegion(cell.getRowIndex(), cell.getColumnIndex()).getFirstColumn());
content.append(getCellValue(mergedCell)).append("\t");
} else {
content.append(getCellValue(cell)).append("\t");
}
}
content.append("\n");
}
content.append("\n");
}
workbook.close();
return content.toString();
}
// 獲取單元格的值(處理不同數(shù)據(jù)類型)
private String getCellValue(Cell cell) {
if (cell == null) {
return"";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
return cell.getCellFormula(); // 或者用cell.getNumericCellValue()獲取公式結(jié)果
default:
return"";
}
}六、總結(jié)
看到這里,相信大家已經(jīng)掌握了 SpringBoot+Apache Tika 的數(shù)據(jù)提取方法。咱們來回顧一下:
- Tika 的優(yōu)勢:萬能解析(支持 1000 + 文件格式)、用法簡單(同一套 API)、開源穩(wěn)定(Apache 項(xiàng)目)、可擴(kuò)展(支持 OCR、自定義解析器)。
- 核心用法:引入依賴→配置 Tika 實(shí)例→封裝工具類→調(diào)用接口解析,幾步就能搞定大部分文件的解析需求。
- 進(jìn)階技巧:OCR 識別圖片文字、保留表格結(jié)構(gòu)、處理加密 PDF、性能優(yōu)化,這些技巧能讓 Tika 更好地滿足項(xiàng)目需求。
- 避坑指南:常見問題的解決方案,幫大家少走彎路。
以前處理文件數(shù)據(jù)提取,我得寫幾百行代碼,還得處理各種格式兼容問題;現(xiàn)在用 SpringBoot+Tika,幾十行代碼就能搞定,開發(fā)效率直接拉滿。如果你也經(jīng)常跟文件解析打交道,趕緊試試這個(gè)組合!




























