Android修行手册 - 实现POI上万行的大数据量Excel读写操作,解决内存溢出

news/2024/7/21 5:51:00 标签: android, excel, poi, office, 办公
  • 点击跳转=>Unity3D特效百例
  • 点击跳转=>案例项目实战源码
  • 点击跳转=>游戏脚本-辅助自动化
  • 点击跳转=>Android控件全解手册
  • 点击跳转=>Scratch编程案例
  • 点击跳转=>软考全系列

👉关于作者

专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)
有什么需要欢迎底部卡片私我,交流让学习不再孤单

在这里插入图片描述

👉实践过程

😜问题

搞过POI的都知道,在处理Excel文件时,POI提供了两种模式:用户模式和SAX事件驱动模式。用户模式API丰富使用起来相对简单,但当遇到大文件、大量数据或复杂格式时,可能会导致内存溢出。因此,官方推荐使用SAX事件驱动模式来解析大型Excel文件。
开始想解决方法之前,我们要先知道 Excel2003与Excel2007 的区别。

Excel2003与Excel2007

主要有两打区别,支持的最大行和最大列不同,当然是2007年以后的支持的更多啦。
另一个区别就是存储方式的不同,2003版的是二进制存储,2007版的是基于XML的一种文本格式,大大提升性能和减少文件尺寸,而且支持性更广。
我们重点关注2007以后的Excel功能。

😜解决

在这里插入图片描述
下面将介绍如何使用SAX事件驱动模式读取大型Excel文件。
在这里插入图片描述
需要提前预备的三方JAR包,如果读者需要,可随时文章后面V联系我。

SAX解析读取,xlsx

首先,我们需要创建一个类,继承自DefaultHandler,并重写process()、startElement()、characters()和endElement()这四个方法。其中,process()方法用于遍历所有的sheet,依次调用startElement()、characters()和endElement()这三个方法。startElement()方法用于设定单元格的数字类型(如日期、数字、字符串等),characters()方法用于获取该单元格对应的索引值或内容值(如果单元格类型是字符串、INLINESTR、数字、日期则获取的是索引值;其他如布尔值、错误、公式则获取的是内容值)。endElement()方法根据startElement()的单元格数字类型和characters()的索引值或内容值,就能得到并打印出单元格内的数据内容。

package cn.akitaka.bigdatakt

import org.apache.poi.openxml4j.opc.OPCPackage
import org.apache.poi.ss.usermodel.BuiltinFormats
import org.apache.poi.ss.usermodel.DataFormatter
import org.apache.poi.xssf.eventusermodel.XSSFReader
import org.apache.poi.xssf.eventusermodel.XSSFReader.SheetIterator
import org.apache.poi.xssf.model.SharedStringsTable
import org.apache.poi.xssf.model.StylesTable
import org.apache.poi.xssf.usermodel.XSSFRichTextString
import org.xml.sax.Attributes
import org.xml.sax.InputSource
import org.xml.sax.SAXException
import org.xml.sax.helpers.DefaultHandler
import org.xml.sax.helpers.XMLReaderFactory
/**
 * Created by akitaka on 2023-11-06 960576866@qq.com 
 * @describe 需要用到 三方jar包 xercesImpl
 */
class ExcelXlsxReader : DefaultHandler() {
    internal enum class CellDataType { //单元格中的数据可能的数据类型
        BOOL, ERROR, FORMULA, INLINESTR, SSTINDEX, NUMBER, DATE, NULL
    }

    private var sst: SharedStringsTable? = null //共享字符串表
    private var lastIndex: String? = null //上一次的索引值
    private var filePath = "" //文件的绝对路径
    private var sheetIndex = 0 //工作表索引
    private var sheetName = "" //sheet名
    private var totalRows = 0 //总行数
    private val cellList: MutableList<String?> = ArrayList() //一行内cell集合
    private var flag = false //判断整行是否为空行的标记
    private var curRow = 1 //当前行
    private var curCol = 0 //当前列
    private var isTElement = false //T元素标识
    private var startElementFlag = true //判断上一单元格是否为文本空单元格
    private var endElementFlag = false
    private var charactersFlag = false
    /**
     * @return the exceptionMessage
     */
    val exceptionMessage: String? = null //异常信息,如果为空则表示没有异常
    
    private var nextDataType = CellDataType.SSTINDEX //单元格数据类型,默认为字符串类型
    private val formatter = DataFormatter()
    private var formatIndex: Short = 0 //单元格日期格式的索引
    private var formatString: String? = null //日期格式字符串

    //定义前一个元素和当前元素的位置,用来计算其中空的单元格数量,如A6和A8等
    private var prePreRef: String? = "A"
    private var preRef: String? = null
    private var ref: String? = null

    //定义该文档一行最大的单元格数,用来补全一行最后可能缺失的单元格
    private var maxRef: String? = null
    private var stylesTable: StylesTable? = null  //单元格

    /**
     * 遍历工作簿中所有的电子表格
     * 并缓存在mySheetList中
     *
     * @param filename
     * @throws Exception
     */
    @Throws(Exception::class)
    fun process(filename: String): Int {
        filePath = filename
        val pkg = OPCPackage.open(filename)
        val xssfReader = XSSFReader(pkg)
        stylesTable = xssfReader.stylesTable
        val sst = xssfReader.sharedStringsTable
        val parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser")
        this.sst = sst
        parser.contentHandler = this
        val sheets = xssfReader.sheetsData as SheetIterator
        while (sheets.hasNext()) { //遍历sheet
            curRow = 1 //标记初始行为第一行
            sheetIndex++
            val sheet = sheets.next() //sheets.next()和sheets.getSheetName()不能换位置,否则sheetName报错
            sheetName = sheets.sheetName
            val sheetSource = InputSource(sheet)
            parser.parse(sheetSource) //解析excel的每条记录,在这个过程中startElement()、characters()、endElement()这三个函数会依次执行
            sheet.close()
        }
        return totalRows //返回该excel文件的总行数,不包括首列和空行
    }

    /**
     * 第一个执行
     *
     * @param uri
     * @param localName
     * @param name
     * @param attributes
     * @throws SAXException
     */
    @Throws(SAXException::class)
    override fun startElement(uri: String, localName: String, name: String, attributes: Attributes) {
        //c => 单元格
        if ("c" == name) {

            //前一个单元格的位置
            if (preRef == null) {
                preRef = attributes.getValue("r")
            } else {
                //中部文本空单元格标识 ‘endElementFlag’ 判断前一次是否为文本空字符串,true则表明不是文本空字符串,false表明是文本空字符串跳过把空字符串的位置赋予preRef
                if (endElementFlag) {
                    preRef = ref
                }
            }

            //当前单元格的位置
            ref = attributes.getValue("r")
            //首部文本空单元格标识 ‘startElementFlag’ 判断前一次,即首部是否为文本空字符串,true则表明不是文本空字符串,false表明是文本空字符串, 且已知当前格,即第二格带“B”标志,则ref赋予preRef
            if (!startElementFlag && !flag) { //上一个单元格为文本空单元格,执行下面的,使ref=preRef;flag为true表明该单元格之前有数据值,即该单元格不是首部空单元格,则跳过
                // 这里只有上一个单元格为文本空单元格,且之前的几个单元格都没有值才会执行
                preRef = ref
            }

            //设定单元格类型
            setNextDataType(attributes)
            endElementFlag = false
            charactersFlag = false
            startElementFlag = false
        }
        isTElement = "t" == name   //当元素为t时
        lastIndex = ""   //置空
    }

    /**
     * 第二个执行
     * 得到单元格对应的索引值或是内容值
     * 如果单元格类型是字符串、INLINESTR、数字、日期,lastIndex则是索引值
     * 如果单元格类型是布尔值、错误、公式,lastIndex则是内容值
     * @param ch
     * @param start
     * @param length
     * @throws SAXException
     */
    @Throws(SAXException::class)
    override fun characters(ch: CharArray, start: Int, length: Int) {
        startElementFlag = true
        charactersFlag = true
        lastIndex += String(ch, start, length)
    }

    /**
     * 第三个执行
     *
     * @param uri
     * @param localName
     * @param name
     * @throws SAXException
     */
    @Throws(SAXException::class)
    override fun endElement(uri: String, localName: String, name: String) {
        //t元素也包含字符串
        if (isTElement) { //这个程序没经过
            val value = lastIndex!!.trim { it <= ' ' } //将单元格内容加入rowList中,在这之前先去掉字符串前后的空白符
            cellList.add(curCol, value)
            endElementFlag = true
            curCol++
            isTElement = false
            if ("" != value) { //如果里面某个单元格含有值,则标识该行不为空行
                flag = true
            }
        } else if ("v" == name) {
            //v => 单元格的值,如果单元格是字符串,则v标签的值为该字符串在SST中的索引
            val value = getDataValue(lastIndex!!.trim { it <= ' ' }, "") //根据索引值获取对应的单元格值
            //补全单元格之间的空单元格
            if (ref != preRef) {
                val len = countNullCell(ref, preRef)
                for (i in 0 until len) {
                    cellList.add(curCol, "")
                    curCol++
                }
            } else if (ref == preRef && !ref!!.startsWith("A")) { //ref等于preRef,且以B或者C...开头,表明首部为空格
                val len = countNullCell(ref, "A")
                for (i in 0..len) {
                    cellList.add(curCol, "")
                    curCol++
                }
            }
            cellList.add(curCol, value)
            curCol++
            endElementFlag = true
            //如果里面某个单元格含有值,则标识该行不为空行
            if ("" != value) {
                flag = true
            }
        } else {
            //如果标签名称为row,这说明已到行尾,调用optRows()方法
            if ("row" == name) {
                //默认第一行为表头,以该行单元格数目为最大数目
                if (curRow == 1) {
                    maxRef = ref
                }
                //补全一行尾部可能缺失的单元格
                if (maxRef != null) {
                    var len = -1
                    //前一单元格,true则不是文本空字符串,false则是文本空字符串
                    len = if (charactersFlag) {
                        countNullCell(maxRef, ref)
                    } else {
                        countNullCell(maxRef, preRef)
                    }
                    for (i in 0..len) {
                        cellList.add(curCol, "")
                        curCol++
                    }
                }
                if (flag && curRow != 1) { //该行不为空行且该行不是第一行,则发送(第一行为列名,不需要)
                    ExcelReaderUtil.sendRows(filePath, sheetName, sheetIndex, curRow, cellList)
                    totalRows++
                }
                cellList.clear()
                curRow++
                curCol = 0
                preRef = null
                prePreRef = null
                ref = null
                flag = false
            }
        }
    }

    /**
     * 处理数据类型
     *
     * @param attributes
     */
    fun setNextDataType(attributes: Attributes) {
        nextDataType = CellDataType.NUMBER //cellType为空,则表示该单元格类型为数字
        formatIndex = -1
        formatString = null
        val cellType = attributes.getValue("t") //单元格类型
        val cellStyleStr = attributes.getValue("s") //
        val columnData = attributes.getValue("r") //获取单元格的位置,如A1,B1
        when (cellType) {
            "b" -> nextDataType = CellDataType.BOOL  //处理布尔值
            "e" -> nextDataType = CellDataType.ERROR   //处理错误
            "inlineStr" -> nextDataType = CellDataType.INLINESTR
            "s" -> nextDataType = CellDataType.SSTINDEX  //处理字符串
            "str" -> nextDataType = CellDataType.FORMULA
        }
        if (cellStyleStr != null) { //处理日期
            val styleIndex = cellStyleStr.toInt()
            val style = stylesTable!!.getStyleAt(styleIndex)
            formatIndex = style.dataFormat
            formatString = style.dataFormatString
            if (formatString == null) {
                nextDataType = CellDataType.NULL
                formatString = BuiltinFormats.getBuiltinFormat(formatIndex.toInt())
            } else {
                if (formatString!!.contains("m/d/yyyy") || formatString!!.contains("yyyy/mm/dd") || formatString!!.contains("yyyy/m/d")) {
                    nextDataType = CellDataType.DATE
                    formatString = "yyyy-MM-dd hh:mm:ss"
                }
            }
        }
    }

    /**
     * 对解析出来的数据进行类型处理
     * @param value   单元格的值,
     * value代表解析:BOOL的为0或1, ERROR的为内容值,FORMULA的为内容值,INLINESTR的为索引值需转换为内容值,
     * SSTINDEX的为索引值需转换为内容值, NUMBER为内容值,DATE为内容值
     * @param thisStr 一个空字符串
     * @return
     */
    fun getDataValue(value: String, thisStr: String): String {
        var thisStr = thisStr
        when (nextDataType) {
            CellDataType.BOOL -> {
                val first = value[0]
                thisStr = if (first == '0') "FALSE" else "TRUE"
            }
            CellDataType.ERROR -> thisStr = "\"ERROR:$value\""
            CellDataType.FORMULA -> thisStr = '"'.toString() + value + '"'
            CellDataType.INLINESTR -> {
                var rtsi: XSSFRichTextString? = XSSFRichTextString(value)
                thisStr = rtsi.toString()
                rtsi = null
            }
            CellDataType.SSTINDEX -> {
                try {
                    val idx = value.toInt()
                    var rtss: XSSFRichTextString? = XSSFRichTextString(sst!!.getEntryAt(idx)) //根据idx索引值获取内容值
                    thisStr = rtss.toString()
                    println(thisStr)
                    //有些字符串是文本格式的,但内容却是日期
                    rtss = null
                } catch (ex: NumberFormatException) {
                    thisStr = value
                }
            }
            CellDataType.NUMBER -> {
                thisStr = if (formatString != null) {
                    formatter.formatRawCellContents(value.toDouble(), formatIndex.toInt(), formatString).trim { it <= ' ' }
                } else {
                    value
                }
                thisStr = thisStr.replace("_", "").trim { it <= ' ' }
            }
            CellDataType.DATE -> {
                thisStr = formatter.formatRawCellContents(value.toDouble(), formatIndex.toInt(), formatString)
                // 对日期字符串作特殊处理,去掉T
                thisStr = thisStr.replace("T", " ")
            }
            else -> thisStr = " "
        }
        return thisStr
    }

    fun countNullCell(ref: String?, preRef: String?): Int {
        //excel2007最大行数是1048576,最大列数是16384,最后一列列名是XFD
        var xfd = ref!!.replace("\\d+".toRegex(), "")
        var xfd_1 = preRef!!.replace("\\d+".toRegex(), "")
        xfd = fillChar(xfd, 3, '@', true)
        xfd_1 = fillChar(xfd_1, 3, '@', true)
        val letter = xfd.toCharArray()
        val letter_1 = xfd_1.toCharArray()
        val res = (letter[0].code - letter_1[0].code) * 26 * 26 + (letter[1].code - letter_1[1].code) * 26 + (letter[2].code - letter_1[2].code)
        return res - 1
    }

    fun fillChar(str: String, len: Int, let: Char, isPre: Boolean): String {
        var str = str
        val len_1 = str.length
        if (len_1 < len) {
            if (isPre) {
                for (i in 0 until len - len_1) {
                    str = let.toString() + str
                }
            } else {
                for (i in 0 until len - len_1) {
                    str += let
                }
            }
        }
        return str
    }
}

继承HSSFListener类来解决Excel2003文件,xls

我们需要创建一个类,继承自HSSFListener,并重写processRecord()方法。这个方法是核心方法,用于处理sheetName和各种单元格数字类型。

package cn.akitaka.bigdatakt

import org.apache.poi.hssf.eventusermodel.*
import org.apache.poi.hssf.eventusermodel.EventWorkbookBuilder.SheetRecordCollectingListener
import org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord
import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord
import org.apache.poi.hssf.model.HSSFFormulaParser
import org.apache.poi.hssf.record.*
import org.apache.poi.hssf.usermodel.HSSFDataFormatter
import org.apache.poi.hssf.usermodel.HSSFWorkbook
import org.apache.poi.poifs.filesystem.POIFSFileSystem
import java.io.FileInputStream

/**
 * Created by akitaka on 2023-11-06 960576866@qq.com 
 * @describe 
 */
class ExcelXlsReader : HSSFListener {
    private val minColums = -1
    private var fs: POIFSFileSystem? = null
    private var totalColums = 0  //把第一行列名的长度作为列的总长
    private var totalRows = 0  //总行数
    private var lastRowNumber = 0  //上一行row的序号
    private var lastColumnNumber = 0  //上一单元格的序号
    private val outputFormulaValues = true  //是否输出formula,还是它对应的值
    private var workbookBuildingListener: SheetRecordCollectingListener? = null  //用于转换formulas

    //excel2003工作簿
    private var stubWorkbook: HSSFWorkbook? = null
    private var sstRecord: SSTRecord? = null
    private var formatListener: FormatTrackingHSSFListener? = null
    private val formatter = HSSFDataFormatter()
    private var filePath = ""  //文件的绝对路径

    //表索引
    private var sheetIndex = 0
    private var orderedBSRs: Array<BoundSheetRecord>? = null
    private val boundSheetRecords:MutableList<BoundSheetRecord> = ArrayList()
    private var nextRow = 0
    private var nextColumn = 0
    private var outputNextStringRecord = false

    //当前行
    private var curRow = 0

    //存储一行记录所有单元格的容器
    private val cellList: MutableList<String?> = ArrayList()
    private var flag = false  //判断整行是否为空行的标记
    private var sheetName: String? = null

    /**
     * 遍历excel下所有的sheet
     */
    @Throws(Exception::class)
    fun process(fileName: String): Int {
        filePath = fileName
        fs = POIFSFileSystem(FileInputStream(fileName))
        val listener = MissingRecordAwareHSSFListener(this)
        formatListener = FormatTrackingHSSFListener(listener)
        val factory = HSSFEventFactory()
        val request = HSSFRequest()
        if (outputFormulaValues) {
            request.addListenerForAllRecords(formatListener)
        } else {
            workbookBuildingListener = SheetRecordCollectingListener(formatListener)
            request.addListenerForAllRecords(workbookBuildingListener)
        }
        factory.processWorkbookEvents(request, fs)
        return totalRows //返回该excel文件的总行数,不包括首列和空行
    }

    /**
     * HSSFListener 监听方法,处理Record
     * 处理每个单元格
     */
    override fun processRecord(record: Record) {
        var thisRow = -1
        var thisColumn = -1
        var thisStr: String? = null
        var value: String? = null
        when (record.sid) {
            BoundSheetRecord.sid -> boundSheetRecords.add(record as BoundSheetRecord)
            BOFRecord.sid -> {
                val br = record as BOFRecord
                if (br.type == BOFRecord.TYPE_WORKSHEET) {
                    //如果有需要,则建立子工作簿
                    if (workbookBuildingListener != null && stubWorkbook == null) {
                        stubWorkbook = workbookBuildingListener!!.stubHSSFWorkbook
                    }
                    if (orderedBSRs == null) {
                        orderedBSRs = BoundSheetRecord.orderByBofPosition(boundSheetRecords)
                    }
                    sheetName = orderedBSRs!![sheetIndex].sheetname
                    sheetIndex++
                }
            }
            SSTRecord.sid -> sstRecord = record as SSTRecord
            BlankRecord.sid -> {
                val brec = record as BlankRecord
                thisRow = brec.row
                thisColumn = brec.column.toInt()
                thisStr = ""
                cellList.add(thisColumn, thisStr)
            }
            BoolErrRecord.sid -> {
                val berec = record as BoolErrRecord
                thisRow = berec.row
                thisColumn = berec.column.toInt()
                thisStr = berec.booleanValue.toString() + ""
                cellList.add(thisColumn, thisStr)
                checkRowIsNull(thisStr) //如果里面某个单元格含有值,则标识该行不为空行
            }
            FormulaRecord.sid -> {
                val frec = record as FormulaRecord
                thisRow = frec.row
                thisColumn = frec.column.toInt()
                if (outputFormulaValues) {
                    if (java.lang.Double.isNaN(frec.value)) {
                        outputNextStringRecord = true
                        nextRow = frec.row
                        nextColumn = frec.column.toInt()
                    } else {
                        thisStr = '"'.toString() + HSSFFormulaParser.toFormulaString(stubWorkbook, frec.parsedExpression) + '"'
                    }
                } else {
                    thisStr = '"'.toString() + HSSFFormulaParser.toFormulaString(stubWorkbook, frec.parsedExpression) + '"'
                }
                cellList.add(thisColumn, thisStr)
                checkRowIsNull(thisStr) //如果里面某个单元格含有值,则标识该行不为空行
            }
            StringRecord.sid -> if (outputNextStringRecord) {
                val srec = record as StringRecord
                thisStr = srec.string
                thisRow = nextRow
                thisColumn = nextColumn
                outputNextStringRecord = false
            }
            LabelRecord.sid -> {
                val lrec = record as LabelRecord
                run {
                    thisRow = lrec.row
                    curRow = thisRow
                }
                thisColumn = lrec.column.toInt()
                value = lrec.value.trim { it <= ' ' }
                value = if (value == "") "" else value
                cellList.add(thisColumn, value)
                checkRowIsNull(value) //如果里面某个单元格含有值,则标识该行不为空行
            }
            LabelSSTRecord.sid -> {
                val lsrec = record as LabelSSTRecord
                run {
                    thisRow = lsrec.row
                    curRow = thisRow
                }
                thisColumn = lsrec.column.toInt()
                if (sstRecord == null) {
                    cellList.add(thisColumn, "")
                } else {
                    value = sstRecord!!.getString(lsrec.sstIndex).toString().trim { it <= ' ' }
                    value = if (value == "") "" else value
                    cellList.add(thisColumn, value)
                    checkRowIsNull(value) //如果里面某个单元格含有值,则标识该行不为空行
                }
            }
            NumberRecord.sid -> {
                val numrec = record as NumberRecord
                run {
                    thisRow = numrec.row
                    curRow = thisRow
                }
                thisColumn = numrec.column.toInt()

                //第一种方式
                //value = formatListener.formatNumberDateCell(numrec).trim();//这个被写死,采用的m/d/yy h:mm格式,不符合要求

                //第二种方式,参照formatNumberDateCell里面的实现方法编写
                val valueDouble = numrec.value
                var formatString = formatListener!!.getFormatString(numrec)
                if (formatString.contains("m/d/yy") || formatString.contains("yyyy/mm/dd") || formatString.contains("yyyy/m/d")) {
                    formatString = "yyyy-MM-dd hh:mm:ss"
                }
                val formatIndex = formatListener!!.getFormatIndex(numrec)
                value = formatter.formatRawCellContents(valueDouble, formatIndex, formatString).trim { it <= ' ' }
                value = if (value == "") "" else value
                //向容器加入列值
                cellList.add(thisColumn, value)
                checkRowIsNull(value) //如果里面某个单元格含有值,则标识该行不为空行
            }
            else -> {}
        }

        //遇到新行的操作
        if (thisRow != -1 && thisRow != lastRowNumber) {
            lastColumnNumber = -1
        }

        //空值的操作
        if (record is MissingCellDummyRecord) {
            val mc = record
            thisRow = mc.row
            curRow = thisRow
            thisColumn = mc.column
            cellList.add(thisColumn, "")
        }

        //更新行和列的值
        if (thisRow > -1) lastRowNumber = thisRow
        if (thisColumn > -1) lastColumnNumber = thisColumn

        //行结束时的操作
        if (record is LastCellOfRowDummyRecord) {
            if (minColums > 0) {
                //列值重新置空
                if (lastColumnNumber == -1) {
                    lastColumnNumber = 0
                }
            }
            lastColumnNumber = -1
            if (flag) { //该行不为空行且该行不是第一行,发送(第一行为列名,不需要)
                if (curRow == 0) {
                    totalColums = cellList.size //获取第一行列名的总数
                } else {
                    //2003版尾部为空单元格的,xls里面是以该行最后一个有值的单元格为结束标记的,尾部空单元格跳过,故需补全
                    if (cellList.size <= totalColums) { // 其他行如果尾部单元格总数小于totalColums,则补全单元格
                        for (i in cellList.size until totalColums) {
                            cellList.add(i, "")
                        }
                    }
                    ExcelReaderUtil.sendRows(filePath, sheetName, sheetIndex, curRow + 1, cellList) //每行结束时,调用sendRows()方法
                    totalRows++
                }
            }
            //清空容器
            cellList.clear()
            flag = false
        }
    }

    /**
     * 如果里面某个单元格含有值,则标识该行不为空行
     * @param value
     */
    fun checkRowIsNull(value: String?) {
        if (value != null && "" != value) {
            flag = true
        }
    }
}

使用帮助类

package cn.akitaka.bigdatakt

import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream

/**
 * Created by akitaka on 2023-11-06 960576866@qq.com 
 * @describe 
 */

object ExcelReaderUtil {
    //excel2003扩展名
    const val EXCEL03_EXTENSION = ".xls"

    //excel2007扩展名
    const val EXCEL07_EXTENSION = ".xlsx"

    /**
     * 每获取一条记录,即打印
     * 在flume里每获取一条记录即发送,而不必缓存起来,可以大大减少内存的消耗,这里主要是针对flume读取大数据量excel来说的
     */
    fun sendRows(filePath: String?, sheetName: String?, sheetIndex: Int, curRow: Int, cellList: List<String?>) {
        val oneLineSb = StringBuffer()
        oneLineSb.append(filePath)
        oneLineSb.append("--")
        oneLineSb.append("sheet$sheetIndex")
        oneLineSb.append("::$sheetName") //加上sheet名
        oneLineSb.append("--")
        oneLineSb.append("row$curRow")
        oneLineSb.append("::")
        for (cell in cellList) {
            oneLineSb.append(cell!!.trim { it <= ' ' })
            oneLineSb.append("|")
        }
        var oneLine = oneLineSb.toString()
        if (oneLine.endsWith("|")) {
            oneLine = oneLine.substring(0, oneLine.lastIndexOf("|"))
        } // 去除最后一个分隔符
        println(oneLine)
    }

    @Throws(Exception::class)
    fun readExcel(fileName: String) {
        var totalRows = 0
        totalRows = if (fileName.endsWith(EXCEL03_EXTENSION)) { //处理excel2003文件
            val excelXls = ExcelXlsReader()
            excelXls.process(fileName)
        } else if (fileName.endsWith(EXCEL07_EXTENSION)) { //处理excel2007文件
            val excelXlsxReader = ExcelXlsxReader()
            excelXlsxReader.process(fileName)
        } else {
            throw Exception("文件格式错误,fileName的扩展名只能是xls或xlsx。")
        }
        println("发送的总行数:$totalRows")
    }

    @Throws(Exception::class)
    fun copyToTemp(file: File?, tmpDir: String?) {
        val fis = FileInputStream(file)
        val file1 = File(tmpDir)
        if (file1.exists()) {
            file1.delete()
        }
        val fos = FileOutputStream(tmpDir)
        val b = ByteArray(1024)
        var n = 0
        while (fis.read(b).also { n = it } != -1) {
            fos.write(b, 0, n)
        }
        fis.close()
        fos.close()
    }
}

写入

对于大数据的Xlsx文件的写入,POI3.8提供了SXSSFSXSSFWorkbook类,采用缓存方式进行大批量写文件。
详情可以查看poi官网示例:跳转链接

核心原理便是通过设置SXXFWorkbook的构造参数,可以设置每次在内存中保持的行数,当达到这个值的时候,那么会把这些数据flush到磁盘上,这样就不会出现内存不够的情况。

因为每人具体写入的内容不同,这个封装工具类麻烦。

推荐一个开源项目:跳转 里面有 hutool-poi 对 Excel 写入进行了封装
文档查看

👉其他

📢作者:小空和小芝中的小空
📢转载说明-务必注明来源:https://zhima.blog.csdn.net/
📢这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气💚,日后定有一番大作为📝!!!旁边有点赞👍收藏🌟今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。

温馨提示点击下方卡片获取更多意想不到的资源。
空名先生


http://www.niftyadmin.cn/n/5156764.html

相关文章

记录:Unity脚本的编写5.0

目录 前言创建动画Unity Animation、Animator常用类关于两者的区别Animator 编写脚本 大型连续剧之在untiy中&#xff08;或者别的什么活动&#xff09; 前言 之前在场景中添加了背景音乐&#xff0c;而在我们的日常的体验中&#xff0c;可以发现游戏或者场景中有很多有趣的动…

Spring加载的过程

1. 环境准备&#xff1a; 在加载过程开始之前&#xff0c;Spring首先会进行环境准备。这包括读取配置文件、初始化配置参数等操作&#xff0c;然后创建应用上下文&#xff08;ApplicationContext&#xff09;容器。Spring支持多种配置方式&#xff0c;如XML配置、注解配置和Jav…

成功解决fatal error: stdatomic.h: No such file or directory #include <stdatomic.h>

成功解决fatal error: stdatomic.h: No such file or directory #include <stdatomic.h> 目录 解决问题 解决思路 解决方法 解决问题 I llama.cpp build info: I UNAME_S: Linux I UNAME_P: x86_64 I UNAME_M: x86_64 I CFLAGS: -I. -Icommon -D_XOPEN_SOUR…

吃透BGP,永远绕不开这些基础概述,看完再也不怕BGP了!

你们好&#xff0c;我的网工朋友。 总有人在私信里抱怨&#xff0c;BGP实在是太难了&#xff01; 一是这玩意儿本来就很复杂&#xff0c;需要处理大量的路由信息和复杂的算法&#xff1b;再一个是需要你有一定的实战经验才能深入理解运作。 虽然BGP确实有一定难度&#xff0c…

gitlab 设置 分支只读

一&#xff0c;设置master分支只读&#xff0c; 并且只有Maintainers 拥有合并权限。 二&#xff0c;设置成员权限 改为developer 三&#xff0c;邀请成员 点击右上角 Invite Members

ifream标签中的子页面,操作父页面的元素

问题描述&#xff1a;子页面内容发生变化时&#xff0c;导航栏不会跟切换 解决办法&#xff1a; window.parent.document.getElementById demo html1 <html> <head><meta charset"UTF-8"><!-- import CSS --><link rel"stylesh…

python tempfile 模块使用

在Python中&#xff0c;tempfile 模块用于创建临时文件和目录&#xff0c;它们可以用于存储中间处理数据&#xff0c;不需要长期保存。该模块提供了几种不同的类和函数来创建临时文件和目录。 下面是几个常用的 tempfile 使用方法&#xff1a; 临时文件 使用 NamedTemporary…

力扣labuladong——一刷day21

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、力扣48. 旋转图像二、力扣54. 螺旋矩阵三、力扣59. 螺旋矩阵 II 通过图形变换处理旋转图像 前言 一、力扣48. 旋转图像 class Solution {public void ro…