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

大白配小猪 2019-12-29

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

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

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

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

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

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

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

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

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

重点备注信息:如下图所示,所有的信息都是在标签中,使用需要根据自己的当前sheel1中的格式获得数据
使用事件模式(Event API)读取Excel2007(.xlsx)文件

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

```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

相关推荐