Рекомендации по чтению файлов POI

задняя часть

POIЭто хорошо известная библиотека классов под Apache для чтения и записи документов Microsoft. Многие люди должны были использовать POI при экспорте отчетов или создания словных документов и чтение их. POI также приносит много прохождения в эти операции. Инструмент, который я недавно сделал, это читать файлы Word и Excel с моего компьютера. Ниже я объясню некоторые из ям, встречающихся ниже в двух аспектах:

словесные статьи

Для файлов Word все, что мне нужно, это извлечь текст тела в файле. Таким образом, вы можете создать метод для чтения файла doc или docx:

    private static String readDoc(String filePath, InputStream is) {
        String text= "";
        try {
            if (filePath.endsWith("doc")) {
                WordExtractor ex = new WordExtractor(is);
                text = ex.getText();
                ex.close();
                is.close();
            } else if(filePath.endsWith("docx")) {
                XWPFDocument doc = new XWPFDocument(is);
                XWPFWordExtractor extractor = new XWPFWordExtractor(doc);
                text = extractor.getText();
                extractor.close();
                is.close();
            }
        } catch (Exception e) {
            logger.error(filePath, e);
        } finally {
            if (is != null) {
                is.close();
            }
        }
        return text;
    }

Теоретически этот код должен быть действителен для чтения большинства файлов doc или docx. Но!!!!обнаружил странную проблему,то есть когда мой код читает какие-то doc файлы,он часто выдает такое исключение:

org.apache.poi.poifs.filesystem.OfficeXmlFileException: The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents.

Что означает это исключение?С точки зрения непрофессионала, файл, который вы открыли, не является файлом doc, вы должны использовать метод чтения docx для его чтения. Но мы явно открываем файл с суффиксом doc!

На самом деле суть doc и docx разная, doc — это тип OLE2, а docx — тип OOXML. Если вы откроете файл docx с zip-файлом, вы найдете несколько папок:

По сути, файл docx представляет собой zip-файл, содержащий несколько файлов xml. Поэтому, хотя некоторые docx-файлы имеют небольшой размер, их внутренние xml-файлы действительно относительно велики, из-за чего при чтении некоторых docx-файлов, которые кажутся не очень большими, расходуется много памяти.

Затем я использую сжатый файл, чтобы открыть файл doc, и, конечно же, его внутренняя часть точно такая же, как показано выше, поэтому, по сути, мы можем думать о нем как о файле docx. Может быть, это потому, что он был сохранен в каком-то режиме совместимости, который вызвал такую ​​неприятную проблему. Итак, теперь ненадежно судить о том, является ли файл doc или docx на основе имени суффикса.

Честно говоря, я не думаю, что это должно быть редкой проблемой. Но в гугле ничего по этому поводу не нашел.how to know whether a file is .docx or .doc format from Apache POIЭтот пример через ZipInputStream Чтобы определить, является ли файл файлом docx:

boolean isZip = new ZipInputStream( fileStream ).getNextEntry() != null;

Но я не думаю, что это хороший подход, потому что мне нужно построить ZipInpuStream, что явно нехорошо. Кроме того, эта операция, по-видимому, влияет на InputStream, поэтому у вас будут проблемы с чтением обычных файлов документов. Или вы используете объект File, чтобы определить, является ли он zip-файлом. Но это тоже нехороший способ, потому что мне тоже нужно читать файлы doc или docx в сжатых файлах, поэтому мой ввод должен быть Inputstream, так что этот вариант тоже невозможен. Я долго общался с кучей иностранцев на stackoverflow.Иногда я очень сомневаюсь в способности этих иностранцев понять, но в конце концов, большой парень дал мне решение, которое привело меня в восторг.FileMagic. Это новая функция, добавленная в POI 3.17:

public enum FileMagic {
    /** OLE2 / BIFF8+ stream used for Office 97 and higher documents */
    OLE2(HeaderBlockConstants._signature),
    /** OOXML / ZIP stream */
    OOXML(OOXML_FILE_HEADER),
    /** XML file */
    XML(RAW_XML_FILE_HEADER),
    /** BIFF2 raw stream - for Excel 2 */
    BIFF2(new byte[]{
            0x09, 0x00, // sid=0x0009
            0x04, 0x00, // size=0x0004
            0x00, 0x00, // unused
            0x70, 0x00  // 0x70 = multiple values
    }),
    /** BIFF3 raw stream - for Excel 3 */
    BIFF3(new byte[]{
            0x09, 0x02, // sid=0x0209
            0x06, 0x00, // size=0x0006
            0x00, 0x00, // unused
            0x70, 0x00  // 0x70 = multiple values
    }),
    /** BIFF4 raw stream - for Excel 4 */
    BIFF4(new byte[]{
            0x09, 0x04, // sid=0x0409
            0x06, 0x00, // size=0x0006
            0x00, 0x00, // unused
            0x70, 0x00  // 0x70 = multiple values
    },new byte[]{
            0x09, 0x04, // sid=0x0409
            0x06, 0x00, // size=0x0006
            0x00, 0x00, // unused
            0x00, 0x01
    }),
    /** Old MS Write raw stream */
    MSWRITE(
            new byte[]{0x31, (byte)0xbe, 0x00, 0x00 },
            new byte[]{0x32, (byte)0xbe, 0x00, 0x00 }),
    /** RTF document */
    RTF("{\\rtf"),
    /** PDF document */
    PDF("%PDF"),
    // keep UNKNOWN always as last enum!
    /** UNKNOWN magic */
    UNKNOWN(new byte[0]);

    final byte[][] magic;

    FileMagic(long magic) {
        this.magic = new byte[1][8];
        LittleEndian.putLong(this.magic[0], 0, magic);
    }

    FileMagic(byte[]... magic) {
        this.magic = magic;
    }

    FileMagic(String magic) {
        this(magic.getBytes(LocaleUtil.CHARSET_1252));
    }

    public static FileMagic valueOf(byte[] magic) {
        for (FileMagic fm : values()) {
            int i=0;
            boolean found = true;
            for (byte[] ma : fm.magic) {
                for (byte m : ma) {
                    byte d = magic[i++];
                    if (!(d == m || (m == 0x70 && (d == 0x10 || d == 0x20 || d == 0x40)))) {
                        found = false;
                        break;
                    }
                }
                if (found) {
                    return fm;
                }
            }
        }
        return UNKNOWN;
    }

    /**
     * Get the file magic of the supplied InputStream (which MUST
     *  support mark and reset).<p>
     *
     * If unsure if your InputStream does support mark / reset,
     *  use {@link #prepareToCheckMagic(InputStream)} to wrap it and make
     *  sure to always use that, and not the original!<p>
     *
     * Even if this method returns {@link FileMagic#UNKNOWN} it could potentially mean,
     *  that the ZIP stream has leading junk bytes
     *
     * @param inp An InputStream which supports either mark/reset
     */
    public static FileMagic valueOf(InputStream inp) throws IOException {
        if (!inp.markSupported()) {
            throw new IOException("getFileMagic() only operates on streams which support mark(int)");
        }

        // Grab the first 8 bytes
        byte[] data = IOUtils.peekFirst8Bytes(inp);

        return FileMagic.valueOf(data);
    }


    /**
     * Checks if an {@link InputStream} can be reseted (i.e. used for checking the header magic) and wraps it if not
     *
     * @param stream stream to be checked for wrapping
     * @return a mark enabled stream
     */
    public static InputStream prepareToCheckMagic(InputStream stream) {
        if (stream.markSupported()) {
            return stream;
        }
        // we used to process the data via a PushbackInputStream, but user code could provide a too small one
        // so we use a BufferedInputStream instead now
        return new BufferedInputStream(stream);
    }
}

Здесь приведен основной код, который в основном судит о типе файла по первым 8 байтам InputStream.Я не думаю, что это самое элегантное решение. Сначала я действительно думал, что первые несколько байтов сжатого файла, кажется, определяются по-разному,magicmumber. Поскольку зависимости FileMagic совместимы с версией 3.16, мне нужно добавить только этот класс, поэтому правильный способ чтения текстовых файлов сейчас таков:

    private static String readDoc (String filePath, InputStream is) {
        String text= "";
        is = FileMagic.prepareToCheckMagic(is);
        try {
            if (FileMagic.valueOf(is) == FileMagic.OLE2) {
                WordExtractor ex = new WordExtractor(is);
                text = ex.getText();
                ex.close();
            } else if(FileMagic.valueOf(is) == FileMagic.OOXML) {
                XWPFDocument doc = new XWPFDocument(is);
                XWPFWordExtractor extractor = new XWPFWordExtractor(doc);
                text = extractor.getText();
                extractor.close();
            }
        } catch (Exception e) {
            logger.error("for file " + filePath, e);
        } finally {
            if (is != null) {
                is.close();
            }
        }
        return text;
    }

Эксель статьи

Для статьи excel я не буду искать сравнение между предыдущей схемой и текущей схемой. Вот моя текущая лучшая практика:

    @SuppressWarnings("deprecation" )
    private static String readExcel(String filePath, InputStream inp) throws Exception {
        Workbook wb;
        StringBuilder sb = new StringBuilder();
        try {
            if (filePath.endsWith(".xls")) {
                wb = new HSSFWorkbook(inp);
            } else {
                wb = StreamingReader.builder()
                        .rowCacheSize(1000)    // number of rows to keep in memory (defaults to 10)
                        .bufferSize(4096)     // buffer size to use when reading InputStream to file (defaults to 1024)
                        .open(inp);            // InputStream or File for XLSX file (required)
            }
            sb = readSheet(wb, sb, filePath.endsWith(".xls"));
            wb.close();
        } catch (OLE2NotOfficeXmlFileException e) {
            logger.error(filePath, e);
        } finally {
            if (inp != null) {
                inp.close();
            }
        }
        return sb.toString();
    }

    private static String readExcelByFile(String filepath, File file) {
        Workbook wb;
        StringBuilder sb = new StringBuilder();
        try {
            if (filepath.endsWith(".xls")) {
                wb = WorkbookFactory.create(file);
            } else {
                wb = StreamingReader.builder()
                        .rowCacheSize(1000)    // number of rows to keep in memory (defaults to 10)
                        .bufferSize(4096)     // buffer size to use when reading InputStream to file (defaults to 1024)
                        .open(file);            // InputStream or File for XLSX file (required)
            }
            sb = readSheet(wb, sb, filepath.endsWith(".xls"));
            wb.close();
        } catch (Exception e) {
            logger.error(filepath, e);
        }
        return sb.toString();
    }

    private static StringBuilder readSheet(Workbook wb, StringBuilder sb, boolean isXls) throws Exception {
        for (Sheet sheet: wb) {
            for (Row r: sheet) {
                for (Cell cell: r) {
                    if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
                        sb.append(cell.getStringCellValue());
                        sb.append(" ");
                    } else if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
                        if (isXls) {
                            DataFormatter formatter = new DataFormatter();
                            sb.append(formatter.formatCellValue(cell));
                        } else {
                            sb.append(cell.getStringCellValue());
                        }
                        sb.append(" ");
                    }
                }
            }
        }
        return sb;
    }

На самом деле, для чтения в Excel самая большая проблема, с которой сталкивается мой инструмент, — это переполнение памяти. Это часто вызывает проблему переполнения памяти при чтении некоторых очень больших файлов Excel. Затем я наконец нашел отличный инструментexcel-streaming-reader, он может выполнять потоковое чтение файлов xlsx, разбивать некоторые особенно большие файлы на маленькие файлы для чтения.

Другая оптимизация заключается в том, что в сценарии, где можно использовать объект File, я использую объект File для чтения файла вместо использования InputStream для чтения, потому что использование InputStream требует загрузки всего этого в память, так что это очень использование памяти .

Наконец, моя маленькая хитрость заключается в использованииcell.getCellTypeЧтобы уменьшить количество данных, потому что мне нужно получить только строковое содержимое некоторого текста и чисел.

Выше приведены некоторые из моих исследований и открытий в использовании POI для чтения файлов, я надеюсь, что они могут быть вам полезны. Приведенные выше примеры также есть в одном из моих инструментов.everywhereПриложение в (это инструмент в основном для помощи в выполнении полнотекстового поиска контента на компьютере), если интересно, можете глянуть Добро пожаловать в звезду или пр.