解析csv文件,读取百万级数据

news2024/10/3 8:29:59

 

最近在处理下载支付宝账单的需求,支付宝都有代码示例,功能完成还是比较简单的,唯一的问题就在于下载后的文件数据读取。账单文件可大可小,要保证其可用以及性能就不能简单粗暴的完成开发就行。

文件下载是是csv格式,此文件按照行读取,每一行中各列数据直接用逗号,隔开的。

前置设置:

  1. 开启了设置内存大小以及GC日志输出配置-Xms800m -Xmx800m -XX:+PrintGCDetails

  2. 测试文件total-file.csv数据量: 100万,文件大小:176M

  3. 定义账单文件的属性字段:

private static final List<String> ALI_FINANCE_LIST = new ArrayList<>(
                Arrays.asList("FINANCE_FLOW_NUMBER", "BUSINESS_FLOW_NUMBER", "MERCHANT_ORDER_NUMBER", "ITEM_NAME", "CREATION_TIME", "OPPOSITE_ACCOUNT", "RECEIPT_AMOUNT", "PAYMENT_AMOUNT", "ACCOUNT_BALANCE", "BUSINESS_CHANNEL", "BUSINESS_TYPE", "REMARK"));
复制代码

相关推荐阅读:

图形化监控工具JConsole

虚拟机的日志和日志参数

第一版:简单粗暴

直来直往,毫无技巧

拿到文件流,直接按行读取,把所有的数据放入到List<Map<String, Object>>中(其中业务相关的校验以及数据筛选都去掉了)

代码如下

    @ApiOperation(value = "测试解析-简单粗暴版")
    @GetMapping("/readFileV1")
    public ResponseEntity readFileV1(){
        File file = new File("/Users/ajisun/projects/alwaysCoding/files/total-file.csv");
        List<Map<String, Object>> context = new ArrayList<>();
        try (
                InputStream stream = new FileInputStream(file);
                InputStreamReader isr = new InputStreamReader(stream, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr)
        ) {
            String line = "";
            int number = 1;
            while ((line = br.readLine()) != null) {
                //去除#号开始的行
                if (!line.startsWith("#")) {
                    if (number >= 1) {
                        //csv是以逗号为区分的文件,以逗号区分
                        String[] columns = line.split(",", -1);
                        //构建数据
                        Map<String, Object> dataMap = new HashMap<>(16);
                        for (int i = 0; i < columns.length; i++) {
                            //防止异常,大于预定义的列不处理
                            if (i > ALI_FINANCE_LIST.size()) {
                                break;
                            }
                            dataMap.put(ALI_FINANCE_LIST.get(i), columns[i].trim());
                        }
                        context.add(dataMap);
                    }
                    number++;
                }
            }
            // TODO 存表
            System.out.println("=====插入数据库,数据条数:"+context.size());
            System.out.println("对象大小:"+(ObjectSizeCalculator.getObjectSize(context)/1048576) +" M");
            context.clear();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码

输出日志以及Jconsole的监控如下

由上面的图可以看出内存和CPU的使用率都比较高,会不断触发Full GC,最终还出现了OOM,内存基本使用完了,cpu使用也达到了近70%。

去除-Xms800m -Xmx800m的内存大小限制后可以把全部数据拿到,结果如下图所示

所有数据可以正常解析读取,Full GC也没用前一次频繁,没有出现OOM。10w条数据大小有1.2G,所占用的内存更是达到2.5G,CPU也是近60%的使用率。

仅仅是200M的csv文件,堆内存就占用了2.5G,如果是更大的文件,内存占用不得起飞了

严重占用了系统资源,对于大文件,此方法不可取。

第二版:循序渐进

缓缓图之,数据分批

第一版内存、CPU占用过大,甚至OOM,主要原因就是把所有数据全部加载到内存了。为了避免这种情况,我们可以分批处理。

参数说明:

  • file:解析的文件

  • batchNumOrder:批次号

  • context:存放数据的集合

  • count:每一批次的数据量

1. 接口API

    @ApiOperation(value = "测试解析-数据分批版")
    @GetMapping("/readFileV2")
    public ResponseEntity readFileV2(@RequestParam(required = false) int count) {
        File file = new File("/Users/ajisun/projects/alwaysCoding/files/total-file.csv");
        List<Map<String, Object>> context = new ArrayList<>();
        int batchNumOrder = 1;
        parseFile(file, batchNumOrder, context, count);
        return null;
    }
复制代码

2. 文件解析

文件解析,获取文件流

private int parseFile(File file, int batchNumOrder, List<Map<String, Object>> context, int count) {
        try (
                InputStreamReader isr = new InputStreamReader(new FileInputStream(file) , StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr)
        ) {
            batchNumOrder = this.readDataFromFile(br, context, batchNumOrder, count);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return batchNumOrder;
    }
复制代码

3. 读取文件数据

按行读取文件,分割每行数据,然后按照#{count}的数量拆分,分批次存储

private int readDataFromFile(BufferedReader br, List<Map<String, Object>> context, int batchNumOrder, int count) throws IOException {
        String line = "";
        int number = 1;
        while ((line = br.readLine()) != null) {
            //去除#号开始的行
            if (!line.startsWith("#")) {
                if (number >= 1) {
                    //csv是以逗号为区分的文件,以逗号区分
                    String[] columns = line.split(",", -1);
                    //构建数据
                    context.add(constructDataMap(columns));
                }
                number++;
            }
​
            if (context.size() >= count) {
                // TODO 存表
                System.out.println("=====插入数据库:批次:" + batchNumOrder + ",数据条数:" + context.size());
                context.clear();
                batchNumOrder++;
            }
        }
        // 最后一批次提交
        if (CollectionUtils.isNotEmpty(context)) {
            System.out.println("=====插入数据库:批次:" + batchNumOrder + ",数据条数:" + context.size());
            context.clear();
        }
        return batchNumOrder;
    }
复制代码

4.组装数据

把每一行数据按照顺序和业务对象ALI_FINANCE_LIST匹配 ,组装成功单个map数据

public Map<String, Object> constructDataMap(String[] columns) {
        Map<String, Object> dataMap = new HashMap<>(16);
        for (int i = 0; i < columns.length; i++) {
            //防止异常,大于预定义的列不处理
            if (i > ALI_FINANCE_LIST.size()) {
                break;
            }
            dataMap.put(ALI_FINANCE_LIST.get(i), columns[i].trim());
        }
        return dataMap;
    }
复制代码

5.执行结果

把文件分批读取插入数据库,可以减少内存的占用以及解决高CPU的问题。已经可以很好的处理文件读取问题了。

但是如果一个文件更大,有1G,2G 甚至更大,虽然不会造成OOM ,但是整个解析的时间就会比较长,然后如果中间出现问题,那么就需要从头再来。

假如是1000万数据的文件,按照一批次1万条插入数据库,然而到999批次的时候失败了(不考虑回滚),那么为了保证数据的完整性,该文件就需要重新上传解析。但实际上只需要最后一批次数据即可, 多了很多重复操作。

可以使用另一种方式处理,第三版

第三版:大而化小

分而治之,文件拆分

主要改动就是在第二版的基础增加文件拆分的功能,把一个大文件按照需求拆分成n个小文件,然后单独解析拆分后的小文件即可。其他方法不变。

1.接口API

获取拆分后的文件,循环解析读取

    @ApiOperation(value = "测试解析-文件拆分版")
    @GetMapping("/readFileV3")
    public ResponseEntity readFileV3(@RequestParam(required = false) int count){
        if (StringUtils.isEmpty(date)) {
            this.execCmd();
        }
        File file = new File("/Users/ajisun/projects/alwaysCoding/files");
        File[] childs = file.listFiles();//可以按照需求自行排序
        for (File file1 : childs) {
            if (!file1.getName().contains(".csv") && file1.getName().contains("total-file-")) {
                file1.renameTo(new File(file1.getAbsolutePath() + ".csv"));
            }
        }
        int batchNumOrder = 1;
        List<Map<String, Object>> context = new ArrayList<>();
        for (File child : childs) {
             if (!child.getName().contains("total-file-")){
                 continue;
             }
            batchNumOrder = parseFile(child, batchNumOrder, context, count);
        }
        return null;
    }
复制代码

2.文件拆分

按照需求使用Linux命令拆分文件,大而化小,然后按照一定规则命名

public List<String> execCmd() {
        List<String> msgList = new ArrayList<String>();
        String command = "cd /Users/ajisun/projects/alwaysCoding/files && split -a 2 -l 10000  total-file.csv  total-file-";
        try {
            ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", command);
            Process process = pb.start();
            BufferedReader ir = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = ir.readLine()) != null) {
                msgList.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(msgList);
        return msgList;
    }
复制代码

这种方式的处理在内存与CPU的占用和第二版基本没有差别。

如果采用这种方式记得文件的清理,避免磁盘空间的占用

技术扩展:文件拆分

cd /Users/ajisun/projects/alwaysCoding/files && split -a 2 -l 10000 total-file.csv total-file-
复制代码

上述字符串是两个命令用&&连接,第一个是进入到指定文件夹,第二个就是按照10000行拆分total-file.csv,而且子文件命名以total-file-开头,后缀默认两位字母结尾. 执行后的结果如下图

mac下不能用数字命名(linux下可以的),只能是默认的字母命名

Linux下:ajisun.log文件按照文件大小50m切割,后缀是2位数字结尾的子文件,子文件以ajisun-开头

总结总结

如果确定了解析的文件都是小文件,而且文件中的数据最多也就几万行,那么直接简单粗暴使用第一版也没问题。

如果文件较大,几十兆,或者文件中的数据有大几十万行,那么就使用第二版的分批处理。

如果文件很大,以G为单位,或者文件中的数据有几百万行,那么就使用第三版的文件拆分

这里只是做文件解析以及读取相关的功能,但是在实际情况中可能会存在各种各样的数据校验,这个需要根据自己的实际情况处理,但是要避免在解析大文件的时候循环校验,以及循环操作数据库。必要时还可以引入中间表存储文件数据(不做任何处理),在中间表中做数据校验 再同步到目标表。

还有没有其他更好,更优的方式,欢迎评论区讨论

我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/54107.html

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

相关文章

干货|成为优秀软件测试工程师的六大必备能力

“软件吞噬世界”、“软件定义一切”。随着软件行业的迅速发展&#xff0c;保障软件质量的关键环节——软件测试也变得越来越重要。而执行测试工作的测试工程师&#xff0c;便是软件质量的把关者。 测试工程师早在2005年就被劳动和社会保障部门列入第四批新职业中。经过短短几…

文件批量从gbk转成utf8的工具

工具名&#xff1a;GB/BIG5/UTF-8 文件编码批量转换程序 下载地址&#xff1a; https://www.wenjiangs.com/wp-content/uploads/2018/05/GB2UTF8.zip 程序功能&#xff1a;将 GB、BIG5、UTF-8 文件相互转换&#xff0c;方便的批量处理能力&#xff0c;主要用于网站文件编码方式…

单商户商城系统功能拆解41—应用中心—用户储值

单商户商城系统&#xff0c;也称为B2C自营电商模式单店商城系统。可以快速帮助个人、机构和企业搭建自己的私域交易线上商城。 单商户商城系统完美契合私域流量变现闭环交易使用。通常拥有丰富的营销玩法&#xff0c;例如拼团&#xff0c;秒杀&#xff0c;砍价&#xff0c;包邮…

R语言进行相关矩阵分析及其网络可视化

数据准备 # 选择感兴趣的列 mydata <- mtcars %>% select(mpg, disp, hp, drat, wt, qsec) # 添加一些缺失值 mydata$hp[3] <- NA # 检查数据 head(mydata, 3) ## mpg disp hp drat wt qsec ## Mazda RX4 21.0 160 110 3.90 2.62 16.5 ## Ma…

基于WiFi小车控制板的单片机小系统原理图

小系统&#xff0c;指的是的用少的元件组成的单片机可以工作的系统。一般在设计小系统的时候分为这四种必要的电路&#xff0c;分别是1、电源供电电路;2、单片机复位电路;3、时钟振荡电路;4、程序的下载接口电路。这次基于我们研发的WiFi小车51核心控制板的小系统原理图来讲一下…

uni-app实现支付及项目打包上传

本文主要介绍uni-app项目中如何实现支付功能&#xff08;支付宝支付、微信支付&#xff09;&#xff0c;及项目如何打包上传。 一、实现支付 前置工作&#xff0c;项目要实现支付功能&#xff0c;首先要在根目录manifest.json文件内App模块配置中进行设置。 其中&#xff0c;a…

机构运动学分析

背景介绍 空间机构具有结构紧凑、运动灵活等特点&#xff0c;在航空航天、精密仪器以及工业设备等领域具有广泛的应用。调研发现&#xff0c;机械臂一般采用伺服电机作为动力源&#xff0c;通过空间连杆驱动末端执行器&#xff0c;大大的减轻了工人的劳动强度。本节中主要是针对…

iconfont小图标从下载到引入到vue项目中的详细教程

地址&#xff1a;iconfont-阿里巴巴矢量图标库 iconfont小图标下载&#xff1a; &#xff08;1&#xff09;查找图标 在搜索框直接文字搜索或者看下面的小图标库&#xff0c;找想要的&#xff0c;每个小图标库都有一个名字&#xff0c;比如&#xff1a;“阿里云官网”&#x…

Vue2中$set的使用

一、什么场景下使用$set set为解决Vue2中双向数据绑定失效而生&#xff0c;只需要关注什么时候双向数据绑定会失效就可以了。 例如&#xff1a; 1.利用数组中某个项的索引直接修改该项的时候 arr[indexOfItem] newValue 2.直接修改数组的长度的时候 arr.length newLength …

gRPC学习笔记(一)

文章目录gRPC初学思维导图异步多函数多类的调用grpc初学总结&#xff1a;杂项gRPC初学思维导图 异步多函数多类的调用 一个类里有多个方法时&#xff0c; 两种方法&#xff1a; 定义不同的类&#xff08;推荐&#xff0c;只管自己的实现&#xff0c;换了请求类型&#xff0c;…

在linux系统上看全世界新闻 -- Clinews的使用详解

一. Clinews介绍 Clinews 和 InstantNews 类似&#xff0c;都是 Linux 命令行下的新闻客户端&#xff0c;安装及配置 Clinews 后就可以在 Linux 命令行下阅读新闻及头条新闻了&#xff0c; 当然还有博客新闻&#xff0c;不需要安装 GUI 应用或移动应用&#xff0c;轻松在 Linu…

值得收藏的30道Python练手题(附详解)

今天给大家分享30道Python练习题&#xff0c;建议大家先独立思考一下解题思路&#xff0c;再查看答案。 1.已知一个字符串为 “hello_world_yoyo”&#xff0c;如何得到一个队列 [“hello”,”world”,”yoyo”] &#xff1f; 使用 split 函数&#xff0c;分割字符串&#xf…

2022年最热门的短网址整理,让你不再选择恐惧

转眼一年又过去了&#xff0c;最近发现网络上有各种各样的短网址平台&#xff0c;让人眼花缭乱&#xff0c;都声称免费并且功能强大&#xff0c;但是据我的了解&#xff0c;很多免费的短网址都是有使用上的限制的&#xff0c;比如生成条数、访问次数、有广告等等、还有各种各样…

校招|拿到腾讯、阿里、字节等10家互联网测试开发岗的offer

前言 首先自我介绍一下&#xff0c;本人北京地区985本硕&#xff0c;工科非计算机专业&#xff0c;课程、毕设课题和编程以及测开都一点关系也没有。但是&#xff0c;通过自己的准备和实习积累的经验&#xff0c;在秋招的时候收获了10家互联网公司的测试开发岗和北京地区一些国…

数字图像处理(入门篇)四 像素关系

目录 1 像素关系 2 像素的领域 &#xff08;1&#xff09;4-邻域 &#xff08;2&#xff09;对角邻域 &#xff08;3&#xff09;8-领域 3 像素的邻接和连接 &#xff08;1&#xff09;4-连接 &#xff08;2&#xff09;8-连接 4 像素的连通 5 连通域 6 像素之间的距…

HMM隐马尔可夫模型

1.概率图模型&#xff1a;HMM&#xff08;隐马&#xff09;,MEMM&#xff08;最大熵&#xff09;,CRF&#xff08;条件随机场&#xff09;概率&#xff1a;既然是一个图那么就是一个有圈有边的结构&#xff0c;圈代表随机向量&#xff0c;随机变量之间有边&#xff0c;边上有概…

互联网企业面试必问 Spring 源码? 拿下Spring 源码,看完这篇就够了

前言 不用说&#xff0c;Spring 已经成为 Java 后端开发的事实上的行业标准。无数公司选择 Spring 作为基本开发框架。大多数 Java 后端程序员在日常工作中也会接触到 Spring。因此&#xff0c;如何很好地使用 Spring&#xff0c;已成为 Java 程序员的必修课之一。 同时&…

SoviChart数据可视化:散点图(Scatter plot)

什么是散点图 散点图也可以称为 x-y 图&#xff0c;用于展示数据的相关性和分布关系&#xff0c;由X轴和Y轴两个变量组成。通过因变量(Y轴数值)随自变量(X轴数值)变化的呈现数据的大致趋势&#xff0c;同时支持从类别和颜色两个维度观察数据的分布情况。 散点图通常用于显示和…

常见分布式事务解决方案

分布式事务&#xff1a;就是指事务的参与者、支持事务的服务器、资源服务器以及事务 管理器分别位于不同的分布式系统的不同节点之上。简单来说&#xff0c;分布式事务指 的就是分布式系统中的事务&#xff0c;它的存在就是为了保证不同数据库节点的数据一 致性。 聊到分布式事…

GIS工具maptalks开发手册(二)01——渲染点、文字和图片

GIS工具maptalks开发手册(二)01——渲染点、文字和图片 1、渲染点 效果 2、渲染文字 效果 3、渲染图片 效果 4.1、html单页面 <!DOCTYPE html> <html> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-wi…