原创总结/朱季谦

设计一款可扩展和基于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>

注意下, com.put.AutoExcel这一行需要填写你的main对应路径,如果路径不对的话,打成jar后,是无法通过java -jar xxx.jar运行的。

我在项目里依赖的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即可运行。

设计一款可扩展和基于windows系统的一键处理表格小工具思路的更多相关文章

  1. 推荐两款远程管理Linux工具(基于Windows系统)

    推荐两款远程管理Linux工具(基于Windows系统) 1.Xshell 百度百科:Xshell 是一个强大的安全终端模拟软件,它支持SSH1, SSH2, 以及Microsoft Windows ...

  2. TriAquae 是一款由国产的基于Python开发的开源批量部署管理工具

    怀着鸡动的心情跟大家介绍一款国产开源运维软件TriAquae,轻松帮你搞定大部分运维工作!TriAquae 是一款由国产的基于Python开发的开源批量部署管理工具,可以允许用户通过一台控制端管理上千 ...

  3. 在基于Windows系统的PHP虚拟主机上实现域名的301永久重定向

    作者:荒原之梦 原文链接:http://zhaokaifeng.com/?p=581 操作背景: 当网站在更换或添加域名.进行网址规范化或删除旧页面时,出于对用户使用体验和搜索引擎优化方面的考虑就需要 ...

  4. 2014年Windows平台软件推荐:神器小工具(骨灰级

    原文  http://www.wtoutiao.com/a/120621.html 底层工具 “If you know how to use Process Monitor competently, ...

  5. Windows平台软件推荐:神器小工具(骨灰级)

    底层工具 "If you know how to use Process Monitor competently, people of both sexes will immediately ...

  6. Windows 7环境下网站性能测试小工具 Apache Bench 和 Webbench使用和下载

    1.简要说明: Apache Bench 是Apache的网站性能测试小程序,Windows平台下的程序名简称ab.exe,要想获得这个80k的可执行程序,用户需要下载整个Apache Httpd软件 ...

  7. 一步步部署基于Windows系统的Jenkins持续集成环境

    如题:本文将介绍如何在Windows环境下运用Jenkins部署持续集成环境.之所以写本文,是因为在最近工作当中,学习使用Jenkins时,确实遇到了一些问题,而大多数教程文档都是基于Mac或是Lin ...

  8. Scrapy框架安装与使用(基于windows系统)

    "人生苦短,我用python".最近了解到一个很好的Spider框架--Scrapy,自己就按着官方文档装了一下,出了些问题,在这里记录一下,免得忘记. Scrapy的安装是基于T ...

  9. 基于windows系统使用GNVM进行node切换版本

    GNVM是什么? GNVM 是一个简单的 Windows 下 Node.js 多版本管理器,类似的 nvm nvmw nodist . 安装 进入官网,下载你所需要的包,直达链接 下载完成 放到我们的 ...

  10. 基于Windows系统应用的权限维持

    1.dll劫持 生成dll 通过msf将dll木马上传到目标应用程序的dll文件夹里面,然后通过msf来将木马dll名称和正常dll名称进行互换 远程开启监听,然后一旦目标机器的应用程序运行就会触发d ...

随机推荐

  1. 自定义go语言日志输出

    自定义输出符合下列需求: 1.含两类日志输出方式:调试模式下输出到控制台:生产环境输出到日志文件 2.调用不同的函数/方法构造不同的输出方式,后续只需调用日志级别对应的函数即可输出该级别日志 工具构造 ...

  2. How to Check and Repair EXT4 Filesystem in Linux

    The fsck (stands for File System Consistency Check) is used to check and repair one or more Linux fi ...

  3. HTTP 协议(超文本传输协议)

    一.HTTP 协议(超文本传输协议) http 协议 版本 1.1 http由来 1960年 http通讯 http原理 URL和URI 区别 Request 请求报文 Response 响应报文 H ...

  4. nmon 采坑

    1.安装 wget http://sourceforge.net/projects/nmon/files/nmon16g_x86.tar.gz 2.解压 tar -zxvf nmon16g_x86.t ...

  5. K Smallest In Unsorted Array

    Find the K smallest numbers in an unsorted integer array A. The returned numbers should be in ascend ...

  6. 用python遍历一个图片文件夹,并输出所有路径到一个 txt 文件

    1 #coding:utf8 2 import os 3 import sys 4 def listfiles(rootDir, txtfile, label=0): 5 ftxtfile = ope ...

  7. mysql 获取表信息 表备注等

    select table_name, table_comment, create_time, update_time from information_schema.tables-- where ta ...

  8. Coder vs Programmer: Difference You Should Know

    In this tech-driven world, you may have heard the terms 'coder' and 'programmer' used interchangeabl ...

  9. Activity基础知识

    Activity 一.Activity是什么 Activity是一种可以包含用户界面的组件,主要用于和用户进行交互.一个应用程序可以包含零个或多个活动. 二.活动的基本用法 1. 手动创建活动 ​ 打 ...

  10. flutter 环境配置以及我的第一个flutter程序(Hello World)

    电脑配置: 操作系统: Windows 7 或更高版本 (64-bit) 磁盘空间: 400 MB (不包括Android Studio的磁盘空间). Windows下所需安装有: 1.Flutter ...