专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  北京大学出的第二份 DeepSeek ... ·  16 小时前  
OSC开源社区  ·  Bun ... ·  昨天  
程序员的那些事  ·  印度把 DeepSeek ... ·  3 天前  
程序员小灰  ·  3个令人惊艳的DeepSeek项目,诞生了! ·  2 天前  
程序猿  ·  “我真的受够了Ubuntu!” ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

POI 读取文件的最佳实践

SegmentFault思否  · 公众号  · 程序员  · 2017-12-02 08:00

正文

POI是 Apache 旗下一款读写微软家文档声名显赫的类库。应该很多人在做报表的导出,或者创建 word 文档以及读取之类的都是用过 POI。POI 也的确对于这些操作带来很大的便利性。我最近做的一个工具就是读取计算机中的 word 以及 excel 文件。下面我就两方面讲解以下遇到的一些坑:

word 篇

对于 word 文件,我需要的就是提取文件中正文的文字。所以可以创建一个方法来读取 doc 或者 docx 文件:

  1.    private static String readDoc(String filePath, InputStream is) {

  2.        String text= "";

  3.        try {

  4.            if (filePath.endsWith("doc")) {

  5.                WordExtractor ex = new WordExtractor(is);

  6.                text = ex.getText();

  7.                ex.close();

  8.                is.close();

  9.            } else if(filePath.endsWith("docx")) {

  10.                XWPFDocument doc = new XWPFDocument(is);

  11.                XWPFWordExtractor extractor = new XWPFWordExtractor(doc);

  12.                text = extractor.getText();

  13.                extractor.close();

  14.                is.close();

  15.            }

  16.        } catch (Exception e) {

  17.            logger.error(filePath, e);

  18.        } finally {

  19.            if (is != null) {

  20.                is.close();

  21.            }

  22.        }

  23.        return text;

  24.    }

理论上来说,这段代码应该对于读取大多数 doc 或者 docx 文件都是有效的。但是!!!!我发现了一个奇怪的问题,就是我的代码在读取某些 doc 文件的时候,经常会给出这样的一个异常:

  1. 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 文件,你会发现一些文件夹:

本质上 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 文件:

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

但我并不觉得这是一个很好的方法,因为我得去构建一个ZipInpuStream,这很显然不好。另外,这个操作貌似会影响到 InputStream,所以你在读取正常的 doc 文件会有问题。或者你使用 File 对象去判断是否是一个 zip 文件。但这也不是一个好方法,因为我还需要在压缩文件中读取 doc 或者 docx 文件,所以我的输入必须是 Inputstream,所以这个选项也是不可以的。 我在 stackoverflow 上和一帮老外扯了大半天,有时候我真的很怀疑这帮老外的理解能力,不过最终还是有一个大佬给出了一个让我欣喜若狂的解决方案,FileMagic。这个是一个 POI 3.17新增加的一个特性:

  1. public enum FileMagic {

  2.    /** OLE2 / BIFF8+ stream used for Office 97 and higher documents */

  3.    OLE2(HeaderBlockConstants._signature),

  4.    /** OOXML / ZIP stream */

  5.    OOXML(OOXML_FILE_HEADER),

  6.    /** XML file */

  7.    XML(RAW_XML_FILE_HEADER),

  8.    /** BIFF2 raw stream - for Excel 2 */

  9.    BIFF2(new byte[]{

  10.            0x09, 0x00, // sid=0x0009

  11.            0x04, 0x00, // size=0x0004

  12.            0x00, 0x00, // unused

  13.            0x70, 0x00  // 0x70 = multiple values

  14.    }),

  15.    /** BIFF3 raw stream - for Excel 3 */

  16.    BIFF3(new byte[]{

  17.            0x09, 0x02, // sid=0x0209

  18.            0x06, 0x00, // size=0x0006

  19.             0x00, 0x00, // unused

  20.            0x70, 0x00  // 0x70 = multiple values

  21.    }),

  22.    /** BIFF4 raw stream - for Excel 4 */

  23.    BIFF4(new byte[]{

  24.            0x09, 0x04, // sid=0x0409

  25.            0x06, 0x00, // size=0x0006

  26.            0x00, 0x00, // unused

  27.            0x70, 0x00  // 0x70 = multiple values

  28.    },new byte[]{

  29.            0x09, 0x04, // sid=0x0409

  30.            0x06, 0x00, // size=0x0006

  31.            0x00, 0x00, // unused

  32.            0x00, 0x01

  33.    }),

  34.     /** Old MS Write raw stream */

  35.    MSWRITE(

  36.            new byte[]{0x31, (byte)0xbe, 0x00, 0x00 },

  37.            new byte[]{0x32, (byte)0xbe, 0x00, 0x00 }),

  38.    /** RTF document */

  39.    RTF("{\\rtf"),

  40.    /** PDF document */

  41.    PDF("%PDF"),

  42.    // keep UNKNOWN always as last enum!

  43.    /** UNKNOWN magic */

  44.    UNKNOWN(new byte[0]);

  45.    final byte[][] magic;

  46.    FileMagic(long magic) {

  47.         this.magic = new byte[1][8];

  48.        LittleEndian.putLong(this.magic[0], 0, magic);

  49.    }

  50.    FileMagic(byte[]... magic) {

  51.        this.magic = magic;

  52.    }

  53.    FileMagic(String magic) {

  54.        this(magic.getBytes(LocaleUtil.CHARSET_1252));

  55.    }

  56.    public static FileMagic valueOf(byte[] magic) {

  57.        for (FileMagic fm : values()) {

  58.            int i=0;

  59.            boolean found = true;

  60.            for (byte[] ma : fm.magic) {

  61.                for (byte m : ma) {

  62.                     byte d = magic[i++];

  63.                    if (!(d == m || (m == 0x70 && (d == 0x10 || d == 0x20 || d == 0x40)))) {

  64.                        found = false;

  65.                        break;

  66.                    }

  67.                }

  68.                if (found) {

  69.                    return fm;

  70.                }

  71.            }

  72.        }

  73.        return UNKNOWN;

  74.    }

  75.    /**

  76.     * Get the file magic of the supplied InputStream (which MUST

  77.     *  support mark and reset).

  78.     *

  79.     * If unsure if your InputStream does support mark / reset,

  80.     *  use {@link #prepareToCheckMagic(InputStream)} to wrap it and make

  81.     *  sure to always use that, and not the original!

  82.     *

  83.     * Even if this method returns {@link FileMagic#UNKNOWN} it could potentially mean,

  84.     *  that the ZIP stream has leading junk bytes

  85.     *

  86.     * @param inp An InputStream which supports either mark/reset

  87.     */

  88.    public static FileMagic valueOf(InputStream inp) throws IOException {

  89.        if (!inp.markSupported()) {

  90.            throw new IOException("getFileMagic() only operates on streams which support mark(int)");

  91.        }

  92.         // Grab the first 8 bytes

  93.        byte[] data = IOUtils.peekFirst8Bytes(inp);

  94.        return FileMagic.valueOf(data);

  95.    }

  96.    /**

  97.     * Checks if an {@link InputStream} can be reseted (i.e. used for checking the header magic) and wraps it if not

  98.     *

  99.     * @param stream stream to be checked for wrapping

  100.     * @return a mark enabled stream

  101.     */

  102.    public static InputStream prepareToCheckMagic(InputStream stream) {

  103.        if (stream.markSupported()) {

  104.            return stream;

  105.        }

  106.         // we used to process the data via a PushbackInputStream, but user code could provide a too small one

  107.        // so we use a BufferedInputStream instead now

  108.        return new BufferedInputStream(stream);

  109.    }

  110. }

在这给出主要的代码,其主要就是根据 InputStream 前 8 个字节来判断文件的类型,毫无以为这就是最优雅的解决方式。一开始,其实我也是在想对于压缩文件的前几个字节似乎是由不同的定义的,magicmumber。因为 FileMagic 的依赖和3.16 版本是兼容的,所以我只需要加入这个类就可以了,因此我们现在读取 word 文件的正确做法是:

  1.    private static String readDoc (String filePath, InputStream is) {

  2.        String text= "";

  3.        is = FileMagic.prepareToCheckMagic(is);

  4.        try {

  5.             if (FileMagic.valueOf(is) == FileMagic.OLE2) {

  6.                WordExtractor ex = new WordExtractor(is);

  7.                text = ex.getText();

  8.                ex.close();

  9.            } else if (FileMagic.valueOf(is) == FileMagic.OOXML) {

  10.                XWPFDocument doc = new XWPFDocument(is);

  11.                XWPFWordExtractor extractor = new XWPFWordExtractor(doc);

  12.                text = extractor.getText();

  13.                extractor.close();

  14.            }

  15.        } catch (Exception e) {

  16.            logger.error("for file " + filePath, e);

  17.        } finally {

  18.            if (is != null) {

  19.                 is.close();

  20.            }

  21.        }

  22.        return text;

  23.    }

excel 篇

对于 excel 篇,我也就不去找之前的方案和现在的方案的对比了。就给出我现在的最佳做法了:

  1.    @SuppressWarnings("deprecation" )

  2.    private static String readExcel(String filePath, InputStream inp) throws Exception {

  3.        Workbook wb;

  4.        StringBuilder sb = new StringBuilder();

  5.        try {

  6.            







请到「今天看啥」查看全文