Apache POI 中如何用SAX解析器读取Excel文件

1,559 阅读4分钟

学习使用Apache POISAX解析器库在Java中读取excel文件。在这个例子中,我们将能够。

  • 使用自定义逻辑来选择我们是否要处理一个特定的工作表(通过工作表名称)。
  • 当一个新的工作表开始或当前工作表结束时进行通知。
  • 获取工作表中的第一行作为标题。
  • 以列名和单元格值对的地图形式获取工作表中的其他行。

1.Maven的依赖性

在应用程序中添加最新版本的org.apache.poi:poiorg.apache.poi:poi-ooxml,如果尚未添加的话。

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.2</version>
</dependency>

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.2</version>
</dependency>

2.核心类

  • OPCPackage。一个.xlsx 文件是建立在OOXML包结构之上的,OPCPackage代表一个容器,可以存储多个数据对象。
  • XSSFReader:使其容易获得OOXML*.xlsx*文件的各个部分,适合于低内存sax解析。
  • DefaultHandler:为其他核心SAX2处理器类中的所有回调提供默认实现。我们扩展了这个类并覆盖了必要的方法来处理事件回调。
  • SAXParser:解析文档并将各种解析器事件的通知发送到注册的事件处理程序。
  • SharedStringsTable。它存储一个工作簿中所有工作表共享的字符串表。当一些字符串在许多行或列中重复时,它有助于提高性能。共享字符串表包含显示字符串的所有必要信息:文本、格式化属性和语音属性。

3.用SAX解析器读取Excel

3.1.重写DefaultHandler

让我们从创建解析事件的事件处理程序开始。下面的SheetHandler扩展了DefaultHandler并提供了以下方法。

  • startElement():当一个新的行或单元格开始时被调用。
  • endElement():当当前行或单元格结束时被调用。
  • readExcelFile():接收一个excel文件,并使用SAXParserXSSFReader来逐页解析该文件。
import org.apache.poi.openxml4j.opc.OPCPackage;
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.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutionException;

public class SheetHandler extends DefaultHandler
{
  protected Map<String, String> header = new HashMap<>();
  protected Map<String, String> rowValues = new HashMap<>();
  private SharedStringsTable sharedStringsTable;

  protected long rowNumber = 0;
  protected String cellId;
  private String contents;
  private boolean isCellValue;
  private boolean fromSST;

  protected static String getColumnId(String attribute) throws SAXException {
    for (int i = 0; i < attribute.length(); i++) {
      if (!Character.isAlphabetic(attribute.charAt(i))) {
        return attribute.substring(0, i);
      }
    }
    throw new SAXException("Invalid format " + attribute);
  }

  @Override
  public void startElement(String uri, String localName, String name,
                           Attributes attributes) throws SAXException {
    // Clear contents cache
    contents = "";
    // element row represents Row
    switch (name) {
      case "row" -> {
        String rowNumStr = attributes.getValue("r");
        rowNumber = Long.parseLong(rowNumStr);
      }
      // element c represents Cell
      case "c" -> {
        cellId = getColumnId(attributes.getValue("r"));
        // attribute t represents the cell type
        String cellType = attributes.getValue("t");
        if (cellType != null && cellType.equals("s")) {
          // cell type s means value will be extracted from SharedStringsTable
          fromSST = true;
        }
      }
      // element v represents value of Cell
      case "v" -> isCellValue = true;
    }
  }

  @Override
  public void characters(char[] ch, int start, int length) {
    if (isCellValue) {
      contents += new String(ch, start, length);
    }
  }

  @Override
  public void endElement(String uri, String localName, String name) {
    if (isCellValue && fromSST) {
      int index = Integer.parseInt(contents);
      contents = new XSSFRichTextString(sharedStringsTable.getItemAt(index).getString()).toString();
      rowValues.put(cellId, contents);
      cellId = null;
      isCellValue = false;
      fromSST = false;
    } else if (isCellValue) {
      rowValues.put(cellId, contents);
      isCellValue = false;
    } else if (name.equals("row")) {
      header.clear();
      if (rowNumber == 1) {
        header.putAll(rowValues);
      }
      try {
        processRow();
      } catch (ExecutionException | InterruptedException e) {
        e.printStackTrace();
      }
      rowValues.clear();
    }
  }

  protected boolean processSheet(String sheetName) {
    return true;
  }

  protected void startSheet() {
  }

  protected void endSheet() {
  }

  protected void processRow() throws ExecutionException, InterruptedException {
  }

  public void readExcelFile(File file) throws Exception {

    SAXParserFactory factory = SAXParserFactory.newInstance();
    SAXParser saxParser = factory.newSAXParser();

    try (OPCPackage opcPackage = OPCPackage.open(file)) {
      XSSFReader xssfReader = new XSSFReader(opcPackage);
      sharedStringsTable = (SharedStringsTable) xssfReader.getSharedStringsTable();

      System.out.println(sharedStringsTable.getUniqueCount());

      Iterator<InputStream> sheets = xssfReader.getSheetsData();

      if (sheets instanceof XSSFReader.SheetIterator sheetIterator) {
        while (sheetIterator.hasNext()) {
          try (InputStream sheet = sheetIterator.next()) {
            String sheetName = sheetIterator.getSheetName();
            if(!processSheet(sheetName)) {
              continue;
            }
            startSheet();
            saxParser.parse(sheet, this);
            endSheet();
          }
        }
      }
    }
  }
}

3.2.创建行处理程序

下面这个ExcelReaderHandler类扩展了上一节中给出的SheetHandler类。它重写了以下方法,因此我们可以编写我们的自定义逻辑来处理从excel文件中的每张表中读取的数据。

  • processSheet()读取:用于确定我们是否要读取工作表。它把工作表的名称作为一个参数,我们可以用它来确定决定。
  • startSheet()读取:每当一个新的工作表开始时都会被调用。
  • endSheet():在当前工作表结束时被调用。
  • processRow():在每一行被调用一次,并提供该行的单元格值。
public class ExcelReaderHandler extends SheetHandler {

  @Override
  protected boolean processSheet(String sheetName) {
    //Decide which sheets to read; Return true for all sheets
    //return "Sheet 1".equals(sheetName);
    System.out.println("Processing start for sheet : " + sheetName);
    return true;
  }

  @Override
  protected void startSheet() {
    //Any custom logic when a new sheet starts
    System.out.println("Sheet starts");
  }

  @Override
  protected void endSheet() {
    //Any custom logic when sheet ends
    System.out.println("Sheet ends");
  }

  @Override
  protected void processRow() {
    if(rowNumber == 1 && !header.isEmpty()) {
      System.out.println("The header values are at line no. " + rowNumber + " " +
          "are :" + header);
    }
    else if (rowNumber > 1 && !rowValues.isEmpty()) {

      //Get specific values here
      /*String a = rowValues.get("A");
      String b = rowValues.get("B");*/

      //Print whole row
      System.out.println("The row values are at line no. " + rowNumber + " are :" + rowValues);
    }
  }
}

4.4.演示

让我们用一个演示程序来了解如何读取excel文件。我们正在读取一个有2个工作表的文件,工作表里有一些值。

让我们使用ExcelReaderHandler来读取excel文件并打印在此过程中读取的值。

import java.io.File;
import java.net.URL;

public class ReadExcelUsingSaxParserExample {
  public static void main(String[] args) throws Exception {

    URL url = ReadExcelUsingSaxParserExample.class
        .getClassLoader()
        .getResource("howtodoinjava_demo.xlsx");

    new ExcelReaderHandler().readExcelFile(new File(url.getFile()));
  }
}

检查输出,其中有来自excel文件的单元格值。

Processing start for sheet : Employee Data
Sheet starts
The header values are at line no. 1 are :{A=ID, B=NAME, C=LASTNAME}
The row values are at line no. 2 are :{A=1, B=Amit, C=Shukla}
The row values are at line no. 3 are :{A=2, B=Lokesh, C=Gupta}
The row values are at line no. 4 are :{A=3, B=John, C=Adwards}
The row values are at line no. 5 are :{A=4, B=Brian, C=Schultz}
Sheet ends

Processing start for sheet : Random Data
Sheet starts
The header values are at line no. 1 are :{A=Key, B=Value}
The row values are at line no. 2 are :{A=1, B=a}
The row values are at line no. 3 are :{A=2, B=b}
The row values are at line no. 4 are :{A=3, B=c}
Sheet ends

5.总结

在这个Apache POI教程中,我们学会了使用SAX解析器来读取excel文件。我们也可以用这个方案来读取巨大的excel文件。