什么是简单的(仅限Scala)方式读入然后写出一个通过List [List [S
我刚刚收到了一堆CSV(逗号分隔值)格式的杂乱数据文件.我需要对数据集进行一些正常的清理,验证和过滤工作.我将在Scala(2.11.7)中进行清理.
在我寻找两个方向的解决方案,输入解析和输出组合时,我发现在输入解析方面,我发现了很多ill informed tangents,包括一个来自“Scala Cookbook”的一个.大多数人专注于非常错误的解决方案“使用String.split(”,“)”来获取CSV行作为List [String].我在作曲输出方面几乎找不到任何东西. 存在哪种简单的Scala代码片段可以轻松完成上述CSV往返?我想避免导入整个库只是为了获取这两个函数(目前我的业务需求使用Java库不是一个可接受的选项). 解决方法
我创建了特定的CSV相关函数,从中可以组成更通用的解决方案.
事实证明,由于逗号(,)和双引号(“)的异常,尝试解析CSV文件非常棘手.如果列值包含逗号或引号,则CSV的规则是整个值必须放在双引号中.如果值中出现任何双引号,则必须通过在现有双引号前面插入一个额外的双引号来转义每个双引号.这就是为什么经常引用StringOps的原因之一.split(“,”)方法根本不起作用,除非可以保证他们永远不会遇到使用逗号/双引号转义规则的文件.这是一个非常不合理的保证. 另外,请考虑有效逗号分隔符和单个双引号的开头之间可能存在字符.或者最终双引号与下一个逗号或行尾之间可以有字符.解决这个问题的规则是为了丢弃那些双引号范围之外的值.这是另一个原因,简单的StringOps.split(“,”)不仅是一个不充分的答案,而且实际上是不正确的. 关于我发现使用StringOps.split(“,”)发现的意外行为的最后一个注释.您知道此代码段中的结果有什么价值吗?: val result = ",".split(",") 如果您猜到“结果引用包含三个元素的Array [String],其中每个元素都是一个空字符串”,那么您将是不正确的.结果引用一个空数组[String].对我来说,一个空的Array [String]不是我期待或需要的答案.因此,对于所有神圣的爱,请请把最后的钉子放在StringOps.split(“,”)棺材里! 所以,让我们从已经读入的文件开始,该文件被呈现为List [String].下面的对象Parser是一个具有两个函数的通用解决方案; fromLine和fromLines.后一个函数fromLines是为方便起见而提供的,仅仅映射了前一个函数fromLine. object Parser { def fromLine(line: String): List[String] = { def recursive( lineRemaining: String,isWithinDoubleQuotes: Boolean,valueAccumulator: String,accumulator: List[String] ): List[String] = { if (lineRemaining.isEmpty) valueAccumulator :: accumulator else if (lineRemaining.head == '"') if (isWithinDoubleQuotes) if (lineRemaining.tail.nonEmpty && lineRemaining.tail.head == '"') //escaped double quote recursive(lineRemaining.drop(2),true,valueAccumulator + '"',accumulator) else //end of double quote pair (ignore whatever's between here and the next comma) recursive(lineRemaining.dropWhile(_ != ','),false,valueAccumulator,accumulator) else //start of a double quote pair (ignore whatever's in valueAccumulator) recursive(lineRemaining.drop(1),"",accumulator) else if (isWithinDoubleQuotes) //scan to next double quote recursive( lineRemaining.dropWhile(_ != '"'),valueAccumulator + lineRemaining.takeWhile(_ != '"'),accumulator ) else if (lineRemaining.head == ',') //advance to next field value recursive( lineRemaining.drop(1),valueAccumulator :: accumulator ) else //scan to next double quote or comma recursive( lineRemaining.dropWhile(char => (char != '"') && (char != ',')),valueAccumulator + lineRemaining.takeWhile(char => (char != '"') && (char != ',accumulator ) } if (line.nonEmpty) recursive(line,Nil).reverse else Nil } def fromLines(lines: List[String]): List[List[String]] = lines.map(fromLine) } 为了验证上述代码适用于所有各种奇怪的输入场景,需要创建一些测试用例.因此,使用Eclipse ScalaIDE工作表,我创建了一组简单的测试用例,可以直观地验证结果.这是工作表内容. val testRowsHardcoded: List[String] = { val superTrickyTestCase = { val dqx1 = '"' val dqx2 = dqx1.toString + dqx1.toString s"${dqx1}${dqx2}a${dqx2},${dqx2}1${dqx1},${dqx1}${dqx2}b${dqx2},${dqx2}2${dqx1},${dqx1}${dqx2}c${dqx2},${dqx2}3${dqx1}" } val nonTrickyTestCases = """,a,b,c a,c a,c "a,1","b,2","c,2" "a"",""1","b"",""2","c"",""2" "a"",""2" """.split("n").tail.toList (superTrickyTestCase :: nonTrickyTestCases.reverse).reverse } val parsedLines = Parser.fromLines(testRowsHardcoded) parsedLines.map(_.mkString("|")).mkString("n") 我在视觉上验证了测试是否正确完成并给我留下了分解的准确原始字符串.所以,我现在已经拥有了输入解析端所需的东西,所以我可以开始我的数据精炼. 在完成数据精炼之后,我需要能够编写输出,这样我就可以重新应用所有CSV编码规则来发送我的精炼数据. 所以,让我们从List [List [String]]开始作为改进的来源.下面的对象Composer是一个具有两个功能的通用解决方案; toLine和toLines.后一个函数toLines是为了方便而提供的,仅仅映射了前一个函数toLine. object Composer { def toLine(line: List[String]): String = { def encode(value: String): String = { if ((value.indexOf(',') < 0) && (value.indexOf('"') < 0)) //no commas or double quotes,so nothing to encode value else //found a comma or a double quote,// so double all the double quotes // and then surround the whole result with double quotes """ + value.replace(""","""") + """ } if (line.nonEmpty) line.map(encode(_)).mkString(",") else "" } def toLines(lines: List[List[String]]): List[String] = lines.map(toLine) } 为了验证上面的代码适用于所有各种奇怪的输入场景,我重用了我用于Parser的测试用例.再次,我在现有代码下面添加了一些代码,在那里我可以直观地验证结果.这是我添加的代码: val composedLines = Composer.toLines(parsedLines) composedLines.mkString("n") val parsedLines2 = Parser.fromLines(composedLines) parsedLines == parsedLines2 保存Scala工作表时,它会执行其内容.最后一行应显示“true”值.这是通过解析器,通过作曲家和通过解析器返回所有测试用例的结果. 顺便说一句,事实证明有大量的variation around the definition of a “CSV file”.所以,这是上面的代码强制执行的the source for the rules. PS.感谢@dhg指出它,有一个CSV Scala library处理解析CSV,以防万一你想要的东西可能更强大,并且有比我上面的Scala代码片段更多的选项. (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |