POI:使用XSSFWorkbook與SXSSFWorkbook在處理Excel合併時容易出現的問題

最近在做的需求中需要將兩個Excel合併。
首先講下POI中處理Excel的幾種方式吧。
1.HSSFWorkbook,用來處理.xls後綴的Excel,即適用於Excel2003以前(包括2003)的版本。因爲其最大隻能處理65535行的數據,所以現在已經很少使用了,所以本文直接忽略該方式。
2.XSSFWorkbook是現在處理Excel比較常見的方式。其適用於.xlsx後綴的Excel,即Excel2007後的版本。能夠最多處理104萬行數據。但是其在讀取/處理Excel時會一口氣將Excel內容寫入到內存,因此在處理的Excel文件較大時可能打爆內存,造成OOM異常。
3.SXSSFWorkbook。相當於是XSSFWorkbook的改良版本,在初始化SXSSFWorkbook實例時,需要填寫一個緩衝行數參數(默認100行),當讀入到內存中的數據超過該數值後,會像隊列一樣將最前面的數據保存到硬盤中,從而避免出現OOM。這麼一看該方式簡直完美啊,不過因爲超過緩存行的數據都寫到硬盤中了,所以如果你想要獲取這塊的內容(比如複製這塊內容到另一個Excel中)就會發現取不到了,因爲不在內存中,所以無法通過SXSSFWorkbook實例獲取該部分內容。

首先講下Spring Boot中使用POI的方式,在pom中引入如下包,具體適用版本自行選擇

		<dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>

1.SXSSFWorkbook

SXSSFWorkbook是無法直接讀取Excel的,需要通過XSSFWorkbook讀取Excel,然後使用XSSFWorkbook實例創建SXSSFWorkbook,因此如果想要讀取一個超大的Excel,後果你懂的=.=。
可以來試下,嘗試讀取一個有50萬行數據的Excel(大小14.5M)

	public void getExcel() throws IOException {
        //3.xlsx 有50萬行數據
        File file2 = new File("D:\\excel\\3.xlsx");
        FileInputStream inputStream2 =  new FileInputStream(file2);
        XSSFWorkbook wb = new XSSFWorkbook(inputStream2);

        SXSSFWorkbook swb = new SXSSFWorkbook(wb);
    }

很明顯OOM了。
在這裏插入圖片描述
我們可以看下SXSSFWorkbook的源碼:

public static final int DEFAULT_WINDOW_SIZE = 100;

//0
/**
*可以看出實際調用的是也是SXSSFWorkbook(XSSFWorkbook workbook)
**/
public SXSSFWorkbook(){
	this(null /*workbook*/);
}

//1
/**
*使用XSSFWorkbook新建SXSSFWorkbook,會使用默認緩存行,即100行
**/
public SXSSFWorkbook(XSSFWorkbook workbook){
	this(workbook, DEFAULT_WINDOW_SIZE);
}

//2
/**
*默認不壓縮
**/
public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize){
	this(workbook,rowAccessWindowSize, false);
}

//3
/**
*抱歉不知道useSharedStringsTable是幹啥的...
*不過從shared string table - a cache of strings in this workbook註釋看這個屬性和緩存相關
**/
public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles){
	this(workbook,rowAccessWindowSize, compressTmpFiles, false);
}

//4
/**
*如果使用XSSFWorkbook 創建SXSSFWorkbook的話是遍歷了XSSFWorkbook 的sheet,然後新建了一遍SXSSFWorkbook的sheet。(有點類似拷貝)
**/
public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles, boolean useSharedStringsTable){
	setRandomAccessWindowSize(rowAccessWindowSize);
	setCompressTempFiles(compressTmpFiles);
	if (workbook == null) {
		_wb=new XSSFWorkbook();
		_sharedStringSource = useSharedStringsTable ? _wb.getSharedStringSource() : null;
	} else {
		_wb=workbook;
		_sharedStringSource = useSharedStringsTable ? _wb.getSharedStringSource() : null;
		for ( Sheet sheet : _wb ) {
			createAndRegisterSXSSFSheet( (XSSFSheet)sheet );
		}
	}
}

從源碼中能看出創建SXSSFWorkbook的過程類似於拷貝。

2.Excel合併

那麼如果我們想將兩個Excel合併成一個Excel,多個sheet該怎麼做呢?
最好的方式是在Excel產生前,把數據源都拿到,然後一次性生成一個Excel。但是很多情況是數據源是不同的,那麼就只能通過讀取Excel來合併了,但是從上面我們知道,當讀取一個很大的Excel時,極大可能會出現OOM異常。所以本文介紹的合併,只限定於一個小Excel文件和一個有數據源的超大Excel的合併。或者是多個小Excel的合併。至於兩個超大Excel的合併(沒有數據源),可以使用阿里的開源項目EasyExcel

直接使用SXSSFWorkbook合併Excel時出現的問題。

首先放上SXSSFUtils.java ,處理SXSSFWorkbook拷貝的工具類。

	public class SXSSFUtils {
		/**
		 * @param fromSheet
		 * @param toSheet
		 */
		public static void mergeSheetAllRegion(Sheet fromSheet, Sheet toSheet) {
			int num = fromSheet.getNumMergedRegions();
			CellRangeAddress cellR = null;
			for (int i = 0; i < num; i++) {
				cellR = fromSheet.getMergedRegion(i);
				toSheet.addMergedRegion(cellR);
			}
		}
	 
		/**
		 * @param wb
		 * @param fromCell
		 * @param toCell
		 */
		public static void copyCell(SXSSFWorkbook wb, Cell fromCell, Cell toCell) {
			toCell.setCellStyle(fromCell.getCellStyle());
			if (fromCell.getCellComment() != null) {
				toCell.setCellComment(fromCell.getCellComment());
			}

			int fromCellType = fromCell.getCellType();
			toCell.setCellType(fromCellType);
			if (fromCellType == XSSFCell.CELL_TYPE_NUMERIC) {
				if (XSSFDateUtil.isCellDateFormatted(fromCell)) {
					toCell.setCellValue(fromCell.getDateCellValue());
				} else {
					toCell.setCellValue(fromCell.getNumericCellValue());
				}
			} else if (fromCellType == XSSFCell.CELL_TYPE_STRING) {
				toCell.setCellValue(fromCell.getRichStringCellValue());
			} else if (fromCellType == XSSFCell.CELL_TYPE_BLANK) {
				// nothing21
			} else if (fromCellType == XSSFCell.CELL_TYPE_BOOLEAN) {
				toCell.setCellValue(fromCell.getBooleanCellValue());
			} else if (fromCellType == XSSFCell.CELL_TYPE_ERROR) {
				toCell.setCellErrorValue(fromCell.getErrorCellValue());
			} else if (fromCellType == XSSFCell.CELL_TYPE_FORMULA) {
				toCell.setCellFormula(fromCell.getCellFormula());
			} else { // nothing29
			}
	 
		}
	 
		/**
		 * @param wb
		 * @param oldRow
		 * @param toRow
		 */
		public static void copyRow(SXSSFWorkbook wb, Row oldRow, Row toRow) {
			toRow.setHeight(oldRow.getHeight());
			for (Iterator cellIt = oldRow.cellIterator(); cellIt.hasNext();) {
				Cell tmpCell = (Cell) cellIt.next();
				Cell newCell = toRow.createCell(tmpCell.getColumnIndex());
				copyCell(wb, tmpCell, newCell);
			}
		}
	 
		/**
		 * @param wb
		 * @param fromSheet
		 * @param toSheet
		 */
		public static void copySheet(SXSSFWorkbook wb, Sheet fromSheet, Sheet toSheet) {
			mergeSheetAllRegion(fromSheet, toSheet);

			for (Iterator rowIt = fromSheet.rowIterator(); rowIt.hasNext();) {
				Row oldRow = (Row) rowIt.next();
				Row newRow = toSheet.createRow(oldRow.getRowNum());
				copyRow(wb, oldRow, newRow);
			}
		}
	 
		public class XSSFDateUtil extends DateUtil {
	 
		}
	}

(1)我們先試下合併兩個自己創建的SXSSFWorkbook

如下,創建兩個各有5W行數據的Excel,這裏SXSSFWorkbook緩存行設置爲1000,目的是將swb2合併到swb中。

	@RequestMapping("/mergeBySXSSF")
    public void mergeBySXSSF() throws IOException {
        System.out.println("start");
        SXSSFWorkbook swb = new SXSSFWorkbook(1000);
        SXSSFSheet sheet = swb.createSheet("1");
        
        SXSSFWorkbook swb2 = new SXSSFWorkbook(1000);
        SXSSFSheet sheet2 = swb2.createSheet("2");

        for (int i = 0; i < 50000; i++) {
            SXSSFRow row1 = sheet.createRow(i);
            SXSSFRow row2 = sheet2.createRow(i);
            for (int j = 0; j < 10; j++) {
                row1.createCell(j).setCellValue("hello SXSSF:" + j);
                row2.createCell(j).setCellValue("hello SXSSF:" + j);
            }
        }

        for(int i = 0;i<swb2.getNumberOfSheets();i++){
            Sheet oldSheet = swb2.getSheetAt(i);
            Sheet newSheet = swb.createSheet(oldSheet.getSheetName());
            SXSSFUtils.copySheet(swb,oldSheet,newSheet);
        }

        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("D:\\excel\\23.xlsx"));
        swb.write(outputStream);
        outputStream.flush();

        System.out.println("end");
    }

如上所示,會通過遍歷swb2的sheet,同時在swb創建一個同名sheet,SXSSFUtils.copySheet的作用是遍歷sheet中的行列,將其從老的sheet中複製一份到新的sheet中,從而達到合併Excel的目的。
結果如下
sheet 1是swb的,沒問題。
在這裏插入圖片描述
然後是sheet 2,這個是swb2的,也是複製合併到swb中的,可以看到很有意思的情況,前49000行都是空的,然後後1000行是正確的。
在這裏插入圖片描述
爲什麼會這樣呢?原因是緩存行,上面的兩個SXSSFWorkbook的緩存行都是1000,也就是說當超過1000行時,前面的數據都會保存到硬盤中,而我們在複製時用的是SXSSFWorkbook實體,是存在內存中的,所以拷貝是就只能複製到最後的1000行了。
如果我們再創建一個SXSSFWorkbook,然後依次把sbwsbw2合併到新的SXSSFWorkbook中,name兩個sheet應該都會缺失前49000行數據。

(2)現在我們試下讀取兩個已有的Excel,然後再合併會發生什麼。

20.xlsx和21.xlsx都是有5W行數據的,20.xlsx只有一個sheet(名字是 1),21.xlsx只有一個sheet(名字是 2)

	@RequestMapping("/mergeBySXSSF")
    public void mergeBySXSSF() throws IOException {
        System.out.println("start");
        File file = new File("D:\\excel\\20.xlsx");
        FileInputStream inputStream =  new FileInputStream(file);
        XSSFWorkbook wb = new XSSFWorkbook(inputStream);
        SXSSFWorkbook swb = new SXSSFWorkbook(wb);

        File file2 = new File("D:\\excel\\21.xlsx");
        FileInputStream inputStream2 =  new FileInputStream(file2);
        XSSFWorkbook wb2 = new XSSFWorkbook(inputStream2);
        SXSSFWorkbook swb2 = new SXSSFWorkbook(wb2);

        for(int i = 0;i<swb2.getNumberOfSheets();i++){
            Sheet oldSheet = swb2.getSheetAt(i);
            Sheet newSheet = swb.createSheet(oldSheet.getSheetName());
            SXSSFUtils.copySheet(swb,oldSheet,newSheet);
        }

        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("D:\\excel\\23.xlsx"));
        swb.write(outputStream);
        outputStream.flush();

        System.out.println("end");
    }

合併結果
sheet 1 正常
在這裏插入圖片描述
然後sheet 2 直接就是空的。在這裏插入圖片描述
我們打個斷點
在這裏插入圖片描述
可以看到雖然可以獲取到swb2的sheet數,也能獲取到該sheet,但是獲取不到這個sheet的最後一行行數,按理說這個數應該是50000,但是查出來是0,這裏原因未知。
因爲查不到行信息,所以拷貝時就全是空了。

(3)現在我們試下讀取兩個已有的Excel,然後後面合併的Excel(swb2)用XSSFWorkbook會如何呢。

	@RequestMapping("/mergeBySXSSF")
    public void mergeBySXSSF() throws IOException {
        System.out.println("start");
        File file = new File("D:\\excel\\20.xlsx");
        FileInputStream inputStream =  new FileInputStream(file);
        XSSFWorkbook wb = new XSSFWorkbook(inputStream);
        SXSSFWorkbook swb = new SXSSFWorkbook(wb);

        File file2 = new File("D:\\excel\\21.xlsx");
        FileInputStream inputStream2 =  new FileInputStream(file2);
        XSSFWorkbook wb2 = new XSSFWorkbook(inputStream2);
     
        for(int i = 0;i<wb2.getNumberOfSheets();i++){
            Sheet oldSheet = wb2.getSheetAt(i);
            Sheet newSheet = swb.createSheet(oldSheet.getSheetName());
            SXSSFUtils.copySheet(swb,oldSheet,newSheet);
        }

        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("D:\\excel\\23.xlsx"));
        swb.write(outputStream);
        outputStream.flush();

        System.out.println("end");
    }

結果如下:
在這裏插入圖片描述
在這裏插入圖片描述
數據終於全了,那麼又回到最初的問題了,如果我想要讀取一個超大的Excel來進行合併的話 要怎麼辦呢?因爲SXSSFWorkbook需要通過XSSFWorkbook讀取Excel,而XSSFWorkbook會將Excel一口氣讀入內存,一般大於2M的Excel都會拋OOM異常。
因爲我這裏的業務場景是會查詢數據源,然後生成Excel(這個Excel很大),然後會從其他服務器上下載一個小的Excel合併,所以我這裏可以直接在生成大Excel時,在保存前,往SXSSFWorkbook中拷貝那個小的Excel(小的Excel下載下來後用XSSFWorkbook讀取,不包裝爲SXSSFWorkbook),然後就能滿足業務需求了。
大致方式如下

    @RequestMapping("/mergeBySXSSF")
    public void mergeBySXSSF() throws IOException {
        System.out.println("start");

		//用數據源的數據生成Excel,因爲數據量多,所以用SXSSFWorkbook操作,這裏用50W數據量做演示
        SXSSFWorkbook swb = new SXSSFWorkbook(1000);
        SXSSFSheet sheet = swb.createSheet("1");
        for (int i = 0; i < 500000; i++) {
            SXSSFRow row1 = sheet.createRow(i);
            //SXSSFRow row2 = sheet2.createRow(i);
            for (int j = 0; j < 10; j++) {
                row1.createCell(j).setCellValue("hello SXSSF:" + j);
            }
        }
        
        //下載需要合併的Excel文件到服務器本地,這裏用windows下的做演示
		File file2 = new File("D:\\excel\\21.xlsx");
        FileInputStream inputStream2 =  new FileInputStream(file2);
        XSSFWorkbook wb2 = new XSSFWorkbook(inputStream2);

		//遍歷sheet複製
        for(int i = 0;i<wb2.getNumberOfSheets();i++){
            Sheet oldSheet = wb2.getSheetAt(i);
            Sheet newSheet = swb.createSheet(oldSheet.getSheetName());
            SXSSFUtils.copySheet(swb,oldSheet,newSheet);
        }

        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("D:\\excel\\23.xlsx"));
        swb.write(outputStream);
        outputStream.flush();

        System.out.println("end");
    }

結果如下:
在這裏插入圖片描述
在這裏插入圖片描述
沒問題,成功拷貝過來了。但是這裏還有一個問題,就是拷貝過來的數據有時會丟失樣式,這個我還沒找到解決方案。

如果我們想要合併兩個已經存在的超大Excel,可以嘗試下阿里的EasyExcel。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章