Poi解析对比excel表格

文章目录

前言

这次不是 Android 的技术分享,是 java 的,当然把 poi 的代码放到 Android 中也可以用,毕竟同源嘛

为啥会有这个文章呢,因为我老婆是会计嘛,她有时候会让我帮忙对账,两个 excel 文件,顺序也不同,需要我来对比出哪里有问题,也就是数不太对应,我想了一下,如果好几百个甚至几千个数字来对账,那我岂不是眼睛都花了,这样我哪里还有时间去happy愉快的撸代码了? 作为一个程序员,我们要解放自己的眼睛,去做一些有意义的事情!

开发环境

Intellij Idea+maven

pom 文件

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0"
 3         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5    <modelVersion>4.0.0</modelVersion>
 6
 7    <groupId>com.kikt</groupId>
 8    <artifactId>ExcelDemo</artifactId>
 9    <version>1.0-SNAPSHOT</version>
10    <build>
11        <plugins>
12            <plugin>
13                <groupId>org.apache.maven.plugins</groupId>
14                <artifactId>maven-compiler-plugin</artifactId>
15                <configuration>
16                    <source>1.7</source>
17                    <target>1.7</target>
18                </configuration>
19            </plugin>
20        </plugins>
21    </build>
22
23    <dependencies>
24        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
25        <dependency>
26            <groupId>org.apache.poi</groupId>
27            <artifactId>poi</artifactId>
28            <version>3.15-beta2</version>
29        </dependency>
30        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
31        <dependency>
32            <groupId>org.apache.poi</groupId>
33            <artifactId>poi-ooxml</artifactId>
34            <version>3.15-beta2</version>
35        </dependency>
36    </dependencies>
37
38</project>

引入了 poi 的解析库的两个文件

结构

首先是 poi 对于 excel 的结构分析 WorkBook->Sheet->Row->Cell 放在 wps/excel 中看,WorkBook 对应的是工作簿,Sheet 是表,Row 顾名思义是行,Cell 是单元格

有了这个基础,我们继续去看

获取数据

得到 sheet 表

要想拿到数据,首先需要先把工作簿拿到,然后拿到 Sheet

首先拿到 WorkBook

1File file = new File(path);
2FileInputStream is = new FileInputStream(file);
3Workbook sheets = WorkbookFactory.create(is);

这里 path 是文件对应的路径

我们这里建一个 Utils 文件用于操作这样的重复数据 ExcelUtils.java

 1public class ExcelUtils {
 2    private ExcelUtils() {
 3    }
 4    public static Sheet getSheet(String path, int sheetPosition) throws IOException, InvalidFormatException {
 5        File file = new File(path);
 6        FileInputStream is = new FileInputStream(file);
 7        Workbook sheets = WorkbookFactory.create(is);
 8        return sheets.getSheetAt(sheetPosition);
 9    }
10
11    public static Sheet getSheet(String path, String sheetName) throws IOException, InvalidFormatException {
12        File file = new File(path);
13        FileInputStream is = new FileInputStream(file);
14        Workbook sheets = WorkbookFactory.create(is);
15        return sheets.getSheet(sheetName);
16    }
17}

两个方法分别使用表格的名字/序号获取 position 从 0 开始,这里为了处理有可能数十个 sheet 的情况,所以增加了一个用名称获取的方法

Sheet 的声明

1public interface Sheet extends Iterable<Row>

Sheet 是一个接口,继承 Iterable,所以可以知道这里的实现类一定实现了 Iterable 接口 可以用 foreach 循环来遍历 Sheet 得到 Row

得到 Cell

1public interface Row extends Iterable<Cell>

Row 同样如此,可以通过 foreach 循环得到 Cell,这样可以每个单元格的遍历

1 Cell getCell(int var1);

Row 中有一个方法,根据一个 int 值得到对应的 Cell 这个方法从 0 开始,这里就涉及到一个问题,Excel 的列标是字母形式,而不是数字,需要转化一下 这里写了一个小算法

 1    private static Map<Integer, Integer> columnMap = new HashMap<>();
 2
 3    private static int getColumnLength(int length) {
 4        Integer columnLength = columnMap.get(length);
 5        if (columnLength == null) {
 6            columnMap.put(length, (int) Math.pow(26, length));
 7        } else {
 8            return columnLength;
 9        }
10        return getColumnLength(length);
11    }
12
13    /**
14     * @param columnLetter 列的字母
15     * @return 列对应的数字
16     */
17    public static int getColumnNumber(String columnLetter) {
18        if (columnLetter == null) {
19            throw new RuntimeException("列号不能为空");
20        }
21        columnLetter = columnLetter.toLowerCase();
22        int letterLength = columnLetter.length();
23        if (letterLength == 1) {
24            char letter = columnLetter.charAt(0);
25            return letter - 97;
26        } else {
27            Integer length =getColumnLength(letterLength - 1);
28            return (getColumnNumber(columnLetter.charAt(0) + "")+1)*length+getColumnNumber(columnLetter.substring(1));
29        }
30    }

可以将 AA、CA 之类的列号转为对应的数字 PS:题外话,这里推荐下 Sedgewick 的《算法》一书,最近重新研读了下,虽然都是基础,但是基础的牢靠对于算法有很大的帮助

正式开始编码的准备工作

这里是对应的两个表的截图,这里我给隐私部位打了些马赛克

img

img

金额之类的可以看到 我们要对比的就是图 1 的 F 列和图 2 的 H 列

1    String recordFilePath = "H:\\1.xls";
2    Sheet recordSheet = ExcelUtils.getSheet(recordFilePath, 0);
3    List<RecordBean> recordBeanList = getRecordList(recordSheet, "a", "f");
4
5    String invoiceFilePath = "2.xls";
6    Sheet invoiceSheet = ExcelUtils.getSheet(invoiceFilePath, "外地预交增值税及附加税");
7    List<InvoiceBean> invoiceBeanList = getInvoiceList(invoiceSheet, "a", "i");

这里我首先通过 util 的方法获取到了 sheet 表,然后将需要解析的列号写入方法内 然后获取到了对应的 List 集合

bean 实体

 1package excel.bean;
 2
 3/**
 4 * Created by kikt on 2017/2/26.
 5 * 记账信息
 6 */
 7public class RecordBean  extends NumberBean{
 8    private int index;
 9    private double number;
10
11    public int getIndex() {
12        return index;
13    }
14
15    public void setIndex(int index) {
16        this.index = index;
17    }
18
19    public double getNumber() {
20        return number;
21    }
22
23    public void setNumber(double number) {
24        this.number = number;
25    }
26
27    @Override
28    public String toString() {
29        return "RecordBean{" +
30                "index=" + index +
31                ", number=" + number +
32                '}';
33    }
34}
 1package excel.bean;
 2
 3/**
 4 * Created by kikt on 2017/2/26.
 5 */
 6public class NumberBean {
 7    private int numberIndex;
 8
 9    public int getNumberIndex() {
10        return numberIndex;
11    }
12
13    public void setNumberIndex(int numberIndex) {
14        this.numberIndex = numberIndex;
15    }
16}

获取 list 的方法

 1private static List<RecordBean> getRecordList(Sheet recordSheet, String indexLetter, String numberLetter) {
 2        List<RecordBean> list = new ArrayList<>();
 3        for (Row cells : recordSheet) {
 4            RecordBean bean = new RecordBean();
 5            Cell indexCell = cells.getCell(ExcelUtils.getColumnNumber(indexLetter));
 6            if (indexCell == null || indexCell.getCellType() != Cell.CELL_TYPE_NUMERIC) {
 7                continue;
 8            }
 9            double numericCellValue = indexCell.getNumericCellValue();
10            bean.setIndex((int) numericCellValue);
11            int columnNumber = ExcelUtils.getColumnNumber(numberLetter);
12            bean.setNumberIndex(columnNumber);
13            bean.setNumber(cells.getCell(columnNumber).getNumericCellValue());
14            list.add(bean);
15        }
16
17        return list;
18    }

另一个大致相同,这里不贴了

然后通过一个 compare 方法比较一下

 1private static List<InvoiceBean> compareList(List<RecordBean> recordBeanList, List<InvoiceBean> invoiceBeanList) {
 2        List<InvoiceBean> unMarkBeanList = new ArrayList<>();
 3
 4        for (int i = recordBeanList.size() - 1; i >= 0; i--) {
 5            RecordBean recordBean = recordBeanList.get(i);
 6            for (int j = 0; j < invoiceBeanList.size(); j++) {
 7                InvoiceBean invoiceBean = invoiceBeanList.get(j);
 8                if (recordBean.getNumber() == invoiceBean.getNumber()) {
 9                    invoiceBeanList.remove(invoiceBean);
10                    recordBeanList.remove(recordBean);
11                    break;
12                }
13            }
14        }
15
16        unMarkBeanList.addAll(invoiceBeanList);
17
18        return unMarkBeanList;
19    }

将相同的移除掉,剩余的就是不同的

保存结果

这里光有比对结果不行,还需要修改表格,将不同的标记出来,以备人工查账

保存 sheet 的方法

 1 public static void saveWorkbook(String path, Workbook workbook) throws IOException {
 2        File file = new File(path);
 3        workbook.write(new FileOutputStream(file));
 4    }
 5
 6    public static void backupSheet(String path, Workbook workbook) throws IOException {
 7        File file = new File(path);
 8        String name = file.getName();
 9        String newPath = file.getParentFile().getAbsolutePath() + "\\backup\\";
10        String newName = newPath + name + "_" + TimeUtils.getTimeString() + ".bak";
11        File newFile = new File(newName);
12        newFile.getParentFile().mkdirs();
13        newFile.createNewFile();
14        workbook.write(new FileOutputStream(newFile));
15    }
16
17    public static void saveSheet(Sheet sheet, String path) throws IOException {
18        Workbook workbook = sheet.getWorkbook();
19        saveWorkbook(path, workbook);
20    }
 1package excel.utils;
 2
 3import java.text.SimpleDateFormat;
 4import java.util.Date;
 5
 6/**
 7 * Created by kikt on 2017/2/26.
 8 */
 9public class TimeUtils {
10
11}public static String getTimeString() {
12    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
13    return sdf.format(new Date());
14}

核心保存的方法是 workbook.write(OutputStream)方法,简单封装了一下,saveSheet()也是封装,参数不同,这里还有一个备份的方法,可以大概看看,简单的说就是修改文件名,加时间戳.bak 后缀,保存成文件

修改样式

保存和备份文件说完了,这里还需要修改下样式,不然谁知道你最后查出了什么

 1 private static void setStyle(Sheet invoiceSheet, int index, int numberIndex) {
 2        for (Row cells : invoiceSheet) {
 3            Cell cell = cells.getCell(ExcelUtils.getColumnNumber("a"));
 4            if (cell != null && cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
 5                if (index == cell.getNumericCellValue()) {
 6                    Cell numberCell = cells.getCell(numberIndex);
 7                    CellStyle cellStyle = invoiceSheet.getWorkbook().createCellStyle();
 8                    cellStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
 9                    cellStyle.setFillForegroundColor(HSSFColor.RED.index);
10                    numberCell.setCellStyle(cellStyle);
11                }
12            }
13        }
14    }

这里没有封装,只是简单的修改了下 核心代码是

1CellStyle cellStyle = invoiceSheet.getWorkbook().createCellStyle();//创建一个新单元格样式
2cellStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);//填充方式是前景色
3cellStyle.setFillForegroundColor(HSSFColor.RED.index);//设置前景色为红色
4numberCell.setCellStyle(cellStyle);//将单元格的样式改为新建的样式

到这里简单的修改样式就结束了,只要在这之后保存 workbook 就可以了

结语

这篇文章主要是解析和简单的修改,后面可能会涉及到生成文件,到时候再写篇文章吧