Python

誰殺了我的同類?或如何有效計算 csv 列中的不同值

  • April 15, 2022

我正在做一些處理,試圖獲取包含 160,353,104 行的文件中有多少不同的行。這是我的管道和標準錯誤輸出。

$ tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 |\
 sort -T. -S1G | tqdm --total=160353104 | uniq -c | sort -hr > users

100%|████████████████████████████| 160353104/160353104 [0:15:00<00:00, 178051.54it/s]
79%|██████████████████████      | 126822838/160353104 [1:16:28<20:13, 027636.40it/s]

zsh: done tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | 
zsh: killed sort -T. -S1G | 
zsh: done tqdm --total=160353104 | uniq -c | sort -hr > users

我的命令行 PS1 或 PS2 列印了管道所有程序的返回碼。✔ 0|0|0|KILL|0|0|0第一個字元是一個綠色複選標記,表示最後一個程序返回 0(成功)。其他數字是每個流水線程序的返回碼,順序相同。所以我注意到我的第四個命令得到了KILL狀態,這是我的排序命令,sort -T. -S1G將本地目錄設置為臨時儲存並緩衝高達 1GiB。

問題是,為什麼它返回了 KILL,是不是意味著有東西發給KILL SIGN它了?有沒有辦法知道“誰殺了”它?

更新

在閱讀Marcus Müller Answer之後,首先我嘗試將數據載入到 Sqlite 中。

所以,也許現在是告訴您,不,不要使用基於 CSV 的數據流的好時機。一個簡單的

sqlite3 place.sqlite

在那個 shell 中(假設你的 CSV 有一個 SQLite 可以用來確定列的標題行)(當然,用該列的名稱替換 $second_column_name)

.import 022_place_canvas_history.csv canvas_history --csv
SELECT $second_column_name, count($second_column_name)   FROM canvas_history 
GROUP BY $second_column_name;

這需要很多時間,所以我讓它處理並去做其他事情。雖然我更多地考慮了Marcus Müller 的另一段答案

您只想知道每個值出現在第二列的頻率。因為你的工具uniq -c(出現)。

所以我想,我可以實現它。當我回到電腦時,我的 Sqlite 導入過程已經停止導致 SSH Broken Pip 的原因,認為它很長時間沒有傳輸數據它關閉了連接。好的,這是一個使用 dict/map/hashtable 實現計數器的好機會。所以我寫了以下distinct文件:

#!/usr/bin/env python3
import sys

conter = dict()

# Create a key for each distinct line and increment according it shows up. 
for l in sys.stdin:
   conter[l] = conter.setdefault(l, 0) + 1 # After Update2 note: don't do this, do just `couter[l] = conter.get(l, 0) + 1`

# Print entries sorting by tuple second item ( value ), in reverse order
for e in sorted(conter.items(), key=lambda i: i[1], reverse=True):
   k, v = e
   print(f'{v}\t{k}')

所以我已經通過以下命令管道使用它。

tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | ./distinct > users2

它執行得非常非常快,預計tqdm不到 30 分鐘,但當達到 99% 時,它變得越來越慢。這個過程使用了大量的 RAM,大約 1.7GIB。我正在處理這些數據的機器,我有足夠儲存空間的機器,是一個只有 2GiB RAM 和 ~1TiB 儲存空間的 VPS。認為它可能變得如此緩慢,因為 SO 不得不處理這些巨大的記憶體,也許做一些交換或其他事情。反正我已經等了,當它終於在 tqdm 中達到 100% 時,所有數據都被發送到./distinct程序中,幾秒鐘後得到以下輸出:

160353105it [30:21, 88056.97it/s]                                                                                            
zsh: done       tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | 
zsh: killed     ./distinct > users2

這一次大部分肯定是由Marcus Müller Answer TLDR 部分中發現的記憶體不足殺手造成的。

所以我剛剛檢查過,我沒有在這台機器上啟用交換。在使用 dmcrypt 和 LVM 完成設置後禁用它,因為您可能會在我的這個答案中獲得更多資訊。

所以我在想的是啟用我的 LVM 交換分區並嘗試再次執行它。同樣在某個時刻,我認為我已經看到 tqdm 使用 10GiB 的 RAM。但我很確定我看錯了或btop輸出混淆了,因為後者只顯示 10MiB,不要認為 tqdm 會使用太多記憶體,因為它只是在讀取新的\n.

在 Stéphane Chazelas 對這個問題的評論中,他們說:

系統日誌可能會告訴你。

我想知道更多關於它的資訊,我應該在 journalctl 中找到一些東西嗎?如果是這樣,該怎麼做?

無論如何,正如Marcus Müller Answer所說,將 csv 載入到 Sqlite 可能是迄今為止最聰明的解決方案,因為它允許以多種方式對數據進行操作,並且可能有一些聰明的方法來導入這些數據而不會出現記憶體不足。

但是現在我對如何找出程序被殺死的原因有兩次好奇,因為我想知道 mysort -T. -S1G和現在 my ./distinct,最後一個幾乎可以肯定它是關於記憶體的。那麼如何檢查說明為什麼這些程序被殺死的日誌呢?

更新2

所以我啟用了我的 SWAP 分區並從這個問題評論中接受了Marcus Müller的建議。使用 pythons collections.Counter。所以我的新程式碼 ( distinct2) 看起來像這樣:

#!/usr/bin/env python3
from collections import Counter
import sys

print(Counter(sys.stdin).most_common())

所以我已經執行了Gnu Screen,即使我再次遇到損壞的管道,我也可以恢復會話,而不是在以下管道中執行它:

tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 --unit-scale=1 | ./distinct2 | tqdm --unit-scale=1 > users5

這讓我得到了以下輸出:

160Mit [1:07:24, 39.6kit/s]
1.00it [7:08:56, 25.7ks/it]

如您所見,對數據進行排序比對數據進行計數要花費更多的時間。您可能注意到的另一件事是 tqdm 第二行輸出僅顯示 1.00it,這意味著它只有一行。所以我使用 head 檢查了 user5 文件:

head -c 150 users5 
[('kgZoJz//JpfXgowLxOhcQlFYOCm8m6upa6Rpltcc63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n', 795), ('JMlte6XKe+nnFvxcjT0hHDYYNgiDXZVOkhr6KT60EtJAGa

如您所見,它將整個元組列表列印在一行中。為了解決這個問題,我使用了好的舊 sed 如下sed 's/),/)\n/g' users5 > users6。之後,我使用 head 檢查了 users6 的內容,其輸出如下:

$ head users6
[('kgZoJz/...c63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n', 795)
('JMlte6X...0EtJAGaezxc4e/eah6JzTReWNdTH4fLueQ20A4drmfqbqsw==\n', 781)
('LNbGhj4...apR9YeabE3sAd3Rz1MbLFT5k14j0+grrVgqYO1/6BA/jBfQ==\n', 777)
('K54RRTU...NlENRfUyJTPJKBC47N/s2eh4iNdAKMKxa3gvL2XFqCc9AqQ==\n', 767)
('8USqGo1...1QSbQHE5GFdC2mIK/pMEC/qF1FQH912SDim3ptEFkYPrYMQ==\n', 767)
('DspItMb...abcd8Z1nYWWzGaFSj7UtRC0W75P7JfJ3W+4ne36EiBuo2YQ==\n', 766)
('6QK00ig...abcfLKMUNur4cedRmY9wX4vL6bBoV/JW/Gn6TRRZAJimeLw==\n', 765)
('VenbgVz...khkTwy/w5C6jodImdPn6bM8izTHI66HK17D4Bom33ZrwuGQ==\n', 758)
('jjtKU98...Ias+PeaHE9vWC4g7p2KJKLBdjKvo+699EgRouCbeFjWsjKA==\n', 730)
('VHg2OiSk...3c3cr2K8+0RW4ILyT1Bmot0bU3bOJyHRPW/w60Y5so4F1g==\n', 713)

足夠好以後工作。現在我想我應該在嘗試使用 dmesg ou journalctl檢查*誰殺死了我的排序之後添加更新。*我也想知道是否有辦法讓這個腳本更快。也許創建一個執行緒池,但必須檢查 python 的 dict 行為,還考慮了其他資料結構,因為我正在計算的列是固定寬度的字元串,也許使用列表來儲存每個不同 user_hash 的頻率。我還閱讀了 Counter 的 python 實現,它只是一個 dict,與我之前的實現幾乎相同,但不是使用dict.setdefaultjust used ,而是在這種情況下沒有真正需要dict[key] = dict.get(key, 0) + 1的誤用。setdefault

更新3

所以我在兔子洞裡越來越深,完全失去了我的目標。我開始尋找更快的排序,可能會寫一些 C 或 Rust,但意識到已經處理了我來處理的數據。所以我在這裡展示 dmesg 輸出和關於 python 腳本的最後一個提示。提示是:使用 dict 或 Counter 進行計數可能比使用 gnu 排序工具對其輸出進行排序更好。排序排序可能比 python 排序的 buitin 函式快。

關於dmesg,查找記憶體不足很簡單,只需sudo dmesg | lessG一下一直向下,而不是?向後搜尋,而不是搜尋Out字元串。找到了其中兩個,一個用於我的 python 腳本,另一個用於我的排序,一個開始這個問題。這是那些輸出:

[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
[1306799.126218] oom_reaper: reaped process 1611241 (sort), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
[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
[1365683.113366] oom_reaper: reaped process 1611945 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

就是這樣,非常感謝您到目前為止的幫助,希望它也能幫助其他人。

TL;DR: out-of-memory-killer 或臨時文件的磁碟空間不足殺死sort。建議:使用不同的工具。


我現在瀏覽了 GNU coreutils sort.c¹。您-S 1G只是意味著該sort程序嘗試以 1GB 的塊分配記憶體,如果不可能,將回退到越來越小的大小。

在用完該緩衝區後,它將創建一個臨時文件來儲存已排序的行²,並對記憶體中的下一個輸入塊進行排序。

消耗完所有輸入後,sort會將兩個臨時文件合併/排序為一個臨時文件(mergesort-style),並依次合併所有臨時文件,直到合併產生總排序輸出,然後輸出到stdout.

這很聰明,因為這意味著您可以對大於可用記憶體的輸入進行排序。

或者,在這些臨時文件本身不保存在 RAM 中的系統上,它們通常是這些天(/tmp/通常是 a tmpfs,這是一個僅 RAM 的文件系統)。因此,編寫這些臨時文件會佔用您要保存的 RAM,並且您的 RAM 即將用完:您的文件有 1.6 億行,而快速的Google表明它是 11GB 的未壓縮數據。

sort您可以通過更改它使用的臨時目錄來“幫助”解決這個問題。您已經這樣做了,-T.將臨時文件放在目前目錄中。可能你那裡的空間不夠了?或者目前目錄是否在tmpfs或類似?

您有一個包含中等數據量的 CSV 文件(對於現代 PC 而言,1.6 億行的數據量並不多)*。*與其將其放入一個旨在處理大量數據的系統中,不如嘗試使用 1990 年代的工具(是的,我剛剛閱讀sortgit 歷史)對其進行操作,當時 16 MB RAM 似乎相當大。

CSV 只是處理大量數據*的錯誤數據格式,您的範例就是完美的例證。*低效的工具以低效的方式處理低效的資料結構(帶有行的文本文件),以用低效的方法實現目標:

您只想知道每個值出現在第二列的頻率。因為你的工具uniq -c(出現)。


所以,也許現在是告訴您,不,不要使用基於 CSV 的數據流的好時機。一個簡單的

sqlite3 place.sqlite

並在那個外殼中(假設您的 CSV 有一個 SQLite 可以用來確定列的標題行)(當然,替換$second_column_name為該列的名稱)

.import 022_place_canvas_history.csv canvas_history --csv
SELECT $second_column_name, count($second_column_name)
 FROM canvas_history
 GROUP BY $second_column_name;

很可能與您獲得一個實際的數據庫文件一樣快,而且還有好處place.sqlite。您可以更靈活地使用它——例如,創建一個表格,您可以在其中提取座標,並將時間轉換為數字時間戳,然後根據您的分析更快、更靈活。


¹ 全域變數,以及何時使用的不一致。他們受傷了。對於 C 作者來說,這是一個不同的時代。它絕對不是糟糕的 C,只是……不是你從更現代的程式碼庫中習慣的那種。感謝 Jim Meyering 和 Paul Eggert 編寫和維護這個程式碼庫!

² 你可以嘗試做以下事情:對一個不太大的文件進行排序,比如說,ls.c有 5577 行,並記錄打開的文件數:

strace -o /tmp/no-size.strace -e openat sort ls.c
strace -o /tmp/s1kB-size.strace -e openat sort -S 1 ls.c
strace -o /tmp/s100kB-size.strace -e openat sort -S 100 ls.c
wc -l /tmp/*-size.strace

引用自:https://unix.stackexchange.com/questions/698627