需求背景
从一个数据库导出一个表的数据,导出文件为CSV文件;需要将数据导入到本地开发环境的数据库里面。CSV文件已经有了,需要解析读CSV文件,并导入进数据库。本文主要研究第一部分,自己写代码解析CSV文件。
Java中可以找到Jar包,工具类,直接使用。也不用自己手工写解析。如果有更成熟的工具,建议直接使用。本文作者写的,只有自己测试过,如果有bug,欢迎留言,不一定及时回。
CSV文件规则
以纯文本形式存储表格数据(数字和文本)。
记录间以换行符分隔;每条记录由字段组成,字段间的分隔符是逗号。
注意:
1、字段中包含有逗号,有换行符,该字段必须用双引号括起来;
2、字段中的双引号用两个双引号表示;
以上提到的逗号和双引号均为半角字符。
如果只是简单的逗号分割,那么就很简单。本文主要研究的就是,有特殊字符作为字段的值,上面注意中的情况出现的情况下,文件的解析。
CSV规则有遗漏的,或者逻辑不完善,能导致歧义的数据案例,评论指出,感谢!
准备测试数据
可以新建一个test.csv文件,用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()
}
这里,表是全量覆盖导入的情况,第一行是表字段名称(类的成员变量的名称)(变量都是基础数据类型,没有对象引用类)。不考虑主键冲突,覆盖更新等情况。如果是具体的某个类,要自己写解析数据映射,保存前的主键检查等等业务逻辑。这里只是一个很抽象的应用举例。