Parsing dei file in Java con Apache Tika

Parsing dei file in Java con Apache Tika

Per parsing dei file intendo:

  • estrarre i metadati
  • estrarre il contenuto
  • visualizzare le immagini (se usiamo un componente grafico)
  • tutto ciò da tantissimi tipi di file

Questo è possibile attraverso una magnifica libreria: Apache Tika!

Oggi vi posto un estratto di un programmino che sto facendo in Java, e che prima o poi metterò sul mio canale di GitHub.

In questo esempio non ci sono componenti grafiche, quindi non potete visualizzare le immagini.

Ciò non toglie che potete aggiungere voi i componenti che vi servono.

Inoltre ho usato Maven, in quanto Apache Tika richiede diverse dipendenze.

Infine, per il design delle classi, ringrazio i suggerimenti del forum HTML.it.

A questo punto direi di cominciare; vedremo come analizzare tutti i files di una determinata directory, andando a identificare il tipo di file, o usando il parser generico nel caso non sia registrato.

Queste le dipendenze da aggiungere al pom.xml:

    <dependencies>
        <dependency>
            <groupId>org.apache.tika</groupId>
            <artifactId>tika-core</artifactId>
            <version>1.14</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tika</groupId>
            <artifactId>tika-parsers</artifactId>
            <version>1.14</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>

A questo punto aggiungiamo un pò di classi; cominciamo con la classe dove registrare il risultato del parsing:

public class FileParseResult {

    private Metadata metadataNames;
    private String content;

    public FileParseResult(Metadata metadataNames, String content) {
        this.metadataNames = metadataNames;
        this.content = content;
    }

    public Metadata getMetadataNames() {
        return metadataNames;
    }

    public void setMetadataNames(Metadata metadataNames) {
        this.metadataNames = metadataNames;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

}

Registriamo i metadati ed il contenuto.

La prossima è una classe astratta che verrà estesa dai vari parser che useremo nel nostro progetto:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.BodyContentHandler;
import org.xml.sax.SAXException;

public abstract class FileParser {

    public final FileParseResult parse(File file) throws IOException, SAXException, TikaException {
        BodyContentHandler handler = new BodyContentHandler();
        Metadata metadata = new Metadata();
        ParseContext context = new ParseContext();
        FileInputStream inputstream = new FileInputStream(file);
        Parser parser = createParser();
        parser.parse(inputstream, handler, metadata, context);
        return new FileParseResult(metadata, handler.toString());
    }

    protected abstract Parser createParser();

}

Qui vi posto solo tre tipi di parser per non farla troppo lunga; voi aggiungete tutti quelli che vi servono seguendo la linea di questi.

Il primo rappresenta il parser generico, richiamato quando l'estensione non cambacia con nessun altro parser:

import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.Parser;

public class GENERICFileParser extends FileParser {

    @Override
    protected Parser createParser() {
        return new AutoDetectParser();
    }

}

In questo caso il file viene parsato con il metodo AutoDetectParser.

import org.apache.tika.parser.Parser;
import org.apache.tika.parser.mp3.Mp3Parser;

public class MP3FileParser extends FileParser {

    @Override
    protected Parser createParser() {
        return new Mp3Parser();
    }

}

Questo è specifico per i file MP3, e sotto ne abbiamo uno per i file MS Office (ce ne uno anche per LibreOffice):

import org.apache.tika.parser.Parser;
import org.apache.tika.parser.microsoft.OfficeParser;

public class OFFICEFileParser extends FileParser {

    @Override
    protected Parser createParser() {
        return new OfficeParser();
    }

}

La prossima classe che vediamo vi sarà più chiara andando avanti con l'articolo; comunque serve per registrare i vari parser nel nostro programma.

In questo modo non dovremmo usare switch o if per impostare il parser corretto (come avevo inizialmente fatto io):

import java.util.HashMap;
import java.util.Map;

public class FileParserRegistry {

    private Map<String, FileParser> map = new HashMap<>();

    public void add(FileParser parser, String... types) {
        for (String type : types) {
            map.put(type, parser);
        }
    }

    public FileParser getForType(String type) {
        return map.get(type);
    }
}

Abbiamo due metodi:

  • con il primo agigungiamo un parser alla Map
  • con il secondo prendiamo il parser corretto

Adesso andiamo a creare una classe per incapsulare alcune informazioni dei file che andiamo interrogare (nome, path e tipo):

public class Record {

    private String nome;
    private String tipo;
    private String path;

    public Record(String nome, String tipo, String path) {
        this.nome = nome;
        this.tipo = tipo;
        this.path = path;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getTipo() {
        return tipo;
    }

    public void setTipo(String tipo) {
        this.tipo = tipo;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

}

Anche questa la useremo più avanti.

Questa, invece, la classe che si occupa di fare lo scan della directory data in input (qui usiamo ance la libreria Apache Commons IO che è sempre impostata nel pom.xml):

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.Tika;

public class Scan {

    public List scanDir(Path directory) throws IOException {
        List list = new ArrayList<>();
        Files.walkFileTree(directory, new FileVisitor() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                File f = file.toFile();
                String ext = FilenameUtils.getExtension(f.getName());
                if (f.isFile() && !ext.equals("aar")) {
                    Record record = new Record(f.getName(), new Tika().detect(f), f.getAbsolutePath());
                    list.add(record);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path t, BasicFileAttributes bfa) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path t, IOException ioe) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path t, IOException ioe) throws IOException {
                return FileVisitResult.CONTINUE;
            }
        });
        return list;
    }
}

Ho dovuto specifcare di saltare i file con estensione aar perchè la libreria andava in crush; questi file me li trovavo nell'SDK di Android, quindi magari voi non avete questo problema.

Bene, siamo arrivati al main, dove andiamo ad aggregare tutto quanto:

import java.io.File;
import java.io.IOException;
import java.util.List;
import org.apache.tika.exception.TikaException;
import org.xml.sax.SAXException;

public class Main {

    public static void main(String[] args) {
        try {
            String dir = "/home/matte/1_TEST";
            Scan scan = new Scan();
            List list = scan.scanDir(new File(dir).toPath());
            for (int i = 0; i < list.size(); i++) {
                String n = list.get(i).getNome();
                String t = list.get(i).getTipo();
                String p = list.get(i).getPath();
                parseFile(n, p, t);
            }
        } catch (IOException | SAXException | TikaException ex) {
            System.out.println(ex.getMessage());
        }
    }

    private static void parseFile(String nome, String strFile, String strType) throws IOException, SAXException, TikaException {
        File f = new File(strFile);
        FileParserRegistry registry = new FileParserRegistry();
        registry.add(new GENERICFileParser(), strType);
        registry.add(new MP3FileParser(), "audio/mpeg");
        registry.add(new OFFICEFileParser(),
                "application/vnd.ms-excel",
                "application/msword",
                "application/vnd.ms-powerpoint"
        );
        FileParser parser = registry.getForType(strType);
        FileParseResult result = parser.parse(f);
        StringBuilder sb = new StringBuilder();
        sb.append(nome).append("n");
        for (String name : result.getMetadataNames().names()) {
            sb.append(name).append(": ").append(result.getMetadataNames().get(name)).append("n");
        }
        System.out.println(sb.toString());
    }
}

Nel metodo parseFile andiamo a registrare i vari parser, indicando quali tipi di file corrispondono ad ogni parser.

Nel metodo main diamo in input una directory, e creiamo una List di Record; ogni file lo mandiamo al metodo parseFile.

Studiatevelo per bene, perchè inizialmente può sembrare un pò complicato.

Enjoy!