8

我确实必须处理非常大的纯文本文件(超过 10 GB,是的,我知道这取决于我们应该称之为大的),而且行很长。

我最近的任务涉及基于另一个文件中的数据进行一些行编辑。

数据文件(应该修改)包含 1500000 行,每行例如 800 个字符长。每一行都是唯一的,并且只包含一个身份号码,每个身份号码都是唯一的)

修改文件是例如1800 行长,包含一个身份号码,以及一个应该在数据文件中修改的数量和日期。

我刚刚将修饰符文件(使用 Vim 正则表达式)转换为 sed,但效率非常低。

假设我在数据文件中有这样一行:

(some 500 character)id_number(some 300 character)

我需要修改 300 字符部分中的数据。

基于修饰符文件,我想出了这样的 sed 行:

/id_number/ s/^\(.\{650\}\).\{20\}/\1CHANGED_AMOUNT_AND_DATA/

所以我有1800行这样的。

但我知道,即使在非常快的服务器上,如果我执行

sed -i.bak -f modifier.sed data.file

它非常慢,因为它必须读取每个模式 x 每一行。

没有更好的方法吗?

注意:我不是程序员,从未(在学校)学习过算法。我可以在服务器上使用 awk、sed、perl 的过时版本。

4

6 回答 6

6

我建议的方法(按理想的顺序)是将这些数据处理为:

  1. 一个数据库(即使是一个简单的基于 SQLite 的带有索引的数据库在 10GB 文件上的性能也比 sed/awk 好得多)
  2. 包含固定记录长度的平面文件
  3. 包含可变记录长度的平面文件

使用数据库可以处理所有减慢文本文件处理速度的小细节(查找您关心的记录、修改数据、将其存储回数据库)。看看 Perl 的 DBD::SQLite。

如果您想坚持使用平面文件,您需要在大文件旁边手动维护一个索引,以便您可以更轻松地查找需要操作的记录编号。或者,更好的是,也许您的 ID 号就是您的记录号?

如果您有可变记录长度,我建议转换为固定记录长度(因为它似乎只有您的 ID 是可变长度)。如果你不能这样做,也许任何现有数据都不会在文件中移动?然后您可以维护前面提到的索引并根据需要添加新条目,不同之处在于您现在指向文件中的绝对位置,而不是指向记录号的索引。

于 2009-05-11T17:17:41.390 回答
3

我建议您使用 Perl 编写的程序(因为我不是 sed/awk 专家,而且我不知道他们完全有能力)。

您的“算法”很简单:首先,您需要构建一个哈希图,它可以为您提供新的数据字符串以应用于每个 ID。这当然是通过读取修改文件来实现的。

填充此 hasmap 后,您可以浏览数据文件的每一行,读取行中间的 ID,然后生成新行,如上所述。

我也不是 Perl 大师,但我认为程序很简单。如果您需要帮助来编写它,请请求它:-)

于 2009-05-11T17:05:08.680 回答
2

对于 perl,您应该使用 substr 来获取 id_number,尤其是在 id_number 具有恒定宽度的情况下。

my $id_number=substr($str, 500, id_number_length);

之后,如果 $id_number 在范围内,则应使用 substr 替换剩余文本。

substr($str, -300,300, $new_text);

Perl 的正则表达式非常快,但在这种情况下不是。

于 2009-05-11T17:18:58.547 回答
1

我的建议是,不要使用数据库。在此类任务中,编写良好的 perl 脚本将在数量级上胜过数据库。相信我,我有很多实践经验。当 perl 完成时,您不会将数据导入数据库。

当你用 800 个字符写 1500000 行时,对我来说似乎是 1.2GB。如果您的磁盘速度非常慢(30MB/s),您将在 40 秒内读取它。更好的 50 -> 24s,100 -> 12s 等等。但是 2GHz CPU 上的 perl 哈希查找(如 db join)速度高于 5Mlookups/s。这意味着您的 CPU 绑定工作将在几秒钟内完成,而您的 IO 绑定工作将在几十秒内完成。如果真的是 10GB,数字会改变,但比例是一样的。

您尚未指定数据修改是否更改大小(如果可以就地进行修改),因此我们不会假设它并将用作过滤器。您尚未指定“修改文件”的格式和修改类型。假设它由选项卡分隔,例如:

<id><tab><position_after_id><tab><amount><tab><data>

我们将从标准输入读取数据并写入标准输出,脚本可以是这样的:

my $modifier_filename = 'modifier_file.txt';

open my $mf, '<', $modifier_filename or die "Can't open '$modifier_filename': $!";
my %modifications;
while (<$mf>) {
   chomp;
   my ($id, $position, $amount, $data) = split /\t/;
   $modifications{$id} = [$position, $amount, $data];
}
close $mf;

# make matching regexp (use quotemeta to prevent regexp meaningful characters)
my $id_regexp = join '|', map quotemeta, keys %modifications;
$id_regexp = qr/($id_regexp)/;     # compile regexp

while (<>) {
  next unless m/$id_regexp/;
  next unless $modifications{$1};
  my ($position, $amount, $data) = @{$modifications{$1}};
  substr $_, $+[1] + $position, $amount, $data;
}
continue { print }

在我的笔记本电脑上,150 万行、1800 个查找 ID、1.2GB 数据大约需要半分钟。对于 10GB,它不应超过 5 分钟。它对你来说合理吗?

如果您开始认为您不受 IO 限制(例如,如果使用一些 NAS)但受 CPU 限制,您可以牺牲一些可读性并将其更改为:

my $mod;
while (<>) {
  next unless m/$id_regexp/;
  $mod = $modifications{$1};
  next unless $mod;
  substr $_, $+[1] + $mod->[0], $mod->[1], $mod->[2];
}
continue { print }
于 2009-05-16T22:29:35.067 回答
0

正如MikeyB 建议的那样,您几乎肯定应该使用数据库。

如果您出于某种原因不想使用数据库,那么如果修改列表适合内存(因为它目前将在 1800 行),最有效的方法是填充yves Baumes建议的修改的哈希表.

如果你到了连修改列表都变得很大的地步,你需要按它们的 ID 对两个文件进行排序,然后执行列表合并——基本上:

  1. 将输入文件“顶部”的 ID 与修改文件“顶部”的 ID 进行比较
  2. 如果匹配,相应地调整记录
  3. 把它写出来
  4. 丢弃具有(按字母顺序或数字顺序)最低 ID 的文件中的“顶部”行,并从该文件中读取另一行
  5. 转到 1。

在幕后,如果您使用单个 SQLUPDATE命令执行此更改,数据库几乎肯定会使用列表合并。

于 2009-05-12T12:52:41.860 回答
0

对 sqlloader 或 datadump 的决定很好。这就是要走的路。

于 2009-05-12T19:04:46.027 回答