百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-09-20 13:57 3 浏览

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

文件下载是是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为单位,或者文件中的数据有几百万行,那么就使用第三版的文件拆分

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

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...