百万级别的数据导出成Excel解决方案(JAVA)

最近做开发时遇到一个需求,导出百万级别的数据到excel文件中。但是用传统的poi方式,查寻数据库然后poi工具写入excel一直内存溢出的错误,无奈中找到两种解决方案一种是写出多个excel文件打包成zip给用户,这种速度还是不快,要求多线程分页操作数据库,比较麻烦。另外一种是利用官方提供的一种基于XML的方案。此文介绍这种方案的代码实现,笔者在本地抽成了一个工具类,如下。

其实对于一个Excel文件来说,最核心的是它的数据。Excel文件中的数据和样式文件是分开存储的,它们都对应于它自己体系中的一个XML文件。有兴趣的朋友可以把Excel文件的后缀名改成“.zip”,然后用压缩文件把它解压缩,可以看到它里面的结构是由一堆的XML文件组成的。如果我们把解压缩后的文件再压缩成一个压缩文件,并把它的后缀名改为Excel文件对应的后缀名“.xlsx”或“.xls”,然后再用Excel程序把它打开。这个时候你会发现它也是可以打开的。笔者本文所要讲述的基于大量的数据生成Excel的方案就是基于这种XML文件的方案,它依赖于一个现有的Excel文件(这个Excel文件可以在运行时生成好),然后把我们的数据生成对应的XML表示,再把我们的XML替换原来的XML文件,再进行打包后就变成了一个Excel文件了。基于这种方式,笔者做了一个测试,生成了一个拥有3500万行,5列的Excel文件,该文件大小为1GB,耗时412秒。这种效率比起我们应用传统的API来说是指数倍的。

细节的实现详情,请读者自己参考以下示例代码,该示例代码是笔者从Apache官方下载的,原地址是https://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/usermodel/examples/BigGridDemo.java。需要注意的是生成的XML中需要应用到的样式需要事先生成,需要应用函数、合并单元格等逻辑的时候,可以先拿一个Excel文件应用对应的函数、合并逻辑,再把它解压缩后查看里面的XML文件的展现形式,然后自己拼接的时候也拼接成对应的形式,这样自己生成的Excel文件也会有对应的效果。

import org.apache.poi.hssf.util.CellReference;

import org.apache.poi.openxml4j.opc.internal.ZipHelper;

import org.apache.poi.ss.usermodel.DateUtil;

import org.apache.poi.ss.usermodel.FillPatternType;

import org.apache.poi.ss.usermodel.HorizontalAlignment;

import org.apache.poi.ss.usermodel.IndexedColors;

import org.apache.poi.xssf.usermodel.XSSFCellStyle;

import org.apache.poi.xssf.usermodel.XSSFDataFormat;

import org.apache.poi.xssf.usermodel.XSSFFont;

import org.apache.poi.xssf.usermodel.XSSFSheet;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import org.springframework.util.StringUtils;

import java.io.*;

import java.util.Calendar;

import java.util.Enumeration;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.zip.ZipEntry;

import java.util.zip.ZipFile;

import java.util.zip.ZipOutputStream;

/**

* @author wangwei (yuhui@shanshu.ai)

* @date 2018/07/20

*/

public class ExcelExportUtil {

private ExcelExportUtil() {}

private static final StringXML_ENCODING ="UTF-8";

    /**

*

    * @param sheetName EXCEL中的表名

    * @param absolutePath 实际服务器路径

    * @param titles 表头

    * @param data 表数据

    * @param 数据泛型

    * @throws Exception

*/

    public static void exportExcel(String sheetName, String path,String absolutePath, String[] titles, List data)

throws Exception {

// Step 1. Create a template file. Setup sheets and workbook-level objects such as

// cell styles, number formats, etc.

        XSSFWorkbook wb =new XSSFWorkbook();

        XSSFSheet sheet = wb.createSheet(sheetName);

        Map styles =createStyles(wb);

        //name of the zip entry holding sheet data, e.g. /xl/worksheets/sheet1.xml

        String sheetRef = sheet.getPackagePart().getPartName().getName();

        //save the template

        FileOutputStream os =new FileOutputStream(path);

        wb.write(os);

        os.close();

        //Step 2. Generate XML file.

        File tmp = File.createTempFile("sheet", ".xml");

        Writer fw =new OutputStreamWriter(new FileOutputStream(tmp), XML_ENCODING);

        writeDate(fw, styles, titles, data);

        fw.close();

        //Step 3. Substitute the template entry with the generated data

        FileOutputStream out =new FileOutputStream(absolutePath);

        //用心拼接生成的XML文件,替换原来模板Excel文件中对应的XML文件,再压缩打包为一个Excel文件。

        substitute(new File(path), tmp, sheetRef.substring(1), out);

        out.close();

        wb.close();

    }

/**

* 支持的Cell样式

*

    * @param wb

    *

    * @return

    */

    private static MapcreateStyles(XSSFWorkbook wb) {

Map styles =new HashMap<>();

        XSSFDataFormat fmt = wb.createDataFormat();

        XSSFCellStyle style1 = wb.createCellStyle();

        style1.setAlignment(HorizontalAlignment.RIGHT);

        style1.setDataFormat(fmt.getFormat("0.0%"));

        styles.put("percent", style1);

        XSSFCellStyle style2 = wb.createCellStyle();

        style2.setAlignment(HorizontalAlignment.CENTER);

        style2.setDataFormat(fmt.getFormat("0.0X"));

        styles.put("coeff", style2);

        XSSFCellStyle style3 = wb.createCellStyle();

        style3.setAlignment(HorizontalAlignment.RIGHT);

        style3.setDataFormat(fmt.getFormat("$#,##0.00"));

        styles.put("currency", style3);

        XSSFCellStyle style4 = wb.createCellStyle();

        style4.setAlignment(HorizontalAlignment.RIGHT);

        style4.setDataFormat(fmt.getFormat("mmm dd"));

        styles.put("date", style4);

        XSSFCellStyle style5 = wb.createCellStyle();

        XSSFFont headerFont = wb.createFont();

        headerFont.setBold(true);

        style5.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());

        style5.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        style5.setFont(headerFont);

        styles.put("header", style5);

        return styles;

    }

/**

* 写出Excel Title

*

    * @param sw

    * @param styles

    * @param titles

    */

    private static void writeTitle(SpreadsheetWriter sw, Map styles, String[] titles) {

//insert header row

        try {

if (titles !=null && titles.length >0) {

sw.insertRow(0);

                int styleIndex = styles.get("header").getIndex();

                int index =0;

                for (String title : titles) {

sw.createCell(index, title, styleIndex);

                    index++;

                }

sw.endRow();

            }

}catch (IOException e) {

e.printStackTrace();

        }

}

/**

* 写出Excel data

*

    * @param out

    * @param styles

    * @param titles

    * @param data

    * @param

    *

    * @throws Exception

*/

    private static void writeDate(Writer out, Map styles, String[] titles, List data)

throws Exception {

SpreadsheetWriter sw =new SpreadsheetWriter(out);

        sw.beginSheet();

        int length =0;

        if (titles !=null) {

writeTitle(sw, styles, titles);

            length = titles.length;

        }

//write data rows

        int rownum =1;

        if (data !=null && data.size() >0) {

for (T obj : data) {

String[] items = obj.toString().split(";");

                for (int i =0; i < length; i++) {

if (i == length) {

break;

                    }

sw.insertRow(rownum);

                    String str = items[i];

                    if (!StringUtils.isEmpty(str)) {

sw.createCell(i, items[i]);

                    }else {

sw.createCell(i, "");

                    }

sw.endRow();

                }

rownum++;

            }

}

sw.endSheet();

    }

/**

    * @param zipfile the template file

    * @param tmpfile the XML file with the sheet data

    * @param entry  the name of the sheet entry to substitute, e.g. xl/worksheets/sheet1.xml

    * @param out    the stream to write the result to

*/

    private static void substitute(File zipfile, File tmpfile, String entry, OutputStream out)throws IOException {

ZipFile zip = ZipHelper.openZipFile(zipfile);

        try {

ZipOutputStream zos =new ZipOutputStream(out);

            Enumeration en = zip.entries();

            while (en.hasMoreElements()) {

ZipEntry ze = en.nextElement();

                if (!ze.getName().equals(entry)) {

zos.putNextEntry(new ZipEntry(ze.getName()));

                    InputStream is = zip.getInputStream(ze);

                    copyStream(is, zos);

                    is.close();

                }

}

zos.putNextEntry(new ZipEntry(entry));

            InputStream is =new FileInputStream(tmpfile);

            copyStream(is, zos);

            is.close();

            zos.close();

        }finally {

zip.close();

        }

}

private static void copyStream(InputStream in, OutputStream out)throws IOException {

byte[] chunk =new byte[1024];

        int count;

        while ((count = in.read(chunk)) >=0) {

out.write(chunk, 0, count);

        }

}

/**

* Writes spreadsheet data in a Writer.

* (YK: in future it may evolve in a full-featured API for streaming data in Excel)

*/

    public static class SpreadsheetWriter {

private final Writer_out;

        private int _rownum;

        public SpreadsheetWriter(Writer out) {

_out = out;

        }

public void beginSheet()throws IOException {

_out.write(""

                                      +"

                                      +".org/spreadsheetml/2006/main\">");

            _out.write("\n");

        }

public void endSheet()throws IOException {

_out.write("");

            _out.write("");

        }

/**

* Insert a new row

*

        * @param rownum 0-based row number

*/

        public void insertRow(int rownum)throws IOException {

_out.write("\n");

            this._rownum = rownum;

        }

/**

* Insert row end marker

*/

        public void endRow()throws IOException {

_out.write("\n");

        }

public void createCell(int columnIndex, String value, int styleIndex)throws IOException {

String ref =new CellReference(_rownum, columnIndex).formatAsString();

            _out.write("

            if (styleIndex != -1) {

_out.write(" s=\"" + styleIndex +"\"");

            }

_out.write(">");

            _out.write("" + value +"");

            _out.write("");

        }

public void createCell(int columnIndex, String value)throws IOException {

createCell(columnIndex, value, -1);

        }

public void createCell(int columnIndex, double value, int styleIndex)throws IOException {

String ref =new CellReference(_rownum, columnIndex).formatAsString();

            _out.write("

            if (styleIndex != -1) {

_out.write(" s=\"" + styleIndex +"\"");

            }

_out.write(">");

            _out.write("" + value +"");

            _out.write("");

        }

public void createCell(int columnIndex, double value)throws IOException {

createCell(columnIndex, value, -1);

        }

public void createCell(int columnIndex, Calendar value, int styleIndex)throws IOException {

createCell(columnIndex, DateUtil.getExcelDate(value, false), styleIndex);

        }

}

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容