将大型CSV文件加载到bash关联数组中缓慢/卡住

时间:2017-11-18 10:29:02

标签: arrays bash

我有一个非常大的CSV文件(~10mil行),其中2个数字列代表id。要求是:给定第一个id,返回第二个id非常快。 我需要让CSV行为像地图结构,它必须在内存中。我找不到将awk变量暴露给shell的方法,所以我想到了使用bash关联数组。

问题是将csv加载到关联数组中会在~8 mil行之后变得非常慢/卡住。我一直试图消除我能想到的减速原因:文件读取/ IO,关联arraylimitations。因此,I have a couple of functions将文件读入关联数组,但所有这些都具有相同的慢速问题。

Here is the test data

  1. loadSplittedFilesViaMultipleArrays - >假设原始文件被拆分为较小的文件(1 mil行)并使用while循环来构建4个关联数组(每个最多3 mil记录)
  2. loadSingleFileViaReadarray - >使用readarray将原始文件读入临时数组,然后通过它来构建关联数组
  3. loadSingleFileViaWhileRead - >使用while循环来构建关联数组
  4. 但我似乎无法弄明白。也许这样做是完全错误的......任何人都可以提出一些建议吗?

3 个答案:

答案 0 :(得分:2)

受到@ HuStmpHrrr评论的启发,我想到了另一个,也许是更简单的选择。

您可以使用 GNU Parallel 将文件拆分为1MB(或其他)大小的块,然后使用所有CPU内核并行搜索每个生成的块:

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

在我的iMac上花了一秒钟,这是最后的记录。

答案 1 :(得分:2)

Bash是这种大小的关联数组的错误工具。考虑使用更适合的语言(Perl,Python,Ruby,PHP,js等)

对于仅限Bash 环境,您可以使用通常随Bash一起安装的sqlite3 sql数据库。 (但它不是POSIX)

首先,您将从csv文件创建数据库。有很多方法可以做到这一点(Perl,Python,Ruby,GUI工具),但这很简单,可以在sqlite3中进行交互式command line shell(此时exp.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等语言来做这件事。)

在任何一种情况下,都会创建一个索引数据库,将第一列映射到第二列。导入将需要一段时间(使用198 MB示例需要15-20秒),但它会从导入的csv创建一个新的持久数据库:

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

然后,您可以从Bash快速查询新数据库:

$ 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

由于db是持久的,你可以只将csv文件的文件时间与db文件进行比较,以测试是否需要重新创建它:

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

答案 2 :(得分: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秒内完成1,000次查找。

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 

可能会稍微改变一下来处理多个键来查找每个请求以减少套接字连接和拆卸开销。