2017-11-18 101 views
1

我有一個非常大的CSV文件(~10mil行),其中包含兩個表示ids的數字列。要求是:給定第一個ID,返回第二個ID非常快。 我需要讓CSV的行爲像一個地圖結構,它必須在內存中。我找不到將awk變量返回給shell的方法,所以我想使用bash關聯數組。將大型CSV文件加載到bash關聯數組中緩慢/卡住

問題是,將csv加載到關聯數組中變得非常緩慢/卡住〜8 mil行後。我一直試圖消除我可以想到的放緩原因:文件讀取/ IO,關聯陣列化限制。因此,I have a couple of functions將文件讀取到關聯數組中,但它們都具有相同的緩慢問題。

Here is the test data

  1. loadSplittedFilesViaMultipleArrays - >假定原始文件被分成更小的文件(1場密耳的行),並使用一個同時讀取循環來建立4個關聯數組(最大3個密耳的每個記錄)
  2. loadSingleFileViaReadarray - >使用readarray將原始文件讀入臨時數組,然後通過它來構建關聯數組
  3. loadSingleFileViaWhileRead - >使用一段時間的讀循環來構建關聯數組

但我似乎無法弄清楚。也許這樣做是完全錯誤的......任何人都可以提出一些建議嗎?

+0

Bash是不是非常適合此類任務。使用適當的編程語言會更好。 – janos

+0

你的問題是根本:你選擇了錯誤的工具... – HuStmpHrrr

+0

'sqlite3'或'mysql'更適合。 – dawg

回答

2

Bash是這種大小的關聯數組的錯誤工具。考慮使用的語言更適合(Perl,Python和Ruby的,PHP,JS,等等等等)

對於一個只有猛砸環境你可以使用它通常與猛砸安裝sqlite3 SQL數據庫。 (但它不是POSIX)

首先,您將從您的csv文件創建數據庫。有很多方法可以做到這一點(Perl,Python和Ruby的,GUI工具),但是這是很簡單的在sqlite3的command line shellexp.db不能在這一點上存在)交互做:

$ sqlite3 exp.db 
SQLite version 3.19.3 2017-06-27 16:48:08 
Enter ".help" for usage hints. 
sqlite> create table mapping (id integer primary key, n integer); 
sqlite> .separator "," 
sqlite> .import /tmp/mapping.csv mapping 
sqlite> .quit 

或者,管的SQL語句:

#!/bin/bash 

cd /tmp 

[[ -f exp.db ]] && rm exp.db # must be a new db as written 

echo 'create table mapping (id integer primary key, n integer); 
.separator "," 
.import mapping.csv mapping' | sqlite3 exp.db 

(注:如寫的,exp.db必須不存在,或者你會得到INSERT failed: UNIQUE constraint failed: mapping.id你可以寫這樣的數據庫exp.db被更新,而不是通過CSV文件創建的,但你可能會想。使用像Python,Perl,Tcl,Ruby等語言來做到這一點。)

無論哪種情況,這將創建一個將第一列映射到第二列的索引數據庫。進口將需要一段時間(15-20秒與198 MB的例子),但它創建從導入CSV新的持久性數據庫:

$ ls -l exp.db 
-rw-r--r-- 1 dawg wheel 158105600 Nov 19 07:16 exp.db 

然後你就可以快速地從猛砸查詢該新的數據庫:

$ time sqlite3 exp.db 'select n from mapping where id=1350044575' 
1347465036 

real 0m0.004s 
user 0m0.001s 
sys  0m0.001s 

我的舊iMac需要4毫秒。

如果你想使用bash的變量爲您查詢,您可以連接或根據需要建立查詢字串:

$ q=1350044575 
$ sqlite3 exp.db 'select n from mapping where id='"$q" 
1347465036 

而且由於分貝是持久性的,你可以比較csv文件的文件倍該數據庫文件來測試是否需要重新創建:

if [[ ! -f "$db_file" || "$csv_file" -nt "$db_file" ]]; then 
    [[ -f "$db_file" ]] && rm "$db_file" 
    echo "creating $db_file" 
    # create the db as above... 
else 
    echo "reusing $db_file"  
fi  
# query the db... 

更多:

  1. sqlite tutorial
  2. sqlite home
+0

不錯!不確定爲什麼我的「exp.db」爲151MB,而你的是274MB?我在Mac上使用3.19.3版本。 –

+0

哇!這是一個很大的文件。我的'sql'超級生鏽,所以可能有辦法創建一個更小,更快的文件。這主要是我試圖通過的概念。 – dawg

+0

好的 - 在我的主要mac上試過,文件是158MB。示例CSV是198MB。 Hmmmm。猜猜這取決於mac和文件的歷史。第一個文件可能有一些事務歷史記錄。 – dawg

1

我做了一個小的基於Perl的TCP服務器,它將CSV讀入哈希,然後永遠循環查找通過TCP從客戶端來的請求。這是不言自明:

#!/usr/bin/perl 
use strict; 
use warnings; 

################################################################################ 
# Load hash from CSV at startup 
################################################################################ 
open DATA, "mapping.csv"; 
my %hash; 
while(<DATA>) { 
    chomp $_; 
    my ($field1,$field2) = split /,/, $_; 
    if($field1 ne '') { 
     $hash{$field1} = $field2; 
    } 
} 
close DATA; 
print "Ready\n"; 

################################################################################ 
# Answer queries forever 
################################################################################ 
use IO::Socket::INET; 

# auto-flush on socket 
$| = 1; 
my $port=5000; 

# creating a listening socket 
my $socket = new IO::Socket::INET (
    LocalHost => '127.0.0.1', 
    LocalPort => $port, 
    Proto => 'tcp', 
    Listen => 5, 
    Reuse => 1 
); 
die "cannot create socket $!\n" unless $socket; 

while(1) 
{ 
    # waiting for a new client connection 
    my $client_socket = $socket->accept(); 

    my $data = ""; 
    $client_socket->recv($data, 1024); 

    my $key=$data; 
    chomp $key; 
    my $reply = "ERROR: Not found $key"; 
    if (defined $hash{$key}){ 
     $reply=$hash{$key}; 
    } 
    print "DEBUG: Received $key: Replying $reply\n"; 

    $client_socket->send($reply); 
    # notify client that response has been sent 
    shutdown($client_socket, 1); 
} 

所以,你的代碼保存以上爲go.pl,然後使其可執行文件:

chmod +x go.pl 

然後啓動服務器與背景:

./go.pl & 

然後,當您想要作爲客戶端進行查找時,可以使用如下標準的socat實用程序將密鑰發送到localhost:5000:

socat - TCP:127.0.0.1:5000 <<< "1350772177" 
1347092335 

作爲一個快速的標杆,它在8秒1000個查詢。

START=$SECONDS; tail -1000 *csv | awk -F, '{print $1}' | 
    while read a; do echo $a | socat - TCP:127.0.0.1:5000 ; echo; done; echo $START,$SECONDS 

它可能會稍微改變一下,以處理多個鍵來查找每個請求以減少套接字連接和拆卸開銷。

+0

嗯...這是OP的問題的大規模矯枉過正。你不這麼認爲嗎? – HuStmpHrrr

+0

@HuStmpHrrr不,一點也不 - 我認爲8ms很不錯。它已經在這裏9個小時沒有任何其他的建議... –

+0

謝謝@MarkSetchell,但我想避免使用Perl(我不想在組織中引入新的語言)) – treaz

2

受@ HuStmpHrrr的評論的啓發,我想到了另一個,也許更簡單的選擇。

您可以使用GNU並行到了分割文件到1MB(或其它)大小的塊,然後用你的所有CPU核心搜索所得的各塊的並行:

parallel --pipepart -a mapping.csv --quote awk -F, -v k=1350044575 '$1==k{print $2;exit}' 
1347465036 

下注意到第二個在我的iMac上,這是最後一個記錄。