Java小知識----POI事件模式讀取Excel 2007


一、知識背景

1.讀取excel的方法選擇問題

java中讀excel中的時間,我們通常用POI去解析,在使用new HSSFWorkbook(NEW FileInputStream(excelFile))來讀取Workbook,對Excel2003以前(包括2003)的版本沒有問題,但讀取Excel2007時發生如下異常:
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. You need TO CALL a different part of POI TO PROCESS this DATA (eg XSSF instead of HSSF)
        該錯誤意思是說,文件中的數據是用Office2007+XML保存的,而現在卻調用OLE2 Office文檔處理,應該使用POI不同的部分來處理這些數據,比如使用XSSF來代替HSSF。
        於是按提示使用XSSF代替HSSF,用new XSSFWorkbook(excelFile)來讀取Workbook,對Excel2007沒有問題了,可是在讀取Excel2003以前(包括2003)的版本時卻發生了如下新異常:
org.apache.poi.openxml4j.exceptions.InvalidOperationException: Can't open the specified file: '*.xls'
        該錯誤是說,操作無效,不能打開指定的xls文件。
        下載POI的源碼后進行單步調試,發現剛開始的時候還是對的,但到ZipFile類后就找不到文件了,到網上查了下,原來是XSSF不能讀取Excel2003以前(包括2003)的版本,這樣的話,就需要在讀取前判斷文件是2003前的版本還是2007的版本,然后對應調用HSSF或XSSF來讀取。
        簡而言之:由於HSSFWorkbook只能操作excel2003一下版本,XSSFWorkbook只能操作excel2007以上版本,所以利用Workbook接口創建對應的對象操作excel來處理兼容性

2.讀取excel包含多sheet多數據的時候,出現內存溢出的問題。

POI提供UserModel和事件驅動兩種方式讀取excel。UserModel方式操作簡潔,但是內存消耗大,稍微大一點的excel讀取就會報內存溢出

二、解析步驟

當使用POI事件模式解析Excel XLSX文檔時:

  • POI根據xlsx文檔的路徑path獲取到文件File - file
  • 使用java.util.zip.ZipFile打開file文件 - zip
  • 從zip中獲取到[Content_Types].xml
  • 解析[Content_Types].xml,記錄解析出Excel各個xml名稱:ArrayList
  • Excel解析成ZipPackage實例對象

三、代碼樣例

1、從DefaultHandler派生事件處理類ExcelAbstract

 

import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.apache.poi.openxml4j.opc.OPCPackage;
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;

/**
 * POI事件驅動讀取Excel文件的抽象類。
 * 
 * @author  Charles
 * 
 */
public abstract class ExcelAbstract extends DefaultHandler {
    private SharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;
    private String curCellName= "";
    private int curRow = 0;
   private boolean numberFlag;
    private boolean isTElement;

    /**
     * 讀取當前行的數據。key是單元格名稱如A1,value是單元格中的值。如果單元格式空,則沒有數據。
     */
    private Map<String, String> rowValueMap = new HashMap<>();

    /**
     * 處理單行數據的回調方法。
     * 
     * @param curRow 當前行號
     * @param rowValueMap 當前行的值
     * @throws SQLException
     */
    public abstract void optRows(int curRow, Map<String, String> rowValueMap);

    /**
     * 讀取Excel指定sheet頁的數據。
     * 
     * @param filePath 文件路徑
     * @param sheetNum sheet頁編號.從1開始。
     * @throws Exception
     */
    public void readOneSheet(String filePath, int sheetNum) throws Exception {
        OPCPackage pkg = OPCPackage.open(filePath);
        XSSFReader r = new XSSFReader(pkg);
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = getSheetParser(sst);

        // 根據 rId# 或 rSheet# 查找sheet
        InputStream sheet2 = r.getSheet("rId" + sheetNum);
        InputSource sheetSource = new InputSource(sheet2);
        parser.parse(sheetSource);
        sheet2.close();
        pkg.close();
    }

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

        XMLReader parser = getSheetParser(sst);
        
        SheetIterator sheets = (SheetIterator) r.getSheetsData();
        while(sheets.hasNext){
                InputStream sheet = sheets.next();
                  
                InputSource sheetSource = new InputSource(sheet2);
                parser.parse(sheetSource);
                sheet2.close();
        }
        pkg.close();
    }

    @Override
    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        // c => 單元格
        if (name.equals("c")) {
            
            // 如果下一個元素是 SST 的索引,則將nextIsString標記為true
            String cellType = attributes.getValue("t");
            if (cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
            String cellNumberType = attributes.getValue("s");
            if (cellNumberType .equals("2")) {
                numberFlag= true;
            } else {
                numberFlag= false;
            }
        }
        if (name.equals("t")) {
           isTElement= true;
        } else {
           isTElement= false;
        }
        // 置空
        lastContents = "";
        
        /**
         * 記錄當前讀取單元格的名稱
         */
        String cellName = attributes.getValue("r");
        if (cellName != null && !cellName.isEmpty()) {
            curCellName = cellName;
        }
    }

    @Override
    public void endElement(String uri, String localName, String name) throws SAXException {
        // 根據SST的索引值的到單元格的真正要存儲的字符串
        // 這時characters()方法可能會被調用多次
        if (nextIsString) {
            int idx = Integer.parseInt(lastContents);
            lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString(); 
        }

        if(isTElement){
            String value  = lastContents.trim();
            rowValueMap.put(curCellName,value);
            isTElement = false;
        }
        // v => 單元格的值,如果單元格是字符串則v標簽的值為該字符串在SST中的索引
        // 將單元格內容加入rowlist中,在這之前先去掉字符串前后的空白符
        else if (name.equals("v")) {
            String value = lastContents.trim();
            value = value.equals("") ? " " : value;
            if(numberFlag){
                BigDecimal bd = new BigDecimal(value);
                value = bd.setScale(3,BigDecimal.ROUND_UP).toString();
            }
            rowValueMap.put(curCellName, value);
        } else {
            // 如果標簽名稱為 row ,這說明已到行尾,調用 optRows() 方法
            if (name.equals("row")) {
                optRows(curRow, rowValueMap);
                rowValueMap.clear();
                curRow++;
            }
        }
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
        // 得到單元格內容的值
        lastContents += new String(ch, start, length);
    }
    
    /**
     * 獲取單個sheet頁的xml解析器。
     * @param sst
     * @return
     * @throws SAXException
     */
    private XMLReader getSheetParser(SharedStringsTable sst) throws SAXException {
        XMLReader parser = XMLReaderFactory.createXMLReader("com.sun.org.apache.xerces.internal.parsers.SAXParser");
        this.sst = sst;
        parser.setContentHandler(this);
        return parser;
    }
}
            

 

 

2、從ExcelAbstract派生ExcelReaderUtil處理每一行數據

 

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.poi.hssf.usermodel.HSSFDateUtil;

/**
 * Excel讀取公共類。
 * @author Charles
 * 
 */
public class ExcelReaderUtil extends ExcelAbstract {
    
    /**
     * 提取列名稱的正則表達式
     */
    private static final String DISTILL_COLUMN_REG = "^([A-Z]{1,})";
    
    /**
     * 讀取excel的每一行記錄。map的key是列號(A、B、C...), value是單元格的值。如果單元格是空,則沒有值。
     */
    private List<Map<String, String>> dataList = new ArrayList<>();
    
    @Override
    public void optRows(int curRow, Map<String, String> rowValueMap) {
        
        Map<String, String> dataMap = new HashMap<>();
        rowValueMap.forEach((k,v)->dataMap.put(removeNum(k), v));
        dataList.add(dataMap);
    }
    
    /**
     * 日期數字轉換為字符串。
     * 
     * @param dateNum excel中存儲日期的數字
     * @return 格式化后的字符串形式
     */
    public static String dateNum2Str(String dateNum) {
        Date date = HSSFDateUtil.getJavaDate(Double.parseDouble(dateNum));
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        return formatter.format(date);
    }
    
    /**
     * 刪除單元格名稱中的數字,只保留列號。
     * @param cellName 單元格名稱。如:A1
     * @return 列號。如:A
     */
    private String removeNum(String cellName) {
        Pattern pattern = Pattern.compile(DISTILL_COLUMN_REG);
        Matcher m = pattern.matcher(cellName);
        if (m.find()) {
            return m.group(1);
        }
        
        return "";
    }

    public List<Map<String, String>> getDataList() {
        return dataList;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM