sed 优化(基于较小数据集的大文件修改)

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

“它很慢,因为它必须读取每个模式和每一行。”
“难道没有更好的方法吗?”
“注意:我不是程序员,在学校从未学过算法。我可以在服务器上使用awk、sed、过时的perl版本。”

1
Perl 的版本是什么? - yves Baumes
perl 5.8.6 i586-linux-thread-multi - Zsolt Botykai
1
那确实是一个过时的 Perl 版本,但我怀疑它并没有你的备注中所说的那么糟糕 ;) - user55400
6个回答

6

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

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

使用数据库可以解决所有减缓文本文件处理速度的细节问题(查找所关心的记录,修改数据,将其存储回数据库)。在Perl中,可以考虑使用DBD::SQLite。

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

如果您有可变记录长度,我建议将其转换为固定记录长度(因为似乎只有您的ID是可变长度的)。如果您无法做到这一点,那么任何现有数据是否永远不会在文件中移动呢?然后,您可以维护先前提到的索引,并根据需要添加新条目,不同之处在于索引不再指向记录编号,而是指向文件中的绝对位置。


1
数据库解决方案(使用sqlldr、sqlplus)刚刚完成,而sed仍在7%运行中... - Zsolt Botykai

3
我建议您使用Perl编写一个程序(因为我不是sed / awk大师,也不知道它们确切的功能)。
您的“算法”很简单:首先需要构建一个哈希映射表,该表可以为每个ID提供新的数据字符串。当然,这是通过读取修改器文件来实现的。
一旦这个哈希表被填充,您可以浏览数据文件的每一行,读取行中间的ID,并生成如上所述的新行。
我也不是Perl大师,但我认为这个程序相当简单。如果您需要编写它,请提出请求 :-)

听起来是一个不错的解决方案,只要可以通过合理的努力提取出一行的ID - 这在问题中并不清楚,但我认为这是一个好的假设。 - user55400

2

如果id_number具有固定的宽度,您应该使用substr来获取id_number,特别是在使用perl时。

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

如果$id_number在范围内,你应该使用substr替换剩余的文本。

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

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


1

我的建议是,不要使用数据库。在这种任务中,编写良好的Perl脚本将比数据库快上一个数量级。相信我,我有很多实际经验。当Perl完成时,您将不必将数据导入数据库。

当您编写1500000行,每行800个字符时,对我来说它看起来像1.2GB。如果您的磁盘非常慢(30MB / s),则需要40秒才能读取它。更好的情况下,50-> 24秒,100-> 12秒等等。但是,Perl哈希查找(类似于数据库连接)在2GHz CPU上的速度超过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 }

虽然我的任务已经完成,但我也会尝试您的解决方案,因为 Oracle 并不总是可用的。无论如何,感谢您的帮助。 - Zsolt Botykai

0

你几乎肯定要使用数据库,就像MikeyB建议的那样。

如果由于某种原因你不想使用数据库,那么如果修改列表可以放入内存中(目前只有1800行),最有效的方法是使用哈希表,将修改内容填充到哈希表中,正如yves Baumes所建议的

如果你的修改列表变得非常庞大,你需要按照ID对两个文件进行排序,然后执行列表合并--基本上:

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

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


0

关于sqlloader或datadump的决定,这是个不错的选择。就这么办吧。


这应该被发布为评论。 - Viet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接