使用事件模式(Event API)读取Excel2007(.xlsx)文件

POI的事件模式占用内存更小,它利用基础的XML数据进行处理,适用于愿意学习.xlsx文件结构以及在java中处理XML的开发人员;也能有效预防出现java.lang.OutOfMemoryError: GC overhead limit exceeded问题。

1.了解下Excel文件的XML结构

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

markdown

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

markdown

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

markdown

1.4、xl文件夹包括了需要的数据和格式信息,是重点关注的对象
  • workbook.xml: 记录了工作表基本信息,是我们重点关注的文件之一。
  • styles.xml: 记录了每个单元格的样式。
  • worksheets: 里面包括了我们的每个sheet的信息,储存在xml文件中。
    markdown
1.5、workbook.xml重点关注的就是sheets和sheet两个标签
  • sheet标签中name属性记录的就是sheet的名称
  • sheet标签中r:id属性记录了当前sheet和之前提到的记录sheet信息的xml之间的对应关系,储存在_rels文件夹下的xml文件中。
  • sheet标签还有一个属性state标识来是否隐藏。
    重点备注信息:r:id="rId3"是获取数据关键
    markdown
1.6、一般一个Excel文件有几个sheet页,就会有几个XML文件与之对应。其中sheet页和xml文件就是根据【新建 Microsoft Excel 工作表xl_relsworkbook.xml.rels】文件对应起来的

重点备注信息:如下图所示,所有的信息都是在标签中,使用需要根据自己的当前sheel1中的格式获得数据
markdown

.读取.xlsx文件实例(java代码)

import com.inspur.evaluation.message.consume.receive.utils.StringHelper;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class POIEventModelUtil {

    public static void main(String[] args) throws Exception {
        OPCPackage pkg = OPCPackage.open("E:/ceshi/广州市评价详情明细数据20191105.xlsx", PackageAccess.READ);
        XSSFReader r = new XSSFReader(pkg);
		//根据workbook.xml中r:id的值获得流
        InputStream is = r.getSheet("rId3");
        //debug 查看转换的xml原始文件,方便理解后面解析时的处理,
        byte[] isBytes = IOUtils.toByteArray(is);
        //读取流,查看文件内容
        streamOut(new ByteArrayInputStream(isBytes));

        //下面是SST 的索引会用到的
        SharedStringsTable sst = r.getSharedStringsTable();
        System.out.println("excel的共享字符表sst------------------start");
        sst.writeTo(System.out);
        System.out.println();
        System.out.println("excel的共享字符表sst------------------end");

        XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
        List<List<String>> container = new ArrayList<>();
        parser.setContentHandler(new Myhandler(sst, container));

        InputSource inputSource = new InputSource(new ByteArrayInputStream(isBytes));
        parser.parse(inputSource);

        is.close();

        printContainer(container);
    }

    /**
     * 输出获得excel内容
     * @param container
     */
    public static void printContainer(List<List<String>> container) {
        System.out.println("excel内容------------- -start");
        for (List<String> stringList : container) {
            for (String str : stringList) {
                System.out.printf("%3s", str + " | ");
            }
            System.out.println();
        }
        System.out.println("excel内容---------------end");
    }

    /**
     * 读取流,查看文件内容
     * @param in
     * @throws Exception
     */
    public static void streamOut(InputStream in) throws Exception {
        System.out.println("excel转为xml------------start");
        byte[] buf = new byte[1024];
        int len;
        while ((len = in.read(buf)) != -1) {
            System.out.write(buf, 0, len);
        }
        System.out.println();
        System.out.println("excel转为xml------------end");
    }
}

class Myhandler extends DefaultHandler {

    //取SST 的索引对应的值
    private SharedStringsTable sst;

    public void setSst(SharedStringsTable sst) {
        this.sst = sst;
    }

    //解析结果保存
    private List<List<String>> container;

    public Myhandler(SharedStringsTable sst, List<List<String>> container) {
        this.sst = sst;
        this.container = container;
    }

    /**
     * 存储cell标签下v标签包裹的字符文本内容
     * 在v标签开始后,解析器自动调用characters()保存到 lastContents
     * 【但】当cell标签的属性 s是 t时, 表示取到的lastContents是 SharedStringsTable 的index值
     * 需要在v标签结束时根据 index(lastContents)获取一次真正的值
     */
    private String lastContents;

    //有效数据矩形区域,A1:Y2
    private String dimension;

    //根据dimension得出每行的数据长度
    private int longest;

    //上个有内容的单元格id,判断空单元格
    private String lastCellid;

    //上一行id, 判断空行
    private String lastRowid;

    // 判断单元格cell的c标签下是否有v,否则可能数据错位
    private boolean hasV = false;


    //行数据保存
    private List<String> currentRow;

    //单元格内容是SST 的索引
    private boolean isSSTIndex = false;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
      //System.out.println("startElement:"+qName);
        lastContents = "";
        if (qName.equals("dimension")) {
            dimension = attributes.getValue("ref");
            longest = covertRowIdtoInt(dimension.substring(dimension.indexOf(":") + 1));
        }
        //行开始
        if (qName.equals("row")) {
            String rowNum = attributes.getValue("r");
            //判断空行
            if (lastRowid != null) {
                //与上一行相差2, 说明中间有空行
                int gap = Integer.parseInt(rowNum) - Integer.parseInt(lastRowid);
                if (gap > 1) {
                    gap -= 1;
                    while (gap > 0) {
                        container.add(new ArrayList<>());
                        gap--;
                    }
                }
            }

            lastRowid = attributes.getValue("r");
            currentRow = new ArrayList<>();
        }
        if (qName.equals("c")) {
            String rowId = attributes.getValue("r");
            //空单元判断,添加空字符到list
            if (lastCellid != null) {
                int gap = covertRowIdtoInt(rowId) - covertRowIdtoInt(lastCellid);
                for (int i = 0; i < gap - 1; i++) {
                    currentRow.add("");
                }
            } else {
                //第一个单元格可能不是在第一列
                if (!"A1".equals(rowId)) {
                    for (int i = 0; i < covertRowIdtoInt(rowId) - 1; i++) {
                        currentRow.add("");
                    }
                }
            }
            lastCellid = rowId;

            //判断单元格的值是SST的索引,不能直接characters方法取值
            if (attributes.getValue("t") != null && attributes.getValue("t").equals("s")) {
                isSSTIndex = true;
            } else {
                isSSTIndex = false;
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {

        //行结束,存储一行数据
        if (qName.equals("row")) {
            //判断最后一个单元格是否在最后,补齐列数
            //【注意】有的单元格只修改单元格格式,而没有内容,会出现c标签下没有v标签,导致currentRow少
            if (covertRowIdtoInt(lastCellid) < longest) {
                int min = Math.min(currentRow.size(), covertRowIdtoInt(lastCellid));
                for (int i = 0; i < longest - min; i++) {
                    currentRow.add("");
                }
            }
            container.add(currentRow);
            lastCellid = null;
        }

        //单元格结束,没有v时需要补位
        if (qName.equals("c")) {
            if (!hasV) currentRow.add("");
            hasV = false;
        }

        //单元格内容标签结束,characters方法会被调用处理内容
        //2019-12-29 13:09:14  因为当前读取的sheel1.xml中内容存储大多都在<t></t>标签中,因此在此新增此单元格
        if (qName.equals("v") || qName.equals("t")) {
            hasV = true;
            //单元格的值是SST 的索引
            if (isSSTIndex) {
                String sstIndex = lastContents.toString();
                try {
                    int idx = Integer.parseInt(sstIndex);
                    XSSFRichTextString rtss = new XSSFRichTextString(
                            sst.getEntryAt(idx));
                    lastContents = rtss.toString();
                    if (StringHelper.isNotEmpty(lastContents)) {
                        currentRow.add(lastContents);
                    } else {
                        currentRow.add("");
                    }
                } catch (NumberFormatException ex) {
                    System.out.println(lastContents);
                }
            } else {
                currentRow.add(lastContents);
            }
        }
    }


    /**
     * 获取element的文本数据
     *
     * @see org.xml.sax.ContentHandler#characters
     */
    public void characters(char[] ch, int start, int length) throws SAXException {
        lastContents += new String(ch, start, length);
    }

    /**
     *
     * @param cellId 单元格定位id,行列号
     * @return
     */
    public static int covertRowIdtoInt(String cellId) {
        StringBuilder sb = new StringBuilder();
        String column = "";
        //从cellId中提取列号
        for (char c : cellId.toCharArray()) {
            if (Character.isAlphabetic(c)) {
                sb.append(c);
            } else {
                column = sb.toString();
            }
        }
        //列号字符转数字
        int result = 0;
        for (char c : column.toCharArray()) {
            result = result * 26 + (c - 'A') + 1;
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(Myhandler.covertRowIdtoInt("AB7"));
    }
}
###本文出自以下文章,
[POI事件模式指北(二)-Excel2007](https://yq.aliyun.com/articles/690513 "POI事件模式指北(二)-Excel2007")
posted @ 2019-12-29 13:38  晓之羽  阅读(1249)  评论(0编辑  收藏  举报