java POI 事务驱动读取Excel大文件

当使用poi去处理大的excel文件时,直接使用poi里提供的数据读取方法容易产生内存溢出的情况,在这里插入代码片这时候需要将xlsx格式的文件转化为xml文件来读取。

  1. 对于新手来说,先来看看xlsx格式与xml格式的区别吧。
    xlsx格式数据
    xml格式数据
    我们不难看到xml格式的数据里,每个数据的位置,内容,类型都被xml标签描述,所以在后续读取xml格式的文件时,这些标签也是需要用到的。也正因为这些标签,当数据量量很大时相同数据量的xml文件往往比xlsx文件占用更多的存储空间。

  2. 用到的第三方包
    在这里插入图片描述
    这里用到的包可能比必要的包的多,但都下下来不碍事。但是xerceslmpl包不能和xerces包放在一起,会产生jar冲突,因为xerceslmpl其实是xerces的升级版,如果你的referenced libraries里面两个都有的话,记得删除一个。

  3. 代码
    代码来源于网络,作者根据自己的需求进行了一些修改

import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * POI事件驱动读取Excel文件的抽象类。
 * DefaultHandler里的方法都是no operation,需要我们自己去重写
 * 
 * @author elon
 * @version 2018年7月7日
 */
public abstract class ExcelAbstract extends DefaultHandler {
    private SharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;

    private int curRow = 0;
    private String curCellName = "";

    /**
     * 读取当前行的数据。key是单元格名称如A1,value是单元格中的值。如果单元格式空,则没有数据。
     */
    private Map<String, String> rowValueMap = new HashMap<>();

    /**
     * 处理单行数据的回调方法。
     * 
     * @param curRow 当前行号
     * @param rowValueMap 当前行的值
     * @throws SQLException
     */
    public abstract void optRows(int curRow, Map<String, String> rowValueMap);

    /**
     * 读取Excel指定sheet页的数据。
     * 
     * @param filePath 文件路径
     * @param sheetNum sheet页编号.从1开始。
     * @throws Exception
     */
    public void readOneSheet(String filePath, int sheetNum) throws Exception {
        OPCPackage pkg = OPCPackage.open(filePath);
        XSSFReader r = new XSSFReader(pkg);
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = getSheetParser(sst);

        // 根据 rId# 或 rSheet# 查找sheet
        InputStream sheet2 = r.getSheet("rId" + sheetNum);
        InputSource sheetSource = new InputSource(sheet2);
        parser.parse(sheetSource);
        sheet2.close();
        pkg.close();
    }

    @Override
    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
        // c => 单元格
        if (name.equals("c")) {
            
            // 如果下一个元素是 SST 的索引,则将nextIsString标记为true
            String cellType = attributes.getValue("t");
            if (cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
        }
        
        // 置空
        lastContents = "";
        
        /**
         * 记录当前读取单元格的名称
         */
        String cellName = attributes.getValue("r");
        if (cellName != null && !cellName.isEmpty()) {
            curCellName = cellName;
        }
    }

    @Override
    public void endElement(String uri, String localName, String name) throws SAXException {
        // 根据SST的索引值的到单元格的真正要存储的字符串
        // 这时characters()方法可能会被调用多次
        if (nextIsString) {
            try {
                int idx = Integer.parseInt(lastContents);
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
            } catch (Exception e) {

            }
        }

        // v => 单元格的值,如果单元格是字符串则v标签的值为该字符串在SST中的索引
        // 将单元格内容加入rowlist中,在这之前先去掉字符串前后的空白符
        if (name.equals("v")) {
            String value = lastContents.trim();
            value = value.equals("") ? " " : value;
           // System.out.println(curCellName);
            rowValueMap.put(curCellName, value);
        } else {
            // 如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法
            if (name.equals("row")) {
                optRows(curRow, rowValueMap);
                rowValueMap.clear();
                curRow++;
            }
        }
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
        // 得到单元格内容的值
        lastContents += new String(ch, start, length);
    }
    
    /**
     * 获取单个sheet页的xml解析器。
     * @param sst
     * @return
     * @throws SAXException
     */
    private XMLReader getSheetParser(SharedStringsTable sst) throws SAXException {
        XMLReader parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
        this.sst = sst;
        parser.setContentHandler(this);
        return parser;
    }
}

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.poi.hssf.usermodel.HSSFDateUtil;

/**
 * Excel读取公共类。
 * @author elon
 * @version 2018年7月7日
 */
public class ExcelReaderUtil extends ExcelAbstract {
    
    /**
     * 提取列名称的正则表达式
     */
    private static final String DISTILL_COLUMN_REG = "^([A-Z]{1,})";
    
    /**
     * 读取excel的每一行记录。map的key是列号(A、B、C...), value是单元格的值。如果单元格是空,则没有值。
     * 仅保留每一行里,A,I,H三列的值
     */
    private List<Map<String, String>> dataList = new ArrayList<>();  //存放最后提取到的数据,每一行的记录为一个Map,为一个List元素
    
    @Override
    public void optRows(int curRow, Map<String, String> rowValueMap) {
        
        Map<String, String> dataMap = new HashMap<>();
      //  rowValueMap.forEach((k,v)->dataMap.put(removeNum(k), v));
        for(Map.Entry<String, String> entry : rowValueMap.entrySet()) {
        	if(removeNum(entry.getKey()).equals("D")||removeNum(entry.getKey()).equals("I")||removeNum(entry.getKey()).equals("H")) {
        		dataMap.put(removeNum(entry.getKey()), entry.getValue());
        	}
        }
        dataList.add(dataMap);
    }
    
    /**
     * 日期数字转换为字符串。
     * 
     * @param dateNum excel中存储日期的数字
     * @return 格式化后的字符串形式
     */
    public static String dateNum2Str(String dateNum) {
        Date date = HSSFDateUtil.getJavaDate(Double.parseDouble(dateNum));
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        return formatter.format(date);
    }
    
    /**
     * 比较两个日期远近,返回离当前日期近的那个
     * @param Date1
     * @param Date2
     * @return
     * @throws ParseException 
     */
    public static String getCloseDate(String Date1, String Date2) throws ParseException {
    	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		Date date1 = sdf.parse(Date1);
		Date date2 = sdf.parse(Date2);
		if(date1.before(date2)) {
			return Date1;
		}else {
			return Date2;
		}
    	
    }
    
    /**
     * 删除单元格名称中的数字,只保留列号。
     * @param cellName 单元格名称。如:A1
     * @return 列号。如:A
     */
    private String removeNum(String cellName) {
        Pattern pattern = Pattern.compile(DISTILL_COLUMN_REG);
        Matcher m = pattern.matcher(cellName);
        if (m.find()) {
            return m.group(1);
        }
        
        return "";
    }

    public List<Map<String, String>> getDataList() {
        return dataList;
    }
}
import java.util.List;
import java.util.Map;

import com.model.JDRecord;

public class TestExcel {
    public static void main(String[] args) {
        try {
            ExcelReaderUtil excel = new ExcelReaderUtil();
            excel.readOneSheet("D:\\test.xlsx", 1);
            List<Map<String, String>> dataList = excel.getDataList();
            for (Map<String, String> map : dataList) {

				System.out.println(map);
			}
           // System.out.println(dataList.size());
           // System.out.println(dataList.get(0));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在最后运行时,如果直接成功很好。如果运行失败,读不出来数据的话,可以试试先将需要读取的xlsx文件打开来,重新保存一下,再运行程序读取,可能会成功,其中具体的原理,作者也还没弄明白,只能当作一个小bug来处理了,欢迎了解的大佬来解惑。

最后检测,用老旧的电脑(还是32位的),读取8~9MB的excel xlsx文件都在十秒以内,包括对数据的一些处理操作。几十MB的文件没测试过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值