原創總結/朱季謙
設計一款可擴充套件和基於windows系統的一鍵處理表格小工具思路
日常開發當中,業務人員經常會遇到一些重複性整理表格的事情,這時候,就可以透過一些方式進行自動化程式處理,提高工作(摸魚)效率。
例如,業務人員有這樣需要,日常需要手工整理以下原始xlsx表格資料,這些資料格式都是固定死,他們需要去除【手機號】這一列,然後在第一行增加一個表頭標題【審計結果表】,同時需要將【日期】格式統一整理成yyyy-mm-dd格式的,最後需要在日期列前面增加一列【是否合格】,統一設定值為1。
整理後的表格如下:
注意,真實需求會比以上需求更加複雜,這裡只是以一個比較簡單的需求展開演示,來設計一個可一鍵傻瓜式自動化整理日常表格的工具。
工具的開發環境如下:
Java,Bat,需要依賴處理表格的poi的maven依賴。
一、建立一個maven工程,pom.xml依賴如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>auto-put-file</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven-clean-plugin.version>3.1.0</maven-clean-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-dependency-plugin.version>3.1.2</maven-dependency-plugin.version>
<maven-deploy-plugin.version>2.8.2</maven-deploy-plugin.version>
<maven-install-plugin.version>2.5.2</maven-install-plugin.version>
<maven-jar-plugin.version>3.1.2</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>xdocreport</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>${maven-clean-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>${maven-deploy-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>${maven-install-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven-jar-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<archive>
<manifest>
<mainClass>com.put.AutoExcel</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
注意下,
我在專案裡依賴的4.1.2版本的org.apache.poi依賴包,最開始使用的是4.1.0版本的,但發現裡面有一個很大的bug,就是使用XSSFSheet中處理指定行資料下移的方法sheet.shiftRows(0,sheet.getLastRowNum(),1,true,false)時,會發現指定下移位置之後的資料,都被刪除完了,導致下移後的表格都成了一片空白。後來,我改成了4.1.2版本,才沒有這個問題,但是,注意了,4.1.2版本依舊存在一個bug,那便是將第二列資料透過sheet.shiftColumns(1,sheet.getRow(0).getLastCellNum(),-1)左移覆蓋掉第一列時,會出現以下異常:
Exception in thread "Thread-0" java.lang.IndexOutOfBoundsException
at org.apache.xmlbeans.impl.store.Xobj.removeElement(Xobj.java:2206)
at org.apache.xmlbeans.impl.store.Xobj.remove_element(Xobj.java:2236)
at org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRowImpl.removeC(Unknown Source)
at org.apache.poi.xssf.usermodel.XSSFRow.fixupCTCells(XSSFRow.java:612)
at org.apache.poi.xssf.usermodel.XSSFRow.onDocumentWrite(XSSFRow.java:582)
at org.apache.poi.xssf.usermodel.XSSFSheet.write(XSSFSheet.java:3640)
at org.apache.poi.xssf.usermodel.XSSFSheet.commit(XSSFSheet.java:3585)
at org.apache.poi.ooxml.POIXMLDocumentPart.onSave(POIXMLDocumentPart.java:490)
at org.apache.poi.ooxml.POIXMLDocumentPart.onSave(POIXMLDocumentPart.java:495)
at org.apache.poi.ooxml.POIXMLDocument.write(POIXMLDocument.java:236)
at com.put.service.impl.ConToImageServiceImpl.export(ConToImageServiceImpl.java:64)
at com.put.AutoExcel.lambda$main$0(AutoExcel.java:26)
at java.lang.Thread.run(Thread.java:745)
檢視一些資料得知,即使到了5.x版本,該bug一直沒有完全修復,只能透過先刪除第一列後,再將第二列之後的資料往左移一列的方式,曲線解決這個反向移動問題。
二、基於Java SPI設計一套可便於後期擴充套件的介面實現
我在工程裡使用到了Java SPI的服務發現機制,便於後期如果有需要進行工具處理新功能擴充套件,只需要增加一個實現類,放到com.put.service.DisposeService檔案目錄裡,這樣,後期就不需要改動原有工具的程式碼了。
1、先建立一個介面DisposeService:
package com.put.service;
/**
* TODO
*
* @author zhujiqian
* @date 2023/3/16 09:38
**/
public interface DisposeService {
public void export(String sourceFile, String sourceFileName);
}
這裡的sourceFile是包括檔案字尾的字串名,例如:“測試表格檔案.xlsx”,用來讀取檔案內容。
sourceFileName是沒有的檔案字尾的字串名,例如:“測試表格檔案”,用來建立用於存放已經整理的檔案的目錄。
2、建立一個實現類DisposeServiceImpl:
package com.put.service.impl;
import com.put.service.DisposeService;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import static com.put.utils.DateUtil.timeFormat;
/**
* TODO
*
* @author zhujiqian
* @date 2023/3/16 16:40
**/
public class DisposeServiceImpl implements DisposeService {
@Override
public void export(String sourceFile,String sourceFileName) {
System.out.println("開始整理"+sourceFileName+"檔案");
try {
FileInputStream file = new FileInputStream(sourceFile);
XSSFWorkbook workbook = new XSSFWorkbook(file);
//處理第一個sheet,若需要處理多個sheet,可以自行擴充套件
XSSFSheet sheet = workbook.getSheetAt(0);
//移除表格第一列
removeCell(sheet,0);
//移除表格第一列後,接著將原先第二列的資料往左邊移動,即變成了第一列
sheet.shiftColumns(1,sheet.getRow(0).getLastCellNum(),-1);
//表格最後一列往右移動一格
sheet.shiftColumns(sheet.getRow(0).getLastCellNum()-1,sheet.getRow(0).getLastCellNum(),1);
//在倒數第二列地方新增一個表頭標題
sheet.getRow(0).createCell(sheet.getRow(0).getLastCellNum() - 2).setCellValue("是否合格");
for(int i = 1; i<= sheet.getLastRowNum(); i++){
if(sheet.getRow(i) == null){
continue;
}
//單元格為空,則繼續同一列的下一個單元格
if(sheet.getRow(i).getCell(sheet.getRow(i).getLastCellNum()-1) == null ){
continue;
}
//調整最右邊的申請時間資料
int cellNum = sheet.getRow(i).getLastCellNum();
XSSFCell cell = sheet.getRow(i).getCell(cellNum- 1);
cell.setCellType(CellType.STRING);
cell.setCellValue(timeFormat(cell.toString()));
//對倒數第二列標題為【是否合格】的列從第二行開始賦值為1
sheet.getRow(i).createCell(cellNum - 2).setCellValue(1);
}
//資料下移一行,空出第一行,設定新表頭標題
sheet.shiftRows(0,sheet.getLastRowNum(),1,true,false);
XSSFRow rows = sheet.createRow(0);
rows.createCell(0).setCellValue("審計結果表");
FileOutputStream outFile =new FileOutputStream(System.getProperty("user.dir")+"//整理結果//"+sourceFileName+"//"+"處理完的表格.xlsx");
//寫入到新檔案裡
workbook.write(outFile);
file.close();
outFile.flush();
outFile.close();
System.out.println("整理完成");
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void removeCell(XSSFSheet sheet, int index){
for (Row row : sheet) {
Cell cell = row.getCell(index);
if (cell != null) {
row.removeCell(cell);
}
}
}
}
這個方法主要分成以下幾個步驟:
1、讀取原始檔內容,建立一個可讀取表格的XSSFWorkbook物件,並透過workbook.getSheetAt(0)獲取第一個sheet表格;
FileInputStream file = new FileInputStream(sourceFile);
XSSFWorkbook workbook = new XSSFWorkbook(file);
//處理第一個sheet,若需要處理多個sheet,可以自行擴充套件
XSSFSheet sheet = workbook.getSheetAt(0);
2、刪除第一列資料,然後將第二列開始到最後一列的資料往左移動一列,即原本的第二列變成第一列,第三列變成第二列,依次移動;
//移除表格第一列
removeCell(sheet,0);
//移除表格第一列後,接著將原先第二列的資料往左邊移動,即變成了第一列
sheet.shiftColumns(1,sheet.getRow(0).getLastCellNum(),-1);
removeCell(sheet,0)程式碼如下:
public static void removeCell(XSSFSheet sheet, int index){
for (Row row : sheet) {
Cell cell = row.getCell(index);
if (cell != null) {
row.removeCell(cell);
}
}
}
注意一點,前面有提到,直接使用sheet.shiftColumns(1,sheet.getRow(0).getLastCellNum(),-1)對第二列資料往左移動會報錯,故而需要先刪除第一列,再作遷移。
3、表格最後一列往右移動一格,然後在倒數第二列新增一個表頭標題【是否合格】;
//表格最後一列往右移動一格
sheet.shiftColumns(sheet.getRow(0).getLastCellNum()-1,sheet.getRow(0).getLastCellNum(),1);
//在倒數第二列地方新增一個表頭標題
sheet.getRow(0).createCell(sheet.getRow(0).getLastCellNum() - 2).setCellValue("是否合格");
4、調整最右邊的申請時間資料,統一改成“yyyy-mm-dd”格式,同時對倒數第二列標題為【是否合格】的列從第二行開始賦值為1;
for(int i = 1; i<= sheet.getLastRowNum(); i++){
if(sheet.getRow(i) == null){
continue;
}
//單元格為空,則繼續同一列的下一個單元格
if(sheet.getRow(i).getCell(sheet.getRow(i).getLastCellNum()-1) == null ){
continue;
}
//調整最右邊的申請時間資料
int cellNum = sheet.getRow(i).getLastCellNum();
XSSFCell cell = sheet.getRow(i).getCell(cellNum- 1);
cell.setCellType(CellType.STRING);
cell.setCellValue(timeFormat(cell.toString()));
//對倒數第二列標題為【是否合格】的列從第二行開始賦值為1
sheet.getRow(i).createCell(cellNum - 2).setCellValue(1);
}
5、所有資料下移一行,空出第一行設定新表頭標題;
//資料下移一行,空出第一行,設定新表頭標題
sheet.shiftRows(0,sheet.getLastRowNum(),1,true,false);
XSSFRow rows = sheet.createRow(0);
rows.createCell(0).setCellValue("審計結果表");
7、寫入到指定目錄的新檔案,關閉讀取;
FileOutputStream outFile =new FileOutputStream(System.getProperty("user.dir")+"//整理結果//"+sourceFileName+"//"+"處理完的表格.xlsx");
//寫入到新檔案裡
workbook.write(outFile);
file.close();
outFile.flush();
outFile.close();
System.out.println("整理完成");
其中,處理時間的方法程式碼如下,可支援對yyyy/mm/dd hh:mm:ss、yyyy/m/d h:mm:ss、yyyy/m/dd h:mm:ss、yyyymmdd、yyyy/mm/dd、yyyy/m/d、yyyy/m/dd、excel格式這些格式統一處理成“yyyy-mm-dd”:
public static String timeFormat(String date) {
if ("".equals(date) || date == null) return "時間為空";
if (date.length() < 5) return "時間格式錯誤";
if (date.charAt(4) == '-') return date;
String dateFormat = "";
switch (date.length()){
case 19:
case 10:
dateFormat = date.substring(0, 4) + "-" + date.substring(5, 7) + "-" + date.substring(8, 10);
break;
case 9:
if (date.charAt(4) != '/' )break;
case 17:
dateFormat = date.substring(0, 4) + "-" + date.charAt(5) + "-" + date.substring(7, 9);
break;
case 8:
if (date.charAt(4) != '/' ){
dateFormat = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6);
break;
}
case 16:
dateFormat = date.substring(0, 4) + "-" + date.charAt(5) + "-" + date.charAt(7);
break;
case 5:
return numberToDate(date);
default:
return "時間格式錯誤";
}
return dateFormat;
}
public static String numberToDate(String number){
Calendar calendar = new GregorianCalendar(1900,0,-1);
Date date = DateUtils.addDays(calendar.getTime(),Integer.parseInt(number));
//對日期格式化操作
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
因為是使用到Java SPI機制,故而需要在resource目錄下建立一個META_INF.services目錄,目錄下建立一個與介面路徑同名的檔案:com.put.service.DisposeService。該檔案裡存放需要呼叫的DisposeService介面的實現類,然後就可以自動實現介面實現類的自動呼叫處理了。同理,後期若不需要呼叫某個實現類的方法了,只需要將該檔案裡的指定實現類路徑去掉即可。
com.put.service.impl.DisposeServiceImpl
三、最後,新增一個main啟動
public class AutoExcel {
public static void main(String[] argv) {
start();
}
public static void start(){
//獲取Java專案所在目錄
File file = new File(System.getProperty("user.dir"));
for (File f : file.listFiles()){
if (!f.isDirectory() && f.getName().contains(".xlsx")){
String sourceFile = f.getName();
String sourceFileName = sourceFile.substring(0,sourceFile.lastIndexOf("."));
if (StringUtils.isAnyBlank(sourceFile,sourceFileName))return;
//建立檔案目錄
createDirectory(sourceFileName);
//基於SPI機制自動呼叫實現類來實現檔案處理
execute(sourceFile,sourceFileName);
}
}
}
public static void createDirectory(String sourceName){
File file = new File(System.getProperty("user.dir") + "//整理結果//" + sourceName);
if (!file.exists()){
file.mkdirs();
}
}
public static void execute(String sourceFile, String sourceName){
ServiceLoader<DisposeService> serviceLoader = ServiceLoader.load(DisposeService.class);
Iterator<DisposeService> serviceIterator = serviceLoader.iterator();
while (serviceIterator.hasNext()){
DisposeService service = serviceIterator.next();
if (service == null)return;
new Thread(()->{
service.export(sourceFile,sourceName);
}).start();
}
}
}
基於以上程式碼來梳理說明一下流程:
1、獲取Java專案所在的檔案路徑,然後獲取該路徑的.xlsx表格檔名,若是存在多個.xlsx表格檔案,可以同時批次處理。
File file = new File(System.getProperty("user.dir"));
for (File f : file.listFiles()){
if (!f.isDirectory() && f.getName().contains(".xlsx")){
String sourceFile = f.getName();
String sourceFileName = sourceFile.substring(0,sourceFile.lastIndexOf("."));
if (StringUtils.isAnyBlank(sourceFile,sourceFileName))return;
......
}
}
2、建立處理好的表格統一存放目錄,路徑名為,當前目錄//整理結果//檔名同名資料夾//
public static void createDirectory(String sourceName){
File file = new File(System.getProperty("user.dir") + "//整理結果//" + sourceName);
if (!file.exists()){
file.mkdirs();
}
}
例如,處理的檔名為“測試表格檔案.xlsx”,那麼建立的目錄結構效果如下:
3、基於Java SPI機制,讀取獲取介面對應的com.put.service.DisposeService檔案內容,然後反射得到檔案裡指定的實現類,迴圈執行各個實現類的方法:
public static void execute(String sourceFile, String sourceName){
ServiceLoader<DisposeService> serviceLoader = ServiceLoader.load(DisposeService.class);
Iterator<DisposeService> serviceIterator = serviceLoader.iterator();
while (serviceIterator.hasNext()){
DisposeService service = serviceIterator.next();
if (service == null)return;
new Thread(()->{
service.export(sourceFile,sourceName);
}).start();
}
}
該方法裡我用了多執行緒併發處理,因為各個檔案的處理無任何依賴,若是大批次處理時,序列執行實在太慢,但多執行緒處理同時也會存在一個問題是,若大批次表格檔案中每個檔案資料量都很大的話,電腦記憶體太小的話,可能會出現記憶體溢位問題。
三、maven專案打成一個jar,然後編寫一個可在windows執行的bat指令碼。
複製最後一個auto-put-file-1.0-SNAPSHOT-jar-with-dependencies.jar到隨意一個目錄裡,然後編寫一個名字為start.bat指令碼:
@echo off
java -jar auto-put-file-1.0-SNAPSHOT-jar-with-dependencies.jar
將start.bat和auto-put-file-1.0-SNAPSHOT-jar-with-dependencies.jar放同一個目錄裡,然後將需要處理的Excel檔案放到該目錄下,點選start.bat即可執行。