谁杀了我的同类?或如何有效地计算 csv 列中的不同值

wvi*_*ana 14 pipe python sort uniq

我正在做一些处理,试图获取包含 160,353,104 行的文件中有多少不同的行。这是我的管道和 stderr 输出。

\n
$ tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 |\\\n  sort -T. -S1G | tqdm --total=160353104 | uniq -c | sort -hr > users\n\n100%|\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88| 160353104/160353104 [0:15:00<00:00, 178051.54it/s]\n 79%|\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88      | 126822838/160353104 [1:16:28<20:13, 027636.40it/s]\n\nzsh: done tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | \nzsh: killed sort -T. -S1G | \nzsh: done tqdm --total=160353104 | uniq -c | sort -hr > users\n
Run Code Online (Sandbox Code Playgroud)\n

我的命令行 PS1 或 PS2 打印了管道所有进程的返回码。\xe2\x9c\x94 0|0|0|KILL|0|0|0第一个字符是绿色复选标记,表示最后一个进程返回 0(成功)。其他数字是每个管道进程的返回代码,顺序相同。所以我注意到我的第四个命令得到了KILL状态,这是我的排序命令,sort -T. -S1G将本地目录设置为临时存储并缓冲高达 1GiB。

\n

问题是,为什么它返回了 KILL,这是否意味着KILL SIGN向它发送了一些东西?\n有没有办法知道“谁杀了”它?

\n

更新

\n

阅读Marcus M\xc3\xbcller Answer后,首先我尝试将数据加载到 Sqlite 中。

\n
\n

因此,也许现在是告诉您的好时机,不,不要使用基于 CSV 的数据流。一个简单的

\n
sqlite3 place.sqlite\n
Run Code Online (Sandbox Code Playgroud)\n

并在该 shell 中(假设您的 CSV 有一个标题行,SQLite 可以使用该标题行来确定列)(当然,将 $second_column_name\n 替换为该列的名称)

\n
.import 022_place_canvas_history.csv canvas_history --csv\nSELECT $second_column_name, count($second_column_name)   FROM canvas_history \nGROUP BY $second_column_name;\n
Run Code Online (Sandbox Code Playgroud)\n
\n

这花了很多时间,所以我把它留在处理中并去做其他事情。与此同时,我更多地思考了Marcus M\xc3\xbcller 回答中的另一段:

\n
\n

您只想知道每个值在第二列中出现的频率。之所以会发生这种情况,是因为您的工具(uniq -c)很糟糕,并且需要在之前对行进行排序(实际上没有充分的理由。它只是没有实现它可以保存值及其值的映射)频率并在出现时增加频率)。

\n
\n

所以我想,我可以实现它。当我回到计算机时,我的 Sqlite 导入过程已停止,原因是 SSH Broken Pip,认为它很长时间没有传输数据,因此关闭了连接。\n好吧,这是一个使用计数器实现计数器的好机会字典/地图/哈希表。所以我写了以下distinct文件:

\n
#!/usr/bin/env python3\nimport sys\n\nconter = dict()\n\n# Create a key for each distinct line and increment according it shows up. \nfor l in sys.stdin:\n    conter[l] = conter.setdefault(l, 0) + 1 # After Update2 note: don\'t do this, do just `couter[l] = conter.get(l, 0) + 1`\n\n# Print entries sorting by tuple second item ( value ), in reverse order\nfor e in sorted(conter.items(), key=lambda i: i[1], reverse=True):\n    k, v = e\n    print(f\'{v}\\t{k}\')\n
Run Code Online (Sandbox Code Playgroud)\n

所以我通过以下命令管道使用了它。

\n
#!/usr/bin/env python3\nimport sys\n\nconter = dict()\n\n# Create a key for each distinct line and increment according it shows up. \nfor l in sys.stdin:\n    conter[l] = conter.setdefault(l, 0) + 1 # After Update2 note: don\'t do this, do just `couter[l] = conter.get(l, 0) + 1`\n\n# Print entries sorting by tuple second item ( value ), in reverse order\nfor e in sorted(conter.items(), key=lambda i: i[1], reverse=True):\n    k, v = e\n    print(f\'{v}\\t{k}\')\n
Run Code Online (Sandbox Code Playgroud)\n

它进展得非常非常快,预计tqdm不到 30 分钟,但当进入 99% 时,它变得越来越慢。此过程使用了大量 RAM,大约 1.7GIB。我正在处理这些数据的机器,我有足够存储空间的机器,是一个只有 2GiB RAM 和 ~1TiB 存储空间的 VPS。我认为它可能变得如此缓慢,因为必须处理这些巨大的内存,也许要做一些交换或其他事情。 \n无论如何,我一直在等待,当它最终在 tqdm 中达到 100% 时,所有数据都已发送到进程中./distinct,几秒钟后得到以下输出:

\n
tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | ./distinct > users2\n
Run Code Online (Sandbox Code Playgroud)\n

这次大部分肯定是由内存不足杀手造成的,如Marcus M\xc3\xbcller 答案TLDR 部分中所发现的那样。

\n

所以我刚刚检查过,我没有在这台机器上启用交换。使用 dmcrypt 和 LVM 完成设置后将其禁用,因为您可能会在我的这个答案中获得更多信息。

\n

所以我的想法是启用我的 LVM 交换分区并尝试再次运行它。另外,在某个时刻,我认为我已经看到 tqdm 使用 10GiB RAM。但我很确定我看到了错误或btop输出混淆了,因为后者仅显示 10MiB,不认为 tqdm 会使用太多内存,因为它只是在读取新的\\n.

\n

在 St\xc3\xa9phane Chazelas 对这个问题的评论中,他们说:

\n
\n

系统日志可能会告诉你。

\n
\n

我想了解更多,我应该在journalctl中找到一些东西吗?如果可以的话,该怎么办呢?

\n

无论如何,正如Marcus M\xc3\xbcller Answer所说,将 csv 加载到 Sqlite 中可能是迄今为止最智能的解决方案,因为它将允许以多种方式对数据进行操作,并且可能有一些智能的方法来导入此数据而无需输出 -内存。

\n

但现在我对如何找出进程被杀死的原因感到好奇,因为我想了解 mysort -T. -S1G和 now about my ./distinct,最后一个几乎可以肯定它与内存有关。那么如何检查日志来说明为什么这些进程被杀死呢?

\n

更新2

\n

因此,我启用了交换分区,并从此问题评论中采纳了Marcus M\xc3\xbcller的建议。使用 python 集合.Counter。所以我的新代码 ( distinct2) 如下所示:

\n
#!/usr/bin/env python3\nfrom collections import Counter\nimport sys\n\nprint(Counter(sys.stdin).most_common())\n
Run Code Online (Sandbox Code Playgroud)\n

因此,我运行了Gnu Screen,即使管道再次损坏,我也可以恢复会话,而不是在以下管道中运行它:

\n
160353105it [30:21, 88056.97it/s]                                                                                            \nzsh: done       tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | \nzsh: killed     ./distinct > users2\n
Run Code Online (Sandbox Code Playgroud)\n

这让我得到了以下输出:

\n
160Mit [1:07:24, 39.6kit/s]\n1.00it [7:08:56, 25.7ks/it]\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,对数据进行排序比对数据进行计数花费了更多的时间。\n您可能注意到的另一件事是 tqdm 第二行输出仅显示 1.00it,这意味着它只有一行。所以我使用 head 检查了 user5 文件:

\n
#!/usr/bin/env python3\nfrom collections import Counter\nimport sys\n\nprint(Counter(sys.stdin).most_common())\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,它在一行中打印了整个元组列表。为了解决这个问题,我使用了很好的旧 sed ,如下所示sed \'s/),/)\\n/g\' users5 > users6。之后,我使用 head 检查了 users6 内容,其输出如下:

\n
tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 --unit-scale=1 | ./distinct2 | tqdm --unit-scale=1 > users5\n
Run Code Online (Sandbox Code Playgroud)\n

足够好,可以稍后工作。现在我想我应该在尝试使用 dmesg 或 Journalctl检查谁杀死了我的类型后添加更新。我还想知道是否有办法使这个脚本更快。也许创建一个线程池,但必须检查Python的字典行为,还考虑其他数据结构,因为我正在计算的列是固定宽度的字符串,也许使用列表来存储每个不同user_hash的频率。我还阅读了 Counter 的 python 实现,它只是一个字典,与我之前的实现几乎相同,但不是使用dict.setdefaultjustused dict[key] = dict.get(key, 0) + 1,而是在这种情况下没有真正需要的误用setdefault

\n

更新3

\n

所以我已经陷入了兔子洞的深渊,完全失去了目标的焦点。我开始寻找更快的排序,也许写一些 C 或 Rust,但意识到已经处理了我要处理的数据。所以我在这里展示 dmesg 输出以及关于 python 脚本的最后一个提示。提示是:使用 dict 或 Counter 进行计数可能比使用 gnu 排序工具对其输出进行排序更好。排序可能比 python 排序 buitin 函数更快。

\n

关于 dmesg,查找内存不足非常简单,只需按一下即可sudo dmesg | less一直G向下,而不是?向后搜索,而不是搜索Out字符串。找到了其中两个,一个用于我的 python 脚本,另一个用于我的排序,即引发此问题的那个。这是这些输出:

\n
[1306799.058724] Out of memory: Killed process 1611241 (sort) total-vm:1131024kB, anon-rss:1049016kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:2120kB oom_score_adj:0\n[1306799.126218] oom_reaper: reaped process 1611241 (sort), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB\n[1365682.908896] Out of memory: Killed process 1611945 (python3) total-vm:1965788kB, anon-rss:1859264kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3748kB oom_score_adj:0\n[1365683.113366] oom_reaper: reaped process 1611945 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB\n
Run Code Online (Sandbox Code Playgroud)\n

就是这样,非常感谢您到目前为止的帮助,希望它也能帮助其他人。

\n

Mar*_*ler 30

TL;DR:内存不足杀手或磁盘空间不足用于临时文件杀死sort。建议:使用不同的工具。

\n
\n

sort.c我现在已经浏览了 GNU coreutils \xc2\xb9。您的-S 1G意思只是意味着该sort进程尝试分配 1GB 的内存块,如果不可能的话,将回退到越来越小的大小。

\n

耗尽该缓冲区后,它将创建一个临时文件来存储已排序的行\xc2\xb2,并对内存中的下一个输入块进行排序。

\n

消耗完所有输入后,sort会将两个临时文件合并/排序为一个临时文件(mergesort-style),并连续合并所有临时文件,直到合并产生总排序输出,然后将其输出到stdout.

\n

这很聪明,因为这意味着您可以对大于可用内存的输入进行排序。

\n

或者,在这些临时文件本身不保存在 RAM 中的系统上(通常是/tmp/a tmpfs,仅 RAM 文件系统),这很聪明。因此,写入这些临时文件会占用您想要保存的 RAM,并且您的 RAM 即将耗尽:您的文件有 1.6 亿行,快速谷歌搜索后会显示它是 11GB 的未压缩数据。

\n

sort您可以通过更改它使用的临时目录来“帮助”解决这个问题。您已经这样做了,-T.将临时文件放置在当前目录中。可能你那里的空间不够了?或者当前目录是否在tmpfs或类似?

\n

您有一个包含中等数据量的 CSV 文件(对于现代 PC 来说, 1.6 亿行并不是那么多数据)。您不是将其放入旨在处理大量数据的系统中,而是尝试使用 20 世纪 90 年代的工具(是的,我刚刚读过sortgit 历史)对其进行操作,当时 16 MB RAM 似乎相当慷慨。

\n

CSV 是处理任何大量数据的错误数据格式,您的示例完美地说明了这一点。低效的工具以低效的方式处理低效的数据结构(带有行的文本文件),以低效的方式实现目标:

\n

您只想知道每个值在第二列中出现的频率。之所以会发生这种情况,是因为您的工具(uniq -c)很糟糕,并且需要在之前对行进行排序(实际上没有充分的理由。它只是没有实现它可以保存值及其值的映射)频率并在出现时增加频率)。

\n
\n

因此,也许现在是告诉您的好时机,不,不要使用基于 CSV 的数据流。一个简单的

\n
sqlite3 place.sqlite\n
Run Code Online (Sandbox Code Playgroud)\n

并在该 shell 中(假设您的 CSV 有一个标题行,SQLite 可以使用该标题行来确定列)(当然,替换$second_column_name为该列的名称)

\n
.import 022_place_canvas_history.csv canvas_history --csv\nSELECT $second_column_name, count($second_column_name)\n  FROM canvas_history\n  GROUP BY $second_column_name;\n
Run Code Online (Sandbox Code Playgroud)\n

可能会一样快,而且额外的好处是,您会得到一个实际的数据库文件place.sqlite。例如,您可以更灵活地使用 \xe2\x80\x93,创建一个在其中提取坐标的表,并将时间转换为数字时间戳,然后通过分析更快、更灵活。

\n
\n

\xc2\xb9 全局变量,以及何时使用的不一致。他们受伤了。对于 C 语言作者来说,这是一个不同的时代。它绝对不是糟糕的 C,只是……不是您在更现代的代码库中所习惯的。感谢 Jim Meyering 和 Paul Eggert 编写和维护此代码库!

\n

\xc2\xb2 你可以尝试执行以下操作:对一个不太大的文件进行排序,比如说ls.c有 5577 行,并记录打开的文件数:

\n
strace -o /tmp/no-size.strace -e openat sort ls.c\nstrace -o /tmp/s1kB-size.strace -e openat sort -S 1 ls.c\nstrace -o /tmp/s100kB-size.strace -e openat sort -S 100 ls.c\nwc -l /tmp/*-size.strace\n
Run Code Online (Sandbox Code Playgroud)\n

  • 但我在第一句话中就告诉了你是什么杀死了你的进程:它是内存不足的杀手。其他事情不会随机杀死进程。这就是我发现的:消除其他选择和经验。(您的问题下的注释也表明您需要阅读系统日志“dmesg”和/或“journalctl -xe”。) (6认同)