• 如何使用Apache Tika实现文件类型检测与内容提取功能?
  • 发布于 3天前
  • 163 热度
    0 评论
  • 梦清幽
  • 0 粉丝 48 篇博客
  •   
前言
在实际开发中,我们经常需要处理各类文件(如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"));
    }

用户评论