前言
在实际开发中,我们经常需要处理各类文件(如PDF、Word、Excel、图片等),核心需求包括识别文件真实类型(避免后缀名欺骗)和提取文件内容/元数据(如文档正文、创建时间、作者)。Apache Tika作为Apache基金会的开源项目,能高效解决这些问题,且无需手动编写不同格式的解析逻辑。
核心概念
在整合前,先明确Tika的3个核心组件,理解其工作原理:
Detector(检测器):负责识别文件的真实类型,支持通过文件头、字节流、扩展名等多维度检测,避免 “后缀名篡改” 导致的类型误判。
Parser(解析器):根据Detector识别的文件类型,调用对应的解析器提取文件内容(如文本)和元数据(如文件大小、修改时间),Tika内置了PDF、Office、XML等格式的解析器。
Metadata(元数据):存储文件的结构化信息,分为内置元数据(如Metadata.CONTENT_ENCODING、Metadata.CONTENT_TYPE)和自定义元数据(如文档作者、版本号)。
案例
1.依赖添加
备注:Tika依赖较多解析库(如POI、PDFBox),可能与项目中已有的依赖冲突(如POI版本不一致),手动排除即可。
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<version>2.9.2</version>
</dependency>
2.配置 Tika Bean
为避免重复创建Tika实例(提升性能),支持自定义配置(如超时时间、解析器优先级):
@Configuration
public class TikaSelfConfig {
/**
* 全局Tika实例(用于文件类型检测)
*/
@Bean
public Tika tika() throws TikaException, IOException, SAXException {
// 自定义Tika配置:设置文件类型检测超时时间(5秒)
TikaConfig config = new TikaConfig();
return new org.apache.tika.Tika(config) {
@Override
public String detect(java.io.InputStream stream, Metadata metadata) {
try {
// 超时控制:避免解析超大文件阻塞
return super.detect(new TimeoutInputStream(stream, 5000), metadata);
} catch (IOException e) {
throw new RuntimeException("文件类型检测超时(超过5秒)", e);
}
}
};
}
/**
* 自动检测解析器(用于内容提取)
*/
@Bean
public Parser autoDetectParser() {
// AutoDetectParser会根据文件类型自动选择解析器
return new AutoDetectParser();
}
// 注册自定义解析器(在TikaConfig中添加)
@Bean
public Parser customParser() {
return new CustomCsvParser();
}
}
public class TimeoutInputStream extends InputStream {
private final InputStream delegate;
private final long timeoutMillis;
private long lastReadTime;
public TimeoutInputStream(InputStream delegate, long timeoutMillis) {
this.delegate = delegate;
this.timeoutMillis = timeoutMillis;
this.lastReadTime = System.currentTimeMillis();
}
@Override
public int read() throws IOException {
checkTimeout();
int data = delegate.read();
if (data != -1) {
lastReadTime = System.currentTimeMillis();
}
return data;
}
private void checkTimeout() throws IOException {
long elapsed = System.currentTimeMillis() - lastReadTime;
if (elapsed > timeoutMillis) {
throw new IOException("Stream read timeout (elapsed: " + elapsed + "ms)");
}
}
// 堆代码 duidaima.com
// 重写其他read方法(read(byte[]), read(byte[], int, int)),逻辑类似
}
功能 1:文件类型检测
@Service
public class TikaFileDetectService {
private final Tika tika;
// 注入全局Tika Bean
public TikaFileDetectService(Tika tika) {
this.tika = tika;
}
/**
* 1. 基于MultipartFile(文件流)检测真实类型(推荐)
* @param file 上传的文件
* @return 真实MIME类型(如image/jpeg、application/pdf)
*/
public String detectFileByStream(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}
// 元数据:可添加文件名辅助检测(非必需,但能提升准确率)
Metadata metadata = new Metadata();
metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
// 通过文件流检测(Tika会读取文件头字节,不依赖后缀名)
try (InputStream inputStream = file.getInputStream()) {
return tika.detect(inputStream, metadata);
}
}
/**
* 2. 基于字节数组检测(适用于小文件/内存中的文件)
* @param bytes 文件字节数组
* @param fileName 文件名(辅助检测)
* @return 真实MIME类型
*/
public String detectFileByBytes(byte[] bytes, String fileName) {
if (bytes == null || bytes.length == 0) {
throw new IllegalArgumentException("字节数组不能为空");
}
return tika.detect(bytes, Metadata.TIKA_MIME_FILE);
}
/**
* 3. 基于扩展名检测(仅作辅助,准确率低)
* @param fileName 文件名(如test.pdf)
* @return 推测的MIME类型
*/
public String detectFileByExtension(String fileName) {
return tika.detect(fileName);
}
}
功能 2:文件内容与元数据提取
@Service
public class TikaContentExtractService {
private final Parser autoDetectParser;
// 注入自动检测解析器
public TikaContentExtractService(Parser autoDetectParser) {
this.autoDetectParser = autoDetectParser;
}
/**
* 提取文件的文本内容(支持PDF、Word、Excel等)
* @param file 上传的文件
* @return 提取的纯文本
*/
public String extractText(MultipartFile file) throws Exception {
if (file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}
// 1. 内容处理器:BodyContentHandler用于接收文本内容,设置容量(避免大文件OOM)
ContentHandler contentHandler = new BodyContentHandler(10 * 1024 * 1024); // 10MB上限
// 2. 元数据:存储文件的结构化信息
Metadata metadata = new Metadata();
metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
// 3. 解析上下文:用于传递解析器所需的额外信息(如密码,适用于加密文件)
ParseContext parseContext = new ParseContext();
parseContext.set(Parser.class, autoDetectParser); // 绑定当前解析器
// 4. 解析文件并提取内容
try (InputStream inputStream = file.getInputStream()) {
autoDetectParser.parse(inputStream, contentHandler, metadata, parseContext);
return contentHandler.toString();
}
}
/**
* 提取文件的元数据(如创建时间、作者、文件大小)
* @param file 上传的文件
* @return 元数据键值对(格式化输出)
*/
public String extractMetadata(MultipartFile file) throws Exception {
Metadata metadata = new Metadata();
ParseContext parseContext = new ParseContext();
parseContext.set(Parser.class, autoDetectParser);
try (InputStream inputStream = file.getInputStream()) {
autoDetectParser.parse(inputStream, new BodyContentHandler(), metadata, parseContext);
}
// 格式化元数据输出(遍历所有元数据键)
StringBuilder metadataStr = new StringBuilder();
for (String name : metadata.names()) {
metadataStr.append(name).append(": ").append(metadata.get(name)).append("\n");
}
return metadataStr.toString();
}
/**
* 提取加密文件的文本内容(支持PDF、Word、Excel等)
* @param file 上传的文件
* @return 提取的纯文本
* 1. 对旧版 Office 文档(.doc、.xls),使用 POI 的 EncryptionInfo 和 Decryptor 直接解密
* 2. 对新版 Office 文档(.docx、.xlsx),仍使用 Tika 的 PasswordProvider
*/
public String extractEncryptedText(MultipartFile file, String password) throws Exception {
ContentHandler handler = new BodyContentHandler();
Metadata metadata = new Metadata();
metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
metadata.set("password", password);
AutoDetectParser autoDetectParser = new AutoDetectParser();
ParseContext context = new ParseContext();
context.set(PasswordProvider.class,metadata1 -> password);
try (InputStream inputStream = file.getInputStream()) {
autoDetectParser.parse(inputStream, handler, metadata, context);
return handler.toString();
}
}
}
功能 3:自定义解析器
// 自定义解析器:处理.csv格式文件(示例,Tika已内置CSV解析器,此处仅演示扩展)
public class CustomCsvParser extends AbstractParser {
// 声明支持的MIME类型
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return Collections.singleton(MediaType.parse("text/csv"));
}
// 核心解析逻辑
@Override
public void parse(InputStream stream, ContentHandler handler, Metadata metadata, ParseContext context)
throws IOException, SAXException {
// 1. 设置CSV文件的元数据
metadata.add("file_format", "CSV");
metadata.add("delimiter", ",");
// 2. 读取CSV内容并写入ContentHandler
String csvContent = readInputStreamAsString(stream);
handler.characters(csvContent.toCharArray(), 0, csvContent.length());
}
private String readInputStreamAsString(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
}
}
测试代码
@Test
public void tikaFile() throws Exception {
File fakeImageFile = new File("D:\\文档\\自动巡检脚本部署说明.docx");
MultipartFile mockFile = new MockMultipartFile("file",new FileInputStream(fakeImageFile));
System.out.println(tikaContentExtractService.extractText(mockFile));
System.out.println(tikaContentExtractService.extractMetadata(mockFile));
// System.out.println(tikaContentExtractService.extractEncryptedText(mockFile,"123456"));
}