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

news2024/10/7 8:28:16
  • 点击跳转=>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.coloradmin.cn/o/1178333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web前端JS基础------制作一个获取验证码

1&#xff0c;需要一个定时器&#xff0c;和一个button&#xff0c;通过点击事件启动获取验证码 2&#xff0c;参考代码如下 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><…

Linux中for循环

for do done 复习知识点&#xff1a;cut命令&#xff0c;id命令&#xff0c;finger命令&#xff0c;for循环 程序如上&#xff0c;-d 接分隔符&#xff0c;-f后的数字表示分隔后的列 从结果可以看出&#xff0c;系统上没有finger这个命令&#xff0c;后面会学到yum安装命令&a…

上手SQL语句调优必须了解的内容——Explain

在做性能测试时&#xff0c;资深的性能测试工程师&#xff0c;都会帮助研发同学优化sql语句&#xff0c;听起来很高深&#xff0c;但是具体操作是比较容易的&#xff0c;使用expain命令就可以了&#xff01;本文我会用最简单有效的方式带大家掌握expain的使用方法&#xff01; …

JVM虚拟机:垃圾回收器之Serial(年轻代)

本文重点 本文将介绍年轻代的Serial回收器,它最主要的特征就是串行化的回收器。 运行方式 Serial是一个单线程的收集器,在进行垃圾收集的时候,必须暂停其它所有的工作线程(java程序找一个安全点safe point然后才停止执行,进行等待)直到垃圾回收结束,下的运行状态图如…

迅为iTOP-i.MX8M开发板使用 make 工具

make 工具是编译辅助工具&#xff0c;用来解决使用命令编译工程非常繁琐的问题。 调用这个命令工具&#xff1a;我们在 windows 上编程使用 ide &#xff0c;我们有图形界面&#xff0c;有相应的按钮&#xff0c;比如说 build 或者 run 来编译。其实 make 这个编译辅助工具使…

Dajngo学习笔记(3)

电话号码管理 查看功能 class PrettyNum(models.Model):mobilemodels.CharField(verbose_name"电话号",max_length11)pricemodels.IntegerField(verbose_name"价格")level_choice((1,"一级"),(2,"二级"),(3,"三级"))level…

RISC-V处理器设计(四)—— Verilog 代码设计

一、前言 从6月底刚开始接触 risc-v 架构&#xff0c;到现在完成了一个 risc-v cpu 的设计&#xff0c;并且成功移植了 rt-thread nano 到本 cpu 上运行&#xff0c;中间经过了 4个多月的时间&#xff0c;遇到了数不清的问题&#xff0c;也想过放弃&#xff0c;但好在最后还是…

美国阿贡国家实验室发布快速自动扫描套件 FAST,助力显微技术「快速阅读」成为可能

「我高兴地在北京市的天安门广场上看红色的国旗升起」 快速阅读一下这个句子&#xff0c;大家可能会发现&#xff0c;只需「我在天安门广场看升旗」几个字&#xff0c;就能概述我们需要的信息&#xff0c;也就是说&#xff0c;无需逐字逐句地阅读&#xff0c;抓住重点即可破译…

【flutter no devices】

1.在环境变量增加 ANDROID_HOME 值为&#xff1a;C:\Users\Administrator\AppData\Local\Android\Sdk &#xff08;Android sdk 位置) 2 环境变量的path里面增加2个值&#xff1a; %ANDROID_HOME%\platform-tools %ANDROID_HOME%\tools 3 打开cmd&#xff0c;或者在Android st…

嵌入式Linux和stm32区别? 之间有什么关系吗?

嵌入式Linux和stm32区别? 之间有什么关系吗&#xff1f; 主要体现在以下几个方面&#xff1a; 1.硬件资源不同 单片机一般是芯片内部集成flash、ram&#xff0c;ARM一般是CPU&#xff0c;配合外部的flash、ram、sd卡存储器使用。最近很多小伙伴找我&#xff0c;说想要一些嵌…

【滑动窗口】篮里到底能装 “几个水果” 呢?

Problem: 904. 水果成篮 文章目录 题目分析算法原理分析暴力枚举 哈希表滑动窗口优化数组再度优化 复杂度Code 题目分析 首先我们来分析一下本题的思路 首先我们通过题目的描述来理解一下其要表达的含义&#xff0c;题目给到我们一个fruit数组&#xff0c;里面存放的是每棵树上…

Linux Vim批量注释和自定义注释

使用 Vim 编辑 Shell 脚本&#xff0c;在进行调试时&#xff0c;需要进行多行的注释&#xff0c;每次都要先切换到输入模式&#xff0c;在行首输入注释符"#"再退回命令模式&#xff0c;非常麻烦。连续行的注释其实可以用替换命令来完成。 换句话说&#xff0c;在指定…

推特被封号怎么办?如何防封?

今年社交媒体巨头Twitter正式更名与标示为“X”&#xff0c;这一举措引发了广泛关注和讨论。马斯克称&#xff0c;此举是为了将推特重塑为一个广泛的通信和金融交易平台&#xff0c;打造一个像“微信”一样的万能应用程序&#xff0c;也就是“X”&#xff0c;并承诺推特将迅速进…

外汇天眼:全员免费,赢奖金!

外汇市场一直以来都是金融投资者的热门领域之一&#xff0c;但对于新手来说&#xff0c;了解和掌握外汇交易可能需要时间和经验。为了帮助新手入门&#xff0c;提高交易技能&#xff0c;外汇模拟交易应运而生。为的是能够零风险无压力地帮助外汇投资者更好地掌握外汇交易的技巧…

SonarQube的使用心得

一、使用背景&#xff1a; SonarQube 是一个用于代码质量管理的开源平台&#xff0c;用于管理源代码的质量。 通过插件形式&#xff0c;可以支持包括 java, C#, C/C, PL/SQL, Cobol, JavaScrip, Groovy 等等二十几种编程语言的代码质量管理与检测。 Sonar可以从以下七个维度…

给定n个点或一个凸边形,求其最小外接矩形,可视化

这里写目录标题 原理代码 原理 求n个点的最小外接矩形问题可以等价为先求这n个点的凸包&#xff0c;再求这个凸包的最小外接矩形。 其中求凸包可以使用Graham-Scan算法 需要注意的是&#xff0c; 因为Graham-Scan算法要求我们从先找到凸包上的一个点&#xff0c;所以我们可以先…

CRM 报告:跟踪销售业绩的强大工具

对于希望保持良好客户关系的企业来说&#xff0c;CRM&#xff08;客户关系管理&#xff09;报告是不可或缺的。它使企业能够跟踪客户互动&#xff0c;并利用这些数据改善客户服务。 需要注意的是&#xff0c;CRM 报告不是一次性的&#xff0c;而是一个持续的过程。这是因为客户…

Webpack介绍大全

Webpack 一 、什么是webpack WebPack是一个现代JS应用程序的静态模块打包器&#xff08;module bundler&#xff09; 模块&#xff08;模块化开发&#xff0c;可以提高开发效率&#xff0c;避免重复造轮子&#xff09; 打包&#xff08;将各个模块&#xff0c;按照一定的规则…

“第六十二天”

新东西 %[^\n] 这个题测试的时候不知道哪里为什么一直错一个。 int main() {char a[81] { 0 };fgets(a, 80, stdin);int i 0;int n strlen(a);for(i0;i<n;i){if ((a[i] > a && a[i] < y) || (a[i] > A && a[i] < Y))a[i] 1;else if (a[i…

中介模式(Mediator)

简介 当各个模块的调用变得错综复杂时&#xff0c;可以使用中介模式&#xff0c;用一个中介对象完成对象交互&#xff0c;各个对象不需要显示的相互引用。 创建一个中介对象完成所有的调用&#xff1a;Mediator->A ,Mediator->B,Mediator->C,Mediator->D&#xf…