POI事件模式指北(二)-Excel2007

POI事件模式指北(二)-Excel2007

1、简介

Excel2007(.xlsx)是现在最常用的Excel格式,对这种文件的读取也是非常常见的需求;同样的POI也提供用户模式(User API) 和事件模式(Event API) 两种方式供大家使用。

POI的事件模式占用内存更小,它利用基础的XML数据进行处理,适用于愿意学习.xlsx文件结构以及在java中处理XML的开发人员。

那么事不宜迟,Let‘s go!

2、Excel文件的XML结构

了解文件结构之前先来看一下准备的文件,这个文件只有一个sheet页,结构也很简单。

POI事件模式指北(二)-Excel2007

Excel2007是用XMl格式储存,将要读取的文件后缀名改为.zip或者直接用解压缩工具打开,就可以看到这个Excel文件的结构了。

POI事件模式指北(二)-Excel2007

2.1、[Content_Types].xml

[Content_Types].xml文件描述了整个Excel文件的结构,也将根据这个文件组织后面的读取工作。

POI事件模式指北(二)-Excel2007

2.1、docProps文件夹

docProps文件夹下面有两个xml,里面记录的Excel的属性信息。

POI事件模式指北(二)-Excel2007

app.xml : 记录文档的创建时间和修改时间,以及标题、作者等。

core.xml: 记录文档的其他属性,包括版本、是否只读、安全等。

2.2、xl文件夹

xl文件夹包括了需要的数据和格式信息,是重点关注的对象。

POI事件模式指北(二)-Excel2007

workbook.xml: 记录了工作表基本信息,是我们重点关注的文件之一。

styles.xml: 记录了每个单元格的样式。

worksheets: 里面包括了我们的每个sheet的信息,储存在xml文件中。

2.3、workbook.xml

workbook.xml重点关注的就是sheets和sheet两个标签

  • sheet标签中name属性记录的就是sheet的名称
  • sheet标签中r:id属性记录了当前sheet和之前提到的记录sheet信息的xml之间的对应关系,储存在_rels文件夹下的xml文件中。
  • sheet标签还有一个属性state标识来是否隐藏。

POI事件模式指北(二)-Excel2007

2.4、worksheets文件夹

一般一个Excel文件有几个sheet页,就会有几个XML文件与之对应。其中sheet页和xml文件就是根据【新建 Microsoft Excel 工作表xl\_relsworkbook.xml.rels】文件对应起来的。

POI事件模式指北(二)-Excel2007

2.5、sheet1.xml

现在回头看一下开始时展示的文件,实在是再清晰不过了。row标签代表一行、c标签代表一个单元格、v标签代表单元格中的值;但是B2的值是0,可实际的值明明是a,这是怎么回事呢?

POI事件模式指北(二)-Excel2007

其实Excel将所有字符值都储存在共享字符集中,每个用到的地方只用对应的索引来表示,a是第一个字符值,所以索引是0。

2.6、小结

至此Excel文件的基本结构就介绍的差不多了,其实数据基本都储存在sheet*.xml中了,只需要按照规则读取出来就行了。

还有诸如公式单元格,合并单元格,隐藏的单元格并没有介绍,不过了解上面这些,即使遇到其他情况也能从容应对了。

3、读取.xlsx文件实例

读取Excel2007文件的思路就是,先解压,然后依次读取各个xml文件,然后按需要的方式处理。

  1. 用OPCPackage以压缩文件的形式打开文件。
  2. 读取文件流
  3. 捕获开始和结束标签并分类处理

下面是POI官网上的示例,做了简单的修改。

想要更完整的示例,可以看官方SVN上的例子

import java.io.InputStream;
import java.util.Iterator;

import org.apache.poi.ooxml.util.SAXHelper;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;

public class ExampleEventUserModel {
    public void processOneSheet(String filename) throws Exception {
        OPCPackage pkg = OPCPackage.open(filename);
        XSSFReader r = new XSSFReader(pkg);
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = fetchSheetParser(sst);

        //选择要处理的sheet页
        //通常形式为rId#或rSheet#(这里选择了第一个)
        InputStream sheet1 = r.getSheet("rId1");
        InputSource sheetSource = new InputSource(sheet1);
        parser.parse(sheetSource);
        sheet2.close();
    }

    public void processAllSheets(String filename) throws Exception {
        OPCPackage pkg = OPCPackage.open(filename);
        XSSFReader r = new XSSFReader( pkg );
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = fetchSheetParser(sst);

        Iterator<InputStream> sheets = r.getSheetsData();
        while(sheets.hasNext()) {
            System.out.println("处理新sheet:\n");
            InputStream sheet = sheets.next();
            InputSource sheetSource = new InputSource(sheet);
            parser.parse(sheetSource);
            sheet.close();
            System.out.println("");
        }
    }

    public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException, ParserConfigurationException {
        XMLReader parser = SAXHelper.newXMLReader();
        ContentHandler handler = new SheetHandler(sst);
        parser.setContentHandler(handler);
        return parser;
    }

    /**
     * 请参阅 org.xml.sax.helpers.DefaultHandler javadocs
     */
    private static class SheetHandler extends DefaultHandler {
        private SharedStringsTable sst;
        private String lastContents;
        private boolean nextIsString;

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

        public void startElement(String uri, String localName, String name,
                                 Attributes attributes) throws SAXException {
            // c代表单元格
            if(name.equals("c")) {
                // 打印单元格
                System.out.print(attributes.getValue("r") + " - ");
                // 确定该值是否为SST
                String cellType = attributes.getValue("t");
                if(cellType != null && cellType.equals("s")) {
                    nextIsString = true;
                } else {
                    nextIsString = false;
                }
            }
            // 清除缓存
            lastContents = "";
        }

        public void endElement(String uri, String localName, String name)
                throws SAXException {
            // 获取对应的文本值
            if(nextIsString) {
                int idx = Integer.parseInt(lastContents);
                lastContents = sst.getItemAt(idx).getString();
                nextIsString = false;
            }

            // v代表单元格内容
            // Output after we've seen the string contents
            if(name.equals("v")) {
                System.out.println(lastContents);
            }
        }

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

    public static void main(String[] args) throws Exception {
        ExampleEventUserModel example = new ExampleEventUserModel();
        example.processOneSheet(args[0]);
        example.processAllSheets(args[0]);
    }
}

这个例子只处理了文本值,数字处理起来还要更容易一些,但是实际使用的时候会发现,因为xml文件中空单元格和空行直接跳过了,所以要考虑填补空白格和行的事情,只要熟悉了Excel的结构,还是不难的。

4、后记

现在微软终于开窍,使用xml格式组织Excel文件,让更方便的读取Excel文件成为可能。更贴近实战的示例我会在下一篇展示,敬请期待。


参考链接

Apache POI官网: https://poi.apache.org/index.html

上一篇:JavaSE复习日记 : 接口


下一篇:POI事件模式指北(一)-Excel2003