在特定的键上查找哈希数组中的重复项

8

我有一个哈希数组(实际上是CSV行),我需要查找并保留所有匹配两个特定键(用户、部分)的行。以下是数据示例:

[
  { user: 1, role: "staff", section: 123 },
  { user: 2, role: "staff", section: 456 },
  { user: 3, role: "staff", section: 123 },
  { user: 1, role: "exec", section: 123 },
  { user: 2, role: "exec", section: 456 },
  { user: 3, role: "staff", section: 789 }
]

所以我需要返回一个数组,其中只包含出现相同的用户/部分组合超过一次的行,如下所示:
[
  { user: 1, role: "staff", section: 123 },
  { user: 1, role: "exec", section: 123 },
  { user: 2, role: "staff", section: 456 },
  { user: 2, role: "exec", section: 456 }
]

我尝试的双重循环解决方案如下:
enrollments.each_with_index do |a, ai|
  enrollments.each_with_index do |b, bi|
    next if ai == bi

    duplicates << b if a[2] == b[2] && a[6] == b[6]
  end
end

但由于CSV有145K行,所以花费的时间非常漫长。

我如何更有效地获取所需的输出?


你使用什么来读取CSV文件?CSV.foreach应该逐行读取,这样有利于内存的消耗。但是无论如何,你都需要比较所有的行。你可以在内存中完成所有操作,也可以将其保存到数据库中并进行唯一性搜索。 - DiegoSalazar
我正在将CSV读入数组中。所以你建议在读取时进行比较?就像两个嵌套的CSV.foreach块? - lyonsinbeta
绝对不是两个CSV.foreach调用。请看答案。 - DiegoSalazar
2个回答

12

就效率而言,您可能希望尝试这种方法:

grouped = csv_arr.group_by{|row| [row[:user],row[:section]]}
filtered = grouped.values.select { |a| a.size > 1 }.flatten

第一条语句将记录按:user:section关键字分组。结果为:

{[1, 123]=>[{:user=>1, :role=>"staff", :section=>123}, {:user=>1, :role=>"exec", :section=>123}],
 [2, 456]=>[{:user=>2, :role=>"staff", :section=>456}, {:user=>2, :role=>"exec", :section=>456}],
 [3, 123]=>[{:user=>3, :role=>"staff", :section=>123}],
 [3, 789]=>[{:user=>3, :role=>"staff", :section=>789}]}

第二个语句仅选择拥有超过一个成员的群组的值,然后将结果展平以给出:

[{:user=>1, :role=>"staff", :section=>123},
 {:user=>1, :role=>"exec", :section=>123},
 {:user=>2, :role=>"staff", :section=>456},
 {:user=>2, :role=>"exec", :section=>456}]

这可能会提高您操作的速度,但是对于大输入的内存问题,我无法确定其影响,因为这取决于您的计算机、资源和文件大小。


这正好符合我的需求,但我刚刚发现提供给我的数据中有一些垃圾数据。下一个任务是找出如何清理它,以便我可以使用它。谢谢! - lyonsinbeta

0

要在内存中进行此检查,您不需要双重循环,可以保留一个唯一值的数组,并将每个新的 CSV 行与其进行比较:

found = []
unique_enrollments = []

CSV.foreach('/path/to/csv') do |row|
  # do whatever you're doing to parse this row into the hash you show in your question:
  # => { user: 1, role: "staff", section: 123 }
  # you might have to do `next if row.header_row?` if the first row is the header

  enrollment = parse_row_into_enrollment_hash(row)
  unique_tuple = [enrollment[:user], enrollment[:section]]

  unless found.include? unique_tuple
    found << unique_tuple
    unique_enrollments << enrollment
  end
end

现在你有了unique_enrollments。使用这种方法,您逐行解析CSV,因此不会将整个内容保存在内存中。然后构建一个由用户和部分组成的较小的唯一元组数组,用于进行唯一性检查,并构建唯一行的数组。

您可以通过不将unique_enrollments保存在大型数组中,而是构建模型并将其保存到数据库中来进一步优化:

unless found.include? unique_tuple
  found << unique_tuple
  Enrollment.create enrollment
end

通过上述调整,您将能够节省内存,而无需保留大量的注册表数组。尽管缺点是,如果出现问题,您将无法回滚。例如,如果我们之前保留了一个unique_enrollments数组,那么最后您可以执行以下操作:
Enrollment.transaction do
  unique_enrollments.each &:save!
end

现在,如果任何一个保存失败,您都可以回滚了。此外,将一堆数据库调用包装在单个 transaction 中会更快。我会选择这种方法。 编辑:使用unique_enrollments数组,您可以在最后遍历这些内容并创建一个新的CSV文件:
CSV.open('path/to/new/csv') do |csv|
  csv << ['user', 'role', 'staff'] # write the header

  unique_enrollments.each do |enrollment|
    csv << enrollment.values # just the values not the keys
  end
end

没有数据库。我只是试图从本地CSV中获取所需的数据。我还认为我的示例数据可能会引起一些混淆。数据是原封不动地传入,但我只比较某些字段,并希望将其保留原样。基本上是1.加载2.执行检查3.输出符合检查条件的行。 - lyonsinbeta
哦,那很好,只需保存所有唯一的行并使用它们写回到一个新的CSV文件中即可。 - DiegoSalazar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接