Spring Boot 整合 Apache Tika 實(shí)現(xiàn)文件類型檢測(cè)與內(nèi)容提取
前言
在實(shí)際開發(fā)中,我們經(jīng)常需要處理各類文件(如PDF、Word、Excel、圖片等),核心需求包括識(shí)別文件真實(shí)類型(避免后綴名欺騙)和提取文件內(nèi)容/元數(shù)據(jù)(如文檔正文、創(chuàng)建時(shí)間、作者)。Apache Tika作為Apache基金會(huì)的開源項(xiàng)目,能高效解決這些問題,且無(wú)需手動(dòng)編寫不同格式的解析邏輯。
核心概念
在整合前,先明確Tika的3個(gè)核心組件,理解其工作原理:
Detector(檢測(cè)器):負(fù)責(zé)識(shí)別文件的真實(shí)類型,支持通過文件頭、字節(jié)流、擴(kuò)展名等多維度檢測(cè),避免 “后綴名篡改” 導(dǎo)致的類型誤判。Parser(解析器):根據(jù)Detector識(shí)別的文件類型,調(diào)用對(duì)應(yīng)的解析器提取文件內(nèi)容(如文本)和元數(shù)據(jù)(如文件大小、修改時(shí)間),Tika內(nèi)置了PDF、Office、XML等格式的解析器。Metadata(元數(shù)據(jù)):存儲(chǔ)文件的結(jié)構(gòu)化信息,分為內(nèi)置元數(shù)據(jù)(如Metadata.CONTENT_ENCODING、Metadata.CONTENT_TYPE)和自定義元數(shù)據(jù)(如文檔作者、版本號(hào))。
案例
依賴添加
Tika依賴較多解析庫(kù)(如POI、PDFBox),可能與項(xiàng)目中已有的依賴沖突(如POI版本不一致),手動(dòng)排除即可。
<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>配置 Tika Bean
為避免重復(fù)創(chuàng)建Tika實(shí)例(提升性能),支持自定義配置(如超時(shí)時(shí)間、解析器優(yōu)先級(jí)):
@Configuration
public class TikaSelfConfig {
    /**
     * 全局Tika實(shí)例(用于文件類型檢測(cè))
     */
    @Bean
    public Tika tika() throws TikaException, IOException, SAXException {
        // 自定義Tika配置:設(shè)置文件類型檢測(cè)超時(shí)時(shí)間(5秒)
        TikaConfig config = new TikaConfig();
        return new org.apache.tika.Tika(config) {
            @Override
            public String detect(java.io.InputStream stream, Metadata metadata) {
                try {
                    // 超時(shí)控制:避免解析超大文件阻塞
                    return super.detect(new TimeoutInputStream(stream, 5000), metadata);
                } catch (IOException e) {
                    throw new RuntimeException("文件類型檢測(cè)超時(shí)(超過5秒)", e);
                }
            }
        };
    }
    /**
     * 自動(dòng)檢測(cè)解析器(用于內(nèi)容提?。?     */
    @Bean
    public Parser autoDetectParser() {
        // AutoDetectParser會(huì)根據(jù)文件類型自動(dòng)選擇解析器
        return new AutoDetectParser();
    }
    // 注冊(cè)自定義解析器(在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)");
        }
    }
    // 重寫其他read方法(read(byte[]), read(byte[], int, int)),邏輯類似
}功能 1:文件類型檢測(cè)
@Service
public class TikaFileDetectService {
    private final Tika tika;
    // 注入全局Tika Bean
    public TikaFileDetectService(Tika tika) {
        this.tika = tika;
    }
    /**
     * 1. 基于MultipartFile(文件流)檢測(cè)真實(shí)類型(推薦)
     * @param file 上傳的文件
     * @return 真實(shí)MIME類型(如image/jpeg、application/pdf)
     */
    public String detectFileByStream(MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能為空");
        }
        // 元數(shù)據(jù):可添加文件名輔助檢測(cè)(非必需,但能提升準(zhǔn)確率)
        Metadata metadata = new Metadata();
        metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
        // 通過文件流檢測(cè)(Tika會(huì)讀取文件頭字節(jié),不依賴后綴名)
        try (InputStream inputStream = file.getInputStream()) {
            return tika.detect(inputStream, metadata);
        }
    }
    /**
     * 2. 基于字節(jié)數(shù)組檢測(cè)(適用于小文件/內(nèi)存中的文件)
     * @param bytes 文件字節(jié)數(shù)組
     * @param fileName 文件名(輔助檢測(cè))
     * @return 真實(shí)MIME類型
     */
    public String detectFileByBytes(byte[] bytes, String fileName) {
        if (bytes == null || bytes.length == 0) {
            throw new IllegalArgumentException("字節(jié)數(shù)組不能為空");
        }
        return tika.detect(bytes, Metadata.TIKA_MIME_FILE);
    }
    /**
     * 3. 基于擴(kuò)展名檢測(cè)(僅作輔助,準(zhǔn)確率低)
     * @param fileName 文件名(如test.pdf)
     * @return 推測(cè)的MIME類型
     */
    public String detectFileByExtension(String fileName) {
        return tika.detect(fileName);
    }
}功能 2:文件內(nèi)容與元數(shù)據(jù)提取
@Service
public class TikaContentExtractService {
    private final Parser autoDetectParser;
    // 注入自動(dòng)檢測(cè)解析器
    public TikaContentExtractService(Parser autoDetectParser) {
        this.autoDetectParser = autoDetectParser;
    }
    /**
     * 提取文件的文本內(nèi)容(支持PDF、Word、Excel等)
     * @param file 上傳的文件
     * @return 提取的純文本
     */
    public String extractText(MultipartFile file) throws Exception {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能為空");
        }
        // 1. 內(nèi)容處理器:BodyContentHandler用于接收文本內(nèi)容,設(shè)置容量(避免大文件OOM)
        ContentHandler contentHandler = new BodyContentHandler(10 * 1024 * 1024); // 10MB上限
        // 2. 元數(shù)據(jù):存儲(chǔ)文件的結(jié)構(gòu)化信息
        Metadata metadata = new Metadata();
        metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
        // 3. 解析上下文:用于傳遞解析器所需的額外信息(如密碼,適用于加密文件)
        ParseContext parseContext = new ParseContext();
        parseContext.set(Parser.class, autoDetectParser); // 綁定當(dāng)前解析器
        // 4. 解析文件并提取內(nèi)容
        try (InputStream inputStream = file.getInputStream()) {
            autoDetectParser.parse(inputStream, contentHandler, metadata, parseContext);
            return contentHandler.toString();
        }
    }
    /**
     * 提取文件的元數(shù)據(jù)(如創(chuàng)建時(shí)間、作者、文件大?。?     * @param file 上傳的文件
     * @return 元數(shù)據(jù)鍵值對(duì)(格式化輸出)
     */
    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);
        }
        // 格式化元數(shù)據(jù)輸出(遍歷所有元數(shù)據(jù)鍵)
        StringBuilder metadataStr = new StringBuilder();
        for (String name : metadata.names()) {
            metadataStr.append(name).append(": ").append(metadata.get(name)).append("\n");
        }
        return metadataStr.toString();
    }
    /**
     * 提取加密文件的文本內(nèi)容(支持PDF、Word、Excel等)
     * @param file 上傳的文件
     * @return 提取的純文本
     * 1. 對(duì)舊版 Office 文檔(.doc、.xls),使用 POI 的 EncryptionInfo 和 Decryptor 直接解密
     * 2. 對(duì)新版 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已內(nèi)置CSV解析器,此處僅演示擴(kuò)展)
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. 設(shè)置CSV文件的元數(shù)據(jù)
        metadata.add("file_format", "CSV");
        metadata.add("delimiter", ",");
        // 2. 讀取CSV內(nèi)容并寫入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);
    }
}測(cè)試代碼
圖片
@Test
    public void tikaFile() throws Exception {
        File fakeImageFile = new File("D:\\文檔\\自動(dòng)巡檢腳本部署說(shuō)明.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"));
    }














 
 
 












 
 
 
 