【问题标题】:Best way to work with large amounts of CSV data quickly快速处理大量 CSV 数据的最佳方式
【发布时间】:2011-07-30 06:05:09
【问题描述】:

我有需要处理的大型 CSV 数据集(超过 1000 万行)。我还有另外两个文件需要在输出中引用——它们包含的数据放大了我们对 CSV 文件中数百万行的了解。目标是输出一个新的 CSV 文件,其中每条记录都与其他文件的附加信息合并。

假设大型 CSV 文件有交易,但客户信息和账单信息记录在另外两个文件中,我们想要输出一个新的 CSV,其中每笔交易都与客户 ID 和帐户 ID 等相关联。

一位同事有一个用 Java 编写的函数式程序来执行此操作,但速度很慢。原因是具有数百万行的 CSV 文件显然必须遍历很多很多次。

我的问题是——是的,我明白了——我应该如何在 Ruby 中解决这个问题?目标是让它更快(现在 18 多个小时,CPU 活动很少)

我可以将这么多记录加载到内存中吗?如果是这样,我该怎么做?

我知道这有点含糊。只是寻找想法,因为这对我来说有点新。

【问题讨论】:

  • 您应该多写一些关于所需输出的内容。输入文件中的每一条记录是否必须只包含一条记录(行)(以及从辅助查找文件中匹配的一些新列)?为什么 CSV 必须“走很多次”?
  • 正如两个(到目前为止)答案所说,数据库是一个很好的解决方案。 Postgres 或 MySQL 可以轻松处理这个问题,包括批量加载文件。他们会非常快速地导入数据,在您当前看到的 18 小时内,一旦数据进入数据库,他们就能够即时创建您的输出记录。 10+百万条记录对数据库来说不算什么。 :-)

标签: ruby csv


【解决方案1】:

这是我为处理大型 csv 文件(在我的情况下约为 180mb)而编写的一些 ruby​​ 代码。

https://gist.github.com/1323865

一个标准的 FasterCSV.parse 将其全部放入内存需要一个多小时。这使它缩短到大约 10 分钟。

相关部分是这样的:

lines = []
IO.foreach('/tmp/zendesk_tickets.csv') do |line|
  lines << line
  if lines.size >= 1000
    lines = FasterCSV.parse(lines.join) rescue next
    store lines
    lines = []
  end
end
store lines

IO.foreach 不会将整个文件加载到内存中,而只是通过缓冲区逐步执行。当它达到 1000 行时,它会尝试解析 csv 并仅插入这些行。一个棘手的部分是“下一个救援”。如果您的 CSV 有一些跨越多行的字段,您可能需要再抓取几行以获得有效的可解析 csv 字符串。否则,您所在的行可能位于字段的中间。

在要点中,您可以看到另一个不错的优化,它使用 MySQL 的更新 ON DUPLICATE KEY。这允许您批量插入,如果检测到重复键,它只会覆盖该行中的值,而不是插入新行。您可以将其视为一个查询中的创建/更新。您需要在至少一列上设置唯一索引才能使其正常工作。

【讨论】:

  • 在 1G 文件上使用“IO.foreach”比使用“CSV.foreach”快 5 倍。
  • 最后一组
  • @holaSenor 最后的商店线也负责其余的组
  • 优秀的解决方案。我将它与 Rails 6 insert_all 和实体 id 的预取相结合,我也曾查找过用于使用外键构造记录,结果现在速度惊人。 :)
【解决方案2】:

我的经验是,使用 Ruby 时,准备好使实际有效负载的内存使用量大约是 10 倍。当然,以当前的 RAM 量,如果进程一次只加载一个文件,即使乘以 10,10MB 也几乎可以忽略不计 :)

如果您可以一次读取一行(这对于 File 实例很容易),您可以使用 FasterCSV 并一次写入一行。这将使内存消耗O(1) 而不是O(n)。但是对于 10 兆字节的文件,您可能可以将该文件吸到内存中,然后一次性将其写入 CSV,在任何给定时间只考虑很少的进程。

【讨论】:

  • 一次性吞下 10M+ 行的问题与其说是使用可用 RAM,不如说是可扩展性问题,而是读取数据时的内存分配问题。早在我在这里做一些基准测试时,展示了如何快速读取大文件,然后逐行处理它们。找到基准现在将是有趣的部分。
  • 啊,找到了。它是Ruby: start reading at arbitrary point in large file 的一部分。我的解释是 Ruby 读取设定的大小,然后发现它需要越来越多的 RAM,因此它必须重新获取内存并移动变量。通过预先定义要读取多少,它会预先获取它需要的所有内容,然后将传入的数据直接流式传输到缓冲区中。哦,“10 MB 文件”,OP 说它们是 10M+ lines 而不是字节。
  • 是的,记住你可以通过说 a = Array.new(n) # 立即分配 n 个槽来预先分配大小
  • 如果将数据作为数组读取,这将有所帮助。 Read 不会这样做,它只会将其作为字符串拉入。后续处理可以拆分它,但通常迭代 String#each_line 就足够了,并且避免了额外的数据复制。
【解决方案3】:

两个相当快的选择:

  1. 将您的数据放入 sqlite DB。然后它是一个简单的查询,带有一对join,它的执行速度比你自己编写的任何东西都要快——SQL 非常适合这种任务。

  2. 假设您的附加 CSV 文件小到足以放入 RAM,您可以使用客户 ID 作为键将所有内容读入哈希,然后在处理具有 10+M 条记录的主文件时查找该哈希。注意只需要将查找数据放入RAM,主列表可以在小分支中处理。

【讨论】:

    【解决方案4】:

    10M+ 行听起来并不像 那样。如果您可以预加载文件的内容并将内存中的数据与合适的数据结构相匹配(在某些时候您需要地图),您就不必一遍又一遍地运行 CSV 文件。文件访问

    【讨论】:

    • 这就是我的想法。你会使用什么样的结构?
    • @NJ:不知道你的算法是什么,我什至无法猜测。
    【解决方案5】:

    如果您编写了 Java 程序,请确保使用 NIO 库。它们比默认值快得多。我之前使用 NIO 库处理过 500,000 行的文本文件。

    【讨论】:

      【解决方案6】:

      如何使用数据库。

      将记录塞进表中,然后使用连接查询出来。

      导入可能需要一段时间,但数据库引擎将针对连接和检索部分进行优化...

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-21
        • 2018-03-23
        • 2011-09-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多