web-dev-qa-db-fra.com

Comment charger un gros fichier xlsx avec Apache POI?

J'ai un gros fichier .xlsx (141 Mo, contenant 293413 lignes de 62 colonnes chacune). Je dois effectuer certaines opérations à l'intérieur.

J'ai des problèmes avec le chargement de ce fichier (OutOfMemoryError), car POI a une grande empreinte mémoire sur les classeurs XSSF (xlsx).

Cette SO question est similaire et la solution présentée consiste à augmenter la mémoire allouée/maximale de la machine virtuelle.

Cela semble fonctionner pour ce type de fichier (9 Mo), mais pour moi, cela ne fonctionne tout simplement pas, même si vous allouez toute la mémoire système disponible. (Eh bien, ce n'est pas une surprise étant donné que le fichier est 15 fois plus volumineux)

J'aimerais savoir s'il existe un moyen de charger le classeur de manière à ne pas utiliser toute la mémoire, et pourtant, sans effectuer le traitement basé sur (entrer dans) le XML sous-jacent de XSSF. (En d'autres termes, maintenir une solution de points d'intérêt puritains)

S'il n'y a pas de solution, vous pouvez le dire ("Il n'y en a pas.") Et m'indiquer le chemin vers une solution "XML".

33
XenoRo

J'étais dans une situation similaire avec un environnement de serveur Web. La taille typique des téléchargements était d'environ 150 000 lignes et il n'aurait pas été intéressant de consommer une tonne de mémoire à partir d'une seule demande. L’API Streaming API Apache fonctionne bien pour cela, mais elle nécessite une refonte complète de votre logique de lecture. J'avais déjà beaucoup de logique de lecture utilisant l'API standard que je ne voulais pas refaire, alors je l'ai écrit à la place: https://github.com/monitorjbl/Excel-streaming-reader

Ce n'est pas entièrement un remplacement instantané de la classe XSSFWorkbook standard, mais si vous ne faites que parcourir des lignes, son comportement est similaire:

import com.monitorjbl.xlsx.StreamingReader;

InputStream is = new FileInputStream(new File("/path/to/workbook.xlsx"));
StreamingReader reader = StreamingReader.builder()
        .rowCacheSize(100)    // number of rows to keep in memory (defaults to 10)
        .bufferSize(4096)     // buffer size to use when reading InputStream to file (defaults to 1024)
        .sheetIndex(0)        // index of sheet to use (defaults to 0)
        .read(is);            // InputStream or File for XLSX file (required)

for (Row r : reader) {
  for (Cell c : r) {
    System.out.println(c.getStringCellValue());
  }
}     

Il y a des mises en garde pour l'utiliser; En raison de la structure des feuilles XLSX, toutes les données ne sont pas disponibles dans la fenêtre actuelle du flux. Toutefois, si vous essayez simplement de lire des données simples à partir des cellules, cela fonctionne assez bien pour cela.

45
monitorjbl

Une amélioration de l'utilisation de la mémoire peut être réalisée en utilisant un fichier au lieu d'un flux . (Il est préférable d'utiliser une API de streaming, mais les API de streaming ont des limitations, voir http://poi.Apache.org/ tableur/index.html )

Donc au lieu de

Workbook workbook = WorkbookFactory.create(inputStream);

faire

Workbook workbook = WorkbookFactory.create(new File("yourfile.xlsx"));

C'est selon: http://poi.Apache.org/spreadsheet/quick-guide.html#FileInputStream

Files vs InputStreams

"Lorsque vous ouvrez un classeur, un HSSFWorkbook .xls ou un .xlsx XSSFWorkbook, le classeur peut être chargé à partir d'un fichier ou d'un InputStream. L'utilisation d'un objet File permet de réduire la consommation de mémoire, tandis qu'un InputStream nécessite davantage de mémoire mettre tout le fichier en mémoire tampon. "

11
rjdkolb

Le support Excel dans Apache POI, HSSF et XSSF, prend en charge 3 modes différents. 

L'un est un "UserModel" en mémoire, complet, de type DOM, qui prend en charge la lecture et l'écriture. En utilisant les interfaces SS (SpreadSheet) communes, vous pouvez coder de manière transparente pour HSSF (.xls) et XSSF (.xlsx). Cependant, il faut beaucoup de mémoire.

POI prend également en charge une méthode de lecture en continu en lecture seule pour traiter les fichiers, EventModel. Ceci est beaucoup plus bas que UserModel et vous rapproche beaucoup du format de fichier. Pour HSSF (.xls), vous obtenez un flux d’enregistrements et éventuellement de l’aide pour leur traitement (cellules manquantes, suivi du format, etc.). Pour XSSF (.xlsx), vous obtenez des flux d’événements SAX à partir des différentes parties du fichier, qui vous aident à obtenir la bonne partie du fichier et vous permettent également de traiter facilement les petits éléments communs du fichier.

Pour XSSF (.xlsx) uniquement, POI prend également en charge une écriture en flux continu en écriture seule, adaptée aux écritures de bas niveau mais peu de mémoire. Cependant, il ne supporte que les nouveaux fichiers (certains types d’ajout sont possibles). Il n'y a pas d'équivalent HSSF, et en raison des décalages d'octet et d'index dans de nombreux enregistrements, il serait assez difficile à faire ...

Dans votre cas particulier, comme décrit dans vos commentaires de clarification, je pense que vous voudrez utiliser le code XSSF EventModel. Voir la documentation sur les POI pour commencer, puis essayez de regarder cestroisclasses dans POI et Tika qui l'utilisent pour plus de détails.

9
Gagravarr

POI inclut maintenant une API pour ces cas. SXSSF http://poi.Apache.org/spreadsheet/index.html Il ne charge pas tout dans la mémoire, il pourrait donc vous permettre de gérer ce fichier.

Remarque: j'ai lu que SXSSF fonctionnait comme une API d'écriture. Le chargement devrait être fait en utilisant XSSF sans inputstreaming du fichier (pour éviter un chargement complet en mémoire)

6
Alfabravo

Vérifiez ce post. Je montre comment utiliser l'analyseur SAX pour traiter un fichier XLSX.

https://stackoverflow.com/a/44969009/4587961

En bref, j’ai étendu org.xml.sax.helpers.DefaultHandler au traitement de la structure XML pour le fichier XLSX. t est un analyseur d'événements - SAX.

class SheetHandler extends DefaultHandler {

    private static final String ROW_EVENT = "row";
    private static final String CELL_EVENT = "c";

    private SharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;

    private List<String> cellCache = new LinkedList<>();
    private List<String[]> rowCache = new LinkedList<>();

    private SheetHandler(SharedStringsTable sst) {
        this.sst = sst;
    }

    public void startElement(String uri, String localName, String name,
                             Attributes attributes) throws SAXException {
        // c => cell
        if (CELL_EVENT.equals(name)) {
            String cellType = attributes.getValue("t");
            if(cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
        } else if (ROW_EVENT.equals(name)) {
            if (!cellCache.isEmpty()) {
                rowCache.add(cellCache.toArray(new String[cellCache.size()]));
            }
            cellCache.clear();
        }

        // Clear contents cache
        lastContents = "";
    }

    public void endElement(String uri, String localName, String name)
            throws SAXException {
        // Process the last contents as required.
        // Do now, as characters() may be called more than once
        if(nextIsString) {
            int idx = Integer.parseInt(lastContents);
            lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
            nextIsString = false;
        }

        // v => contents of a cell
        // Output after we've seen the string contents
        if(name.equals("v")) {
            cellCache.add(lastContents);
        }
    }

    public void characters(char[] ch, int start, int length)
            throws SAXException {
        lastContents += new String(ch, start, length);
    }

    public List<String[]> getRowCache() {
        return rowCache;
    }
}

Et puis j'analyse le fichier XML de présentation XLSX

private List<String []> processFirstSheet(String filename) throws Exception {
    OPCPackage pkg = OPCPackage.open(filename, PackageAccess.READ);
    XSSFReader r = new XSSFReader(pkg);
    SharedStringsTable sst = r.getSharedStringsTable();

    SheetHandler handler = new SheetHandler(sst);
    XMLReader parser = fetchSheetParser(handler);
    Iterator<InputStream> sheetIterator = r.getSheetsData();

    if (!sheetIterator.hasNext()) {
        return Collections.emptyList();
    }

    InputStream sheetInputStream = sheetIterator.next();
    BufferedInputStream bisSheet = new BufferedInputStream(sheetInputStream);
    InputSource sheetSource = new InputSource(bisSheet);
    parser.parse(sheetSource);
    List<String []> res = handler.getRowCache();
    bisSheet.close();
    return res;
}

public XMLReader fetchSheetParser(ContentHandler handler) throws SAXException {
    XMLReader parser = new SAXParser();
    parser.setContentHandler(handler);
    return parser;
}
3
Yan Khonski

Vous pouvez utiliser SXXSF au lieu d'utiliser HSSF. Je pourrais générer Excel avec 200000 lignes.

0
user1993509

Basé sur la réponse de monitorjbl et la suite de tests explorés à partir de poi, la suite a fonctionné pour moi sur un fichier xlsx à plusieurs feuilles avec 200 Ko d’enregistrements (taille> 50 Mo):

import com.monitorjbl.xlsx.StreamingReader;
. . .
try (
        InputStream is = new FileInputStream(new File("sample.xlsx"));
        Workbook workbook = StreamingReader.builder().open(is);
) {
    DataFormatter dataFormatter = new DataFormatter();
    for (Sheet sheet : workbook) {
        System.out.println("Processing sheet: " + sheet.getSheetName());
        for (Row row : sheet) {
            for (Cell cell : row) {
                String value = dataFormatter.formatCellValue(cell);
            }
        }
    }
}
0
ak2205