gpt4 book ai didi

scala - 读入然后写出一个通过 List[List[String]] 的小 .csv 文件的简单(仅限 Scala)方法是什么?

转载 作者:行者123 更新时间:2023-12-04 14:26:08 28 4
gpt4 key购买 nike

我刚刚收到一堆 CSV(逗号分隔值)格式的杂乱数据文件。我需要对数据集进行一些正常的清理、验证和过滤工作。我将在 Scala (2.11.7) 中进行清理。

在我寻找两个方向的解决方案,输入解析和输出组合的过程中,我发现了很多 ill informed tangents ,包括来自“Scala Cookbook”的一个,在输入解析端。大多数人都专注于非常错误的解决方案“使用 String.split(",")”将 CSV 行作为 List[String] 返回。 .我在合成输出方面几乎没有发现任何东西。

什么样的好简单的 Scala 代码片段 存在哪个可以轻松完成上述CSV往返? 我想避免为了获取这两个函数而导入整个库(此时使用 Java 库对于我的业务需求来说是 Not Acceptable 选择)。

最佳答案

UPDATE 2020/08/30: 请使用 Scala 库 kantan.csv ,以获得最准确和正确的 RFC 4180 实现,它定义了 .csv MIME-type.
虽然我很享受我在创建以下解决方案时所经历的学习过程,但请不要使用它,因为我发现它存在许多问题,尤其是在规模上。为了避免我下面的解决方案产生明显的技术债务,选择一个维护良好的 RFC 驱动的 Scala 原生解决方案应该是您照顾当前和 future 客户的方式。

我创建了特定的 CSV 相关函数,从中可以组成更通用的解决方案。
事实证明,由于逗号 (,) 和双引号 (") 周围的异常,尝试解析 CSV 文件非常棘手。CSV 的规则是,如果列值包含逗号或引号,则整个值必须放在双引号中。如果值中出现任何双引号,则必须通过在现有双引号前面插入额外的双引号来转义每个双引号。这就是为什么经常引用 StringOps.split(",") 的原因之一除非可以保证他们永远不会遇到使用逗号/双引号转义规则的文件,否则方法根本不起作用。这是一种非常不合理的保证。
此外,请考虑在有效的逗号分隔符和单双引号的开头之间可能存在字符。或者在最后一个双引号和下一个逗号或行尾之间可以有字符。解决这个问题的规则是丢弃那些超出双引号范围的值。这是另一个原因,简单的 StringOps.split(",") 不仅答案不充分,而且实际上是不正确的。

关于我使用 StringOps.split(",") 发现的意外行为的最后一个说明。你知道这个代码片段中有什么值 result 吗?:

val result = ",,".split(",")
如果您猜测“ result 引用了一个包含三个元素的 Array[String] ,其中每个元素都是空的 String ”,那么您就错了。 result 引用了一个空的 Array[String] 。对我来说,空的 Array[String] 不是我期待或需要的答案。所以,为了所有圣洁的爱,请把最后一颗钉子钉在 StringOps.split(",") 棺材里!

因此,让我们从已读入的文件开始,该文件显示为 List[String]object Parser下面是一个有两个功能的通用解决方案; fromLinefromLines 。提供后一个函数 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), true, "", accumulator)
else
if (isWithinDoubleQuotes)
//scan to next double quote
recursive(
lineRemaining.dropWhile(_ != '"')
, true
, valueAccumulator + lineRemaining.takeWhile(_ != '"')
, accumulator
)
else
if (lineRemaining.head == ',')
//advance to next field value
recursive(
lineRemaining.drop(1)
, false
, ""
, valueAccumulator :: accumulator
)
else
//scan to next double quote or comma
recursive(
lineRemaining.dropWhile(char => (char != '"') && (char != ','))
, false
, valueAccumulator + lineRemaining.takeWhile(char => (char != '"') && (char != ','))
, accumulator
)
}
if (line.nonEmpty)
recursive(line, false, "", Nil).reverse
else
Nil
}

def fromLines(lines: List[String]): List[List[String]] =
lines.map(fromLine)
}
为了验证上述代码适用于所有各种奇怪的输入场景,需要创建一些测试用例。因此,使用 Eclipse ScalaIDE Worksheet,我创建了一组简单的测试用例,我可以在其中直观地验证结果。这是工作表的内容。
  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,,b,,c
a, b, c
a ,b ,c
a , b , c
"a,1","b,2","c,2"
"a"",""1","b"",""2","c"",""2"
"a"" , ""1" , "b"" , ""2" , "c"",""2"
""".split("\n").tail.toList
(superTrickyTestCase :: nonTrickyTestCases.reverse).reverse
}
val parsedLines =
Parser.fromLines(testRowsHardcoded)
parsedLines.map(_.mkString("|")).mkString("\n")
我目视验证了正确完成的测试,并为我留下了分解的准确原始字符串。所以,我现在有了输入解析端所需的东西,这样我就可以开始我的数据提炼了。
数据精炼完成后,我需要能够组合输出,以便我可以将精炼的数据发回,重新应用所有 CSV 编码规则。
因此,让我们从 List[List[String]] 作为改进的来源开始。 object Composer下面是一个有两个功能的通用解决方案; toLinetoLines 。提供后一个函数 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)
}
为了验证上述代码适用于所有各种奇怪的输入场景,我重用了用于解析器的测试用例。同样,使用 Eclipse ScalaIDE 工作表,我在现有代码下方添加了更多代码,我可以在其中直观地验证结果。这是我添加的代码:
val composedLines =
Composer.toLines(parsedLines)
composedLines.mkString("\n")
val parsedLines2 =
Parser.fromLines(composedLines)
parsedLines == parsedLines2
保存 Scala WorkSheet 后,它会执行其内容。最后一行应显示“true”值。它是将所有测试用例通过解析器、通过 Composer 并通过解析器返回的结果。
顺便说一句,事实证明有很多 variation around the definition of a "CSV file" 。所以,这是上面代码强制执行的 the source for the rules
附注。感谢@dhg 指出,有一个 CSV Scala library 处理解析 CSV,以防万一你想要一些比我上面的 Scala 代码片段更健壮并且有更多选项的东西。

关于scala - 读入然后写出一个通过 List[List[String]] 的小 .csv 文件的简单(仅限 Scala)方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32488364/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com