【POI】Excel的xlsx格式的sheet页复制(到新的Excel)

POI——将Excel的一个sheet复制到新的Excel中

一、场景说明

某个xlsx格式的excel,sheet页很多,每个sheet页中都有图片、图形、绘图、图表等各种东西,现在需要将各个sheet页分离出来,每个sheet页单独保存为一个Excel

二、探索(可跳过)

1、使用POI提供的高级API,将一个Excel的sheet复制到另一个Excel,大致步骤如下

① 打开Excel

② 遍历sheet页

③ 遍历每个sheet页的每一个可见单元格,复制文本、样式、边框

	● 这一步没有过多的问题,最多慢一些

④ 遍历每个sheet页的shapes,获取图形、图片信息,如果存在图形组,还需要递归遍历

	● 到这里会出现问题(使用POI的高级API)

	> 图片复制的过程中,不仅需要复制其原始文件,而且还要复制图片在sheet页中的坐标,若图片在单元格内,则位置的处理相对简单;

	> 若不在单元格内,而是在单元格“上面”(即图层),则图片定位的获取则相对麻烦;

	> 若图片位于一个图形组内,则图片的定位此时不依赖于sheet窗口,而是所在图形组内的相对位置;目前POI的高级API,无法直接获取图形组内的图片的相对位置(截止到POI 5.2.5)
	
	> 若图片不是PNG等常见格式,而是SVG或者OBJ,POI也同样不提供直接获取的方法

	> 到了图形的层面,POI的高级API甚至无法复制
	
	● 综上,夭折

2、使用POI提供的低级API,将一个Excel的sheet复制到另一个Excel

● 大致步骤是相同的

● 不同点在于,我们可以直接通过复制XML的形式,轻松愉快的复制上面提到的图形

● 低级API,即 extends org.apache.xmlbeans.XmlObject 的各种东西,比如 CTWorkbook、CTWorksheet、CTRow等,他们都可以直接获取到xlsx文件的各种XML格式文件的内容
在这里插入图片描述

● 我们可以通过创建一个新的workbook和worksheet,copy旧的workbook、worksheet的xml,新建drawing图层,复制旧的worksheet关联的drawing.xml,此时便解决了图形的问题(前提需要处理好各种引用关系)

● 比如,workbook.xml中,涉及到sheet的引用

<sheets>
	<sheet name="Sheet1" sheetId="1" r:id="rId1" />
</sheets>

● workbook.xml.rels中,有各种引用位置

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId3"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
        Target="styles.xml" />
    <Relationship Id="rId2"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
        Target="theme/theme1.xml" />
    <Relationship Id="rId1"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
        Target="worksheets/sheet1.xml" />
</Relationships>

● worksheet.xml中,有关于drawing(图层)的引用

<drawing r:id="rId1" />

● worksheet.xml.rels中,有关于sheet页引用的组件的位置

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId1"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
        Target="../drawings/drawing1.xml" />
</Relationships>

● drawing.xml描述了各种图片的引用、图形的信息和位置
在这里插入图片描述
● 在 drawing.xml.rels 中,描述了drawing.xml实际引用的图片的地址

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId8"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"
        Target="../ink/ink2.xml" />
    <Relationship Id="rId3"
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
        Target="../media/image3.png" />
    .....
</Relationships>

● 当新建一个sheet时,会在workbook.xml和workbook.xml.refs中创建sheet相关的引用信息;当在sheet中创建drawing时,会在sheet.xml和sheet.xml.refs中创建drawing的相关引用信息;图片、图形等同理

● 因此,要完整复制一个sheet的内容到新的workbook,使用复制XML的形式,则需要在workbook.xml层级,过滤出需要复制的sheet,同时使用新创建的sheet的引用ID,覆盖掉旧的sheet的引用ID;sheet引用的东西、drawing引用的东西,凡是新建的,都要重新处理引用,否则即使复制了XML,也无法完美的在新的workbook中展示

● 涉及到图片的时候,由于图片原始文件都在media文件夹中,且是所有sheet页共用的,因此复制sheet的时候,需要通过引用关系,过滤出需要复制的图片;这里还涉及到图片格式的问题,POI可能不支持某些图片格式,比如在Excel中插入一个图标,如下所示。在Excel展示的时候是矢量图(SVG)格式,在media中其实是保存了2份图片,一个是png,一个是svg,如下下图所示

在这里插入图片描述
在这里插入图片描述
● 当尝试建立新的workbook和SVG的关系的时候,会遇见麻烦,POI并不支持SVG类型,于是,需要扩展POI,使其支持SVG

    /**
     * Adds a picture to the workbook.
     *
     * @param pictureData       The bytes of the picture
     * @param format            The format of the picture.
     *
     * @return the index to this picture (1 based).
     * @see #PICTURE_TYPE_EMF
     * @see #PICTURE_TYPE_WMF
     * @see #PICTURE_TYPE_PICT
     * @see #PICTURE_TYPE_JPEG
     * @see #PICTURE_TYPE_PNG
     * @see #PICTURE_TYPE_DIB
     */
    int addPicture(byte[] pictureData, int format);

扩展:

public final class CXSSFRelation extends POIXMLRelation {

    public static final CXSSFRelation IMAGE_SVG = new CXSSFRelation(
            "image/svg",
            PackageRelationshipTypes.IMAGE_PART,
            "/xl/media/image#.svg",
            CXSSFPictureData::new, CXSSFPictureData::new
    );

    private CXSSFRelation(String type, String rel, String defaultName,
                          NoArgConstructor noArgConstructor,
                          PackagePartConstructor packagePartConstructor) {
        super(type, rel, defaultName, noArgConstructor, packagePartConstructor, null);
    }

}

● 当继续探索,又会发现另一个问题,当图片是OBJ时候…,当sheet页中包含更多的内容,比如图表等…除非穷尽Excel的sheet页中所有可添加的内容,并为此写代码处理

3、删除多余sheet页

● 使用POI删除多余sheet的方式的确可以分离各个sheet到新的Excel,但是,被删除的sheet页的图片等信息不会删除,导致Excel大小几乎不会变;

● 比如,一个Excel有3个sheet页面,每个sheet页 10M,假设整个Excel 30M,使用POI删除sheet2和sheet3,并保存为新的Excel,则新的Excel还是30M

● 如果可以接受Excel大小不变,删除多余sheet的方式是非常快速方便的

三、复制实践

1、效果:拆分Excel中的所有sheet,每个sheet单独保存为一个Excel文件,每个Excel不包含多余内容(其它sheet的内容)

2、步骤

● 读取 src workbook

● 获取需要复制的 sheet 的 CTSheet

● 获取 srcSheet 和 sheetIndex

● 获取整个src workbook 的 PackagePart

● 找到 src workbook 的 PackagePart

● 从 src workbook 的 PackagePart 找到 待复制的 worksheet 的 PackagePart

● 创建新的 workbook(destWb)

● 将 待复制的worksheet 关联的内容,复制过来

● 创建新的 worksheet(destSheet)

● 将 srcSheet 的 relation 复制给 destSheet

● 复制 src sheet.xml 的内容

● 复制 sheet.xml 关联的 sharedStrings.xml 内容

● 复制 printArea, 返回sheet页面的起止colnum

● 从 sheet.xml 提取样式引用ID,为复制 styles.xml 的内容做准备

● 复制 style.xml 整体的样式结构 和 命名空间

● 复制 style.xml 中 font、fill、border、numFmts和单元格样式,并修改相应的引用ID

● 保存 destWb

四、部分代码

public static XSSFWorkbook handle(XSSFWorkbook srcWb, String waitCopySheetName) {
        try {
            // 获取需要复制的 sheet 的 CTSheet
            CTSheet srcCtSheet = srcWb.getCTWorkbook().getSheets().getSheetList().stream().filter(r -> waitCopySheetName.equals(r.getName())).findFirst().orElseThrow(() -> new RuntimeException("未找到[" + waitCopySheetName + "]"));

            // 获取 srcSheet 和 sheetIndex
            XSSFSheet srcSheet = srcWb.getSheet(waitCopySheetName);
            int sheetIndex = srcWb.getSheetIndex(srcSheet);

            // 获取整个workbook的PackagePart
            List<PackagePart> srcWbAllPackPartList = srcWb.getPackagePart().getPackage().getParts();

            // 找到 workbook 的 PackagePart
            PackagePart srcWorkbookPackagePart = ExcelPackagePartUtil.findPackagePartByPartName(srcWbAllPackPartList, Common.WORKBOOK_PART_NAME);

            // 从 workbook 的 PackagePart 找到 待复制的 worksheet 的 PackagePart
            PackageRelationship waitCopySheetPackageRelationship = srcWorkbookPackagePart.getRelationship(srcCtSheet.getId());
            if(Objects.isNull(waitCopySheetPackageRelationship)) {
                throw new RuntimeException("未在workbook中找到[" + waitCopySheetName + "]的 RelationShip");
            }
            PackagePart waitCopySheetPackagePart = srcWorkbookPackagePart.getRelatedPart(waitCopySheetPackageRelationship);
            if(Objects.isNull(waitCopySheetPackagePart)) {
                throw new RuntimeException("未在workbook中找到[" + waitCopySheetName + "]的 PackagePart");
            }

            // 创建新的workbook
            XSSFWorkbook destWb = new XSSFWorkbook();

            // 将 waitCopySheetPackagePart 关联的内容,复制过来
            ExcelPackagePartUtil.recursiveRegistPart(destWb, waitCopySheetPackagePart);

            // 创建新的 worksheet
            XSSFSheet destSheet = destWb.createSheet(waitCopySheetName);

            PackagePart destSheetPackagePart = destSheet.getPackagePart();

            // 将 srcSheet 的 relation 复制给 destSheet
            Map<PackagePart, PackageRelationship> relationAndPackagePart = ExcelPackagePartUtil.findRelationAndPackagePart(waitCopySheetPackagePart);
            relationAndPackagePart.forEach(((packagePart, packageRelationship) -> {
                destSheetPackagePart.addRelationship(
                        packagePart.getPartName(),
                        packageRelationship.getTargetMode(),
                        packageRelationship.getRelationshipType(),
                        packageRelationship.getId());
            }));

            // 复制 src sheet.xml 的内容
            destSheet.getCTWorksheet().set(srcSheet.getCTWorksheet());

            // 复制 sheet.xml 关联的 sharedStrings.xml 内容
            ExcelCTUtil.copySharedStrings(srcWb, destWb, destSheet);

            //  复制 printArea, 返回sheet页面的起止colnum
            int[] sheetColIndexRange = ExcelCommonUtil.copyPrintArea(srcWb, destWb, srcSheet, sheetIndex);

            // 从 sheet.xml 提取样式引用ID,为复制 styles.xml 的内容做准备
            List<Long> srcSheetCellStyleRefIdList = ExcelCTUtil.extractStyleRef(srcSheet);

            // 复制 style.xml 整体的样式结构 和 命名空间
            destWb.getStylesSource().getCTStylesheet().set(srcWb.getStylesSource().getCTStylesheet());

            // 复制 style.xml 中 font、fill、border、numFmts和单元格样式,并修改相应的引用ID
            ExcelCTUtil.copyAndSetStyle(srcWb, destWb, destSheet, srcSheetCellStyleRefIdList);

            // 返回
            return destWb;
        } catch (IOException e) {
            throw new RuntimeException("发生IO错误", e);
        } catch (InvalidFormatException e) {
            throw new RuntimeException("findRelationAndPackagePart发生错误", e);
        } catch (Exception e) {
            throw new RuntimeException("发生未知错误", e);
        }
    }

五、全部代码

https://github.com/kanseiu/Office-Converter.git

  • 20
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值