利用POI读取word、Excel文件的最佳实践教程

前言

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

POI结构说明

包名称说明

HSSF提供读写Microsoft Excel XLS格式档案的功能。

XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能。

HWPF提供读写Microsoft Word DOC格式档案的功能。

HSLF提供读写Microsoft PowerPoint格式档案的功能。

HDGF提供读Microsoft Visio格式档案的功能。

HPBF提供读Microsoft Publisher格式档案的功能。

HSMF提供读Microsoft Outlook格式档案的功能。

下面就word和excel两方面讲解以下遇到的一些坑:

word 篇

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

本质上 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,所以你在读取正常的 doc 文件会有问题。或者你使用 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);
 }
}

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

 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 篇

对于 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中的应用(这款工具主要是可以帮助你在电脑中进行内容的全文搜索),感兴趣的可以看看,欢迎 star 或者 pr。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 关于Poi读取Excel引发内存溢出问题的解决方法

    前言 最近生产环境有个老项目一直内存报警,不时的还出现内存泄漏,导致需要重启服务器,已经严重影响正常服务了. 分析 1.dump内存文件 liunx使用如下命令: ./jmap -dump:format=b,file=heap.hprof pid 2.使用Eclipse Memory Analysis进行分析 异常如下: at org.apache.poi.xssf.usermodel.XSSFRow.<init>(Lorg/openxmlformats/schemas/spreadsheet

  • java的poi技术读取和导入Excel实例

    报表输出是Java应用开发中经常涉及的内容,而一般的报表往往缺乏通用性,不方便用户进行个性化编辑.Java程序由于其跨平台特性,不能直接操纵Excel.因此,本文探讨一下POI视线Java程序进行Excel的读取和导入. 项目结构: java_poi_excel 用到的Excel文件: xls XlsMain .java 类 //该类有main方法,主要负责运行程序,同时该类中也包含了用poi读取Excel(2003版) import java.io.FileInputStream; impor

  • Java使用Apache POI库读取Excel表格文档的示例

    Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能. 项目下载页:http://poi.apache.org/download.html Apache POI 是创建和维护操作各种符合Office Open XML(OOXML)标准和微软的OLE 2复合文档格式(OLE2)的Java API.用它可以使用Java读取和创建,修改MS Excel文件.而且,还可以使用Jav

  • java使用poi读取excel内容方法实例

    复制代码 代码如下: import java.io.BufferedInputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.channels.FileChannel;import java.text.DecimalFormat;import java.text.SimpleDat

  • java使用POI读取properties文件并写到Excel的方法

    本文实例讲述了java使用POI读取properties文件并写到Excel的方法.分享给大家供大家参考.具体实现方法如下: package com.hubberspot.code; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import

  • POI读取excel简介_动力节点Java学院整理

    什么是Apache POI? Apache POI是一种流行的API,它允许程序员使用Java程序创建,修改和显示MS Office文件.这由Apache软件基金会开发使用Java分布式设计或修改Microsoft Office文件的开源库.它包含类和方法对用户输入数据或文件到MS Office文档进行解码. Apache POI组件 Apache POI包含类和方法,来将MS Office所有OLE 2文档复合.此API组件的列表如下. POIFS (较差混淆技术实现文件系统) : 此组件是所

  • java使用poi读取ppt文件和poi读取excel、word示例

    Apache的POI项目可以用来处理MS Office文档,codeplex上还有一个它的.net版本.POI项目可创建和维护操作各种基于OOXML和OLE2文件格式的Java API.大多数MS Office都是OLE2格式的.POI通HSMF子项目来支持Outlook,通过HDGF子项目来支持Visio,通过HPBF子项目来支持Publisher. 使用POI抽取Word简单示例: 要引入poi-3.7.jat和poi-scratchpad-3.7.ajr这两个包. 复制代码 代码如下: p

  • java poi读取excel操作示例(2个代码)

    项目中要求读取excel文件内容,并将其转化为xml格式.常见读取excel文档一般使用POI和JExcelAPI这两个工具.这里我们介绍使用POI实现读取excel文档. 复制代码 代码如下: /* * 使用POI读取EXCEL文件 */import java.io.File;import java.io.FileInputStream;import java.util.ArrayList; import org.apache.poi.hssf.usermodel.HSSFCell;impor

  • 利用POI读取word、Excel文件的最佳实践教程

    前言 POI是 Apache 旗下一款读写微软家文档声名显赫的类库.应该很多人在做报表的导出,或者创建 word 文档以及读取之类的都是用过 POI.POI 也的确对于这些操作带来很大的便利性.我最近做的一个工具就是读取计算机中的 word 以及 excel 文件. POI结构说明 包名称说明 HSSF提供读写Microsoft Excel XLS格式档案的功能. XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能. HWPF提供读写Microsoft Word

  • java利用POI读取excel文件的方法

    摘要:利用java读取excel文件,读取文件并获取文件中每一个sheet中的值. 一.需要提前导入的包: import java.io.File; import java.io.FileInputStream; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook

  • Java如何利用POI读取Excel行数

    这篇文章主要介绍了java如何利用POI读取Execel行数,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java 利用poi 读excel文件的操作,读取总的数据行数一般是通过调用 sheet.getLastRowNum() ;可是这样有时候会出现一些问题,例如,当其中一行的数据的确都为空,可是其原本的格式还在,并没有连带删除,这样计算出来的行数就不真实(比真实的大),还有当出现空白行时(也即某一行没有任何数据,通过Row row = sh

  • Java利用poi读取Excel详解实现

    目录 前言 第一步导入依赖 第二步实现测试类+测试 实际应用 前言 用户可以直接读取本地文件,也可以通过上传文件的形式读取excel 注意:poi对于读取到空白行的时候,会默认的认为是最后一行,将不会再读取空白行下面的数据 第一步导入依赖 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.2</vers

  • Java利用POI读取、写入Excel的方法指南

    前言 Apache POI [1] 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能.POI为"Poor Obfuscation Implementation"的首字母缩写,意为"简洁版的模糊实现". 做项目时经常有通过程序读取Excel数据,或是创建新的Excel并写入数据的需求: 网上很多经验教程里使用的POI版本都比较老了,一些API在新版里已经废弃,这里

  • Java利用EasyExcel读取写入Excel详情

    目录 EasyExcel介绍 为什么使用EasyExcel? 封装使用 例子 EasyExcel介绍 EasyExcel是一个基于Java的.快速.简洁.解决大文件内存溢出的Excel处理工具.他能让你在不用考虑性能.内存的等因素的情况下,快速完成Excel的读.写等功能. 为什么使用EasyExcel? Java解析.生成Excel比较有名的框架有Apache poi.jxl.但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但PO

  • java Apache poi 对word doc文件进行读写操作

    使用POI读写Word doc文件 Apache poi的hwpf模块是专门用来对word doc文件进行读写操作的.在hwpf里面我们使用HWPFDocument来表示一个word doc文档.在HWPFDocument里面有这么几个概念: Range:它表示一个范围,这个范围可以是整个文档,也可以是里面的某一小节(Section),也可以是某一个段落(Paragraph),还可以是拥有共同属性的一段文本(CharacterRun).   Section:word文档的一个小节,一个word文

  • 利用Java读取Word表格中文本和图片的方法实例

    目录 1. 程序环境准备 Jar导入步骤及方法: 方法1:手动导入. 方法2:Maven仓库导入. 2. Java代码 3. 文本.图片读取效果 总结 本文通过Java程序来展示如何读取Word表格,包括读取表格中的文本和图片.下面是具体实现的步骤和方法. 1. 程序环境准备 代码编译工具:IntelliJ IDEA Jdk版本:1.8.0 测试文档:Word .docx 2013 Jar包:free spire.doc.jar 3.9.0 用于测试的Word文档如下: Jar导入步骤及方法:

  • 通过Javascript读取本地Excel文件内容的代码示例

    读取本地Excel文件内容的Javascript代码: 复制代码 代码如下: <script type="text/javascript">function read_excel(){ var filePath="D:\abcd9.com.xls"; //要读取的xls    var sheet_id=2; //读取第2个表    var row_start=3; //从第3行开始读取    var tempStr='';    try{       

  • c# 根据NPOI 读取一个excel 文件的多个Sheet

    大家都知道NPOI组件可以在你本地没有安装office的情况下来 读取,创建excel文件.但是大家一般都是只默认读取一个excel文件的第一个sheet.那么如果要读取一个excel 的所有sheet 要怎么做呢? 下面就来告诉大家如何操作NPOI 读取excel 的所有sheet. 首先我们先讲解操作excel 单独创建的一个类,我命名为 EXECLHELP using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using NPOI.XSSF

随机推荐