Java 解析CSV文件 CSV解析

  • Post author:
  • Post category:java




需求背景

从一个数据库导出一个表的数据,导出文件为CSV文件;需要将数据导入到本地开发环境的数据库里面。CSV文件已经有了,需要解析读CSV文件,并导入进数据库。本文主要研究第一部分,自己写代码解析CSV文件。

Java中可以找到Jar包,工具类,直接使用。也不用自己手工写解析。如果有更成熟的工具,建议直接使用。本文作者写的,只有自己测试过,如果有bug,欢迎留言,不一定及时回。



CSV文件规则

以纯文本形式存储表格数据(数字和文本)。

记录间以换行符分隔;每条记录由字段组成,字段间的分隔符是逗号。

注意:

1、字段中包含有逗号,有换行符,该字段必须用双引号括起来;

2、字段中的双引号用两个双引号表示;

以上提到的逗号和双引号均为半角字符。

如果只是简单的逗号分割,那么就很简单。本文主要研究的就是,有特殊字符作为字段的值,上面注意中的情况出现的情况下,文件的解析。

CSV规则有遗漏的,或者逻辑不完善,能导致歧义的数据案例,评论指出,感谢!



准备测试数据

可以新建一个test.csv文件,用Excel输入值,然后文本打开,观察值。注意包含以下测试案例:

  1. 正常数据
  2. 空数据
  3. 文本有逗号
  4. 文本有引号
  5. 文本中有换行

    例如:
    Excel视图

文本视图

好的测试数据是开发的前提。习惯先准备,罗列测试数据的所有情况,再开发。开发的时候考虑的更全面。开发也可以分类,从解析简单情况开始。



代码


    /**
     * 解析CSV字符行集合
     * @param lines java 自带的读文件流,按换行符分割的,每行是集合的一个元素
     * @return 解析完的结果集合;List<List<String>> resList 所有行是一个集合, 每行的所有字段是一个集合。
     */
    public static List<List<String>> spiltCSVString(List<String> lines) {
        List<String> oneLine = new ArrayList<String>();
        List<List<String>> resList = new ArrayList<>();
        if (lines.isEmpty() || lines.size() == 0) {
            return resList;
        }
        String tempStr = "";
        char specialChar = '\"';
        char splitChar = ',';
        boolean specialFlag = false;
        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);
            if (line.indexOf(specialChar) < 0) {
                oneLine = new ArrayList<String>();
                for (int j = 0; j < line.toCharArray().length; j++) {
                    if (line.charAt(j) == splitChar) {
                        oneLine.add(tempStr);
                        tempStr = "";
                        continue;
                    } else {
                        tempStr += line.charAt(j);
                    }
                }
                if (!"".equals(tempStr)) {
                    oneLine.add(tempStr);
                    tempStr = "";
                }
                resList.add(oneLine);
            } else {
                if (!specialFlag) oneLine = new ArrayList<String>();
                for (int j = 0; j < line.toCharArray().length; j++) {
                    if (line.charAt(j) == specialChar) {
                        if (specialFlag) {
                            if (j + 1 < line.toCharArray().length && line.charAt(j + 1) == specialChar) {
                                tempStr += specialChar;
                            } else {
                                oneLine.add(tempStr);
                                tempStr = "";
                                specialFlag = false;
                            }
                            j++;
                            continue;
                        } else {
                            specialFlag = true;
                            continue;
                        }
                    } else if (line.charAt(j) == splitChar && !specialFlag) {
                        oneLine.add(tempStr);
                        tempStr = "";
                        continue;
                    }
                    tempStr += line.charAt(j);
                }
                if (!specialFlag) {
                    if (!"".equals(tempStr)) {
                        oneLine.add(tempStr);
                        tempStr = "";
                    }
                    resList.add(oneLine);
                } else {
                    tempStr += "\r\n";
                }
            }
        }
        return resList;
    }

先用熟悉的Java写完,更快,再换成m语言的:


/// 解析CSV文件
/// filePath 文件路径,需要放到服务器上,是服务器上的路径,
/// 返回:每行数据分割后组成一个集合;所有行又放在返回的大集合里面。
/// w ##class(ext.util.String).ParseCSVFile("C:\Users\HuangZhi\Desktop\test.csv")
ClassMethod ParseCSVFile(filePath As String)
{
	s fileContent = ..readFileContent(filePath)
	;b
	s contentList = ##class(%ListOfDataTypes).%New()
	for i=1:1: $ListLength(fileContent){
		;write !, $ListGet(fileContent, i)
		d contentList.Insert($ListGet(fileContent, i))
	}
	;b
	s resultList = ..ParseCSVString(contentList)	
	;b
	/**
	for i=1:1: resultList.Count() {
		s contentList = resultList.GetAt(i)
		for j=1:1: contentList.Count() {
			w " || "
			write contentList.GetAt(j)			
		}
		w !,"----------------------------",!
	}
	**/
	q resultList
}

/// w ##class(ext.util.String).readFileContent("C:\Users\HuangZhi\Desktop\temp\fileList.txt")
/// filePath 文件路径,需要放到服务器上,是服务器上的路径,
/// 返回的是数字,文件里面的每行内容是数组的元素
ClassMethod readFileContent(filePath)
{
	s ExistsFlag=##Class(%File).Exists(filePath)
	q:ExistsFlag'=1 "-1^Error: "_ filePath _"  does not exist !"
	Set file = ##class(%File).%New(filePath)
	Set file.Name = filePath
	Set sc = file.Open("WRS")
	if $$$ISERR(sc){
		do file.Close()
		set file=""
		Quit "-1^读取文件异常"_$system.Status.GetErrorText(sc)
	}
	set fileContent = $lb()
	set index = 1
	While('file.AtEnd){
		Set line = file.ReadLine()
		set $list(fileContent, index) = line
		set index = index + 1
	}
	q fileContent
}

/// 解析CSV字符
/// csv是逗号分割的,注意:字段中包含有逗号,有换行符,该字段必须用双引号括起来;字段中的双引号用两个双引号表示;以上提到的逗号和双引号均为半角字符。
/// lines 入参是读CSV文件的每一行,按换行符分割后的,注意一定是没有换行符的 集合;
/// 返回:每行数据分割后组成一个集合;所有行又放在返回的大集合里面。
/// w ##class(ext.util.String).ParseCSVString(contentList)
ClassMethod ParseCSVString(lines As %ListOfDataTypes)
{
	set oneLine = ##class(%ListOfDataTypes).%New()
	set resList = ##class(%ListOfObjects).%New()
	Q:'$d(lines) resList
	s tempStr = "" // 一个字段的值
	s specialChar = """" //转义字符
	s splitChar = "," // 分割符
	s specialFlag = 0 // 是否有需转义的字符:逗号;双引号;换行符;作为内容
	for i = 1 : 1 : lines.Count() {
	    s line =  lines.GetAt(i)
	    if '(line [ specialChar) {
            set oneLine = ##class(%ListOfDataTypes).%New()	            
            for j = 1 : 1 : $L(line) {
                if ($E(line, j) = splitChar) {
                    d oneLine.Insert(tempStr)
                    s tempStr = ""
                    continue
                } else {
                    s tempStr = tempStr _ $E(line, j)
                }
            }
            if '("" = tempStr) {
                d oneLine.Insert(tempStr)
                s tempStr = ""
            }
            d resList.Insert(oneLine)	            
	    } else {
            s:'specialFlag oneLine = ##class(%ListOfDataTypes).%New()
            for j = 1 : 1 : $L(line) {
                if ($E(line, j) = specialChar) {
                    if (specialFlag) {
                        if ($E(line, j + 1) = specialChar) {
                            s tempStr = tempStr _ specialChar
                        } else {
                            d oneLine.Insert(tempStr)
                            s tempStr = ""
                            s specialFlag = 0
                        }
                        s j = j + 1
                        continue	                        
                    } else {
                        s specialFlag = 1	
                        continue                        
                    }
                } elseif ($E(line, j) = splitChar) && ('specialFlag) {
                    d oneLine.Insert(tempStr)
                    s tempStr = ""
                    continue
                }
                s tempStr = tempStr _ $E(line, j)
            }
            if 'specialFlag {
                if '("" = tempStr) {
                    d oneLine.Insert(tempStr)
                    s tempStr = ""
                }
                d resList.Insert(oneLine)
            } else {
                s tempStr = tempStr _ $C(10,13) ;换行符 the line spacing characters
            }
	  }
	}
	Q resList
}



测试结果

在这里插入图片描述

最后使用解析,映射成类对象,保存到数据库。只提供m语言版本:


/// sql 查询,导出的CSV文件,再导入DB; 用于数据转移
/// filePath CSV数据文件
/// className 类名,表名
/// startColumnIndex 开始字段的列索引
/// d ##Class(ext.util.String).ImportCsvFile("C:\Users\HuangZhi\Desktop\data.csv", "websys.AddIns", 2)
ClassMethod ImportCsvFile(filePath, className, startColumnIndex = 1)
{
	s csvFileList = ##class(ext.util.String).ParseCSVFile(filePath)
	s propertyList = csvFileList.GetAt(1) // 第一行为类的字段名称的集合	
	;b
	for i=2:1: csvFileList.Count() {
		s oneLine = csvFileList.GetAt(i)
		continue:(('$D(oneLine)) || (oneLine.Count()=0))
		Set obj = $system.OBJ.New(className)
		for j=startColumnIndex:1: propertyList.Count() {
			set $PROPERTY(obj, propertyList.GetAt(j)) = oneLine.GetAt(j)					
		}
		s rtn = obj.%Save()	
		w:(rtn) !,"success: ",i	
		;b
	}
	w !,"total: ",csvFileList.Count()
}

这里,表是全量覆盖导入的情况,第一行是表字段名称(类的成员变量的名称)(变量都是基础数据类型,没有对象引用类)。不考虑主键冲突,覆盖更新等情况。如果是具体的某个类,要自己写解析数据映射,保存前的主键检查等等业务逻辑。这里只是一个很抽象的应用举例。



版权声明:本文为m0_51327179原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。