巨大的文本文件(6Gb)搜索和替换

时间:2014-01-16 13:13:40

标签: python perl shell sed awk

我有一个巨大的文件(6Gb),其格式为74.000篇文章:

<text id="1">
bla bla bla bla.........
</text>
<text id="2">
bla bla bla bla.........
</text>
<text id="3">
bla bla bla bla.........
</text>
<text id="............ and so on untill 74.000

然后我有另一个文件,其标题对应于每个id,如下所示:

1       title1
2       title2
3       title3
...
74000   title74000

我必须在第一个文件中为每个id添加相应的标题,所以我将第二个文件转换为这个脚本:

sed -i "s/<text id="1">/<text id="1" title="title1">/" file1
sed -i "s/<text id="2">/<text id="2" title="title2">/" file1
sed -i "s/<text id="3">/<text id="3" title="title3">/" file1
...
sed -i "s/<text id="74000">/<text id="74000" title="title74000">/" file1

注意我没有将g放在sed命令的末尾,因为它不是全局serch,这意味着在第一次匹配时它会更改字符串并转到下一个搜索。该脚本可以工作,但是由于文件的大小每次更改需要12分钟,这让我有两年的时间来完成所有更改,而我需要它们尽快,所以我的问题是如果有人知道如何执行此更改以更快的方式,也许与其他一些实用程序,python,perls或任何其他...

7 个答案:

答案 0 :(得分:4)

在Gnu Awk第4版中,您可以尝试:

gawk4 -f a.awk file2 RS="^$" file1

其中a.awk是:

NR==FNR {
   b["<text id=\""$1"\">"]=$2
   next
}

{
    n=split($0,a,/<text id=[^>]*>/,s)
    printf "%s%s",s[0],a[1]
    for (i=1; i<n; i++) {
        ind=index(s[i],">")
        printf "%s%s", substr(s[i],1,ind-1) " title=\""b[s[i]]"\">", a[i+1]
    }
    printf "%s",s[n]
}

输出:

<text id="1" title="title1">
  bla bla bla bla.........
</text>
<text id="2" title="title2">
  bla bla bla bla.........
</text>
<text id="3" title="title3">
  bla bla bla bla.........
</text>

<强>更新

为了好玩,我在3.9Mb xml文件(80000个标题)和1.3Mb信息文件(也是80000个标题)上测试了一些解决方案

  • @HåkonHægland:0.629s
  • @tangent:0.645s
  • @Borodin:0.718s
  • @glennjackman:1.098s

(生成输入文件的脚本可以在这里找到:http://pastebin.com/PpTPt0gk

更新2

为了获得更可靠的计时结果,我平均花费了20多次:

  • @EdMorton:0.485s(Gnu Awk 4.1版)
  • @EdMorton:0.528s(Gnu Awk 3.1.8版)
  • @HåkonHægland:0.589s
  • @Borodin:0.599s
  • @tangent:0.626s
  • @glennjackman:1.074s

答案 1 :(得分:3)

我建议你使用这样的东西。

每次遇到XML文件中的<text>标记时,它都会从titles文件中读取一行,并将title属性插入到标记中。

它还检查两个文件中的ID是否匹配,并每500 <text>个元素打印一个日志输出,以便您可以看到它的进度。

输出发送到单独的文件。您不应该覆盖输入文件,就好像出现问题一样,您丢失了原始数据。

这应该只比复制XML文件慢一点。

use strict;
use warnings;

use IO::Handle;

STDOUT->autoflush;

open my $in_xml,    '<', 'input.xml'  or die "Failed to open XML file: $!";
open my $in_titles, '<', 'titles.txt' or die "Failed to open titles file: $!";
open my $out_xml,   '>', 'output.xml' or die "Failed to open output file: $!";

while (my $xml_line = <$in_xml>) {

  if ( $xml_line =~ /<text/ ) {

    my ($id1) = $xml_line =~ /id="(\d+)"/;
    unless (defined $id1) {
      chomp;
      die sprintf qq{Error in input XML file at line %d: %s\n-}, $in_xml->input_line_number, $_;
    }
    printf "Processing ID %d\n", $id1 unless $id1 % 500;

    my $title_line = <$in_titles>;
    my ($id2, $title) = $title_line =~ /^(\d+)\s+(.+)/;
    unless (defined $id2) {
      chomp $title_line;
      die sprintf qq{Error in input titles file at line %d: %s\n-}, $in_titles->input_line_number, $title_line;
    }

    unless ($id1 == $id2) {
      die sprintf "ID mismatch %d <=> %d\nXML file line %d\ntitles file line %d\n-",
          $id1, $id2, $in_xml->input_line_number, $in_titles->input_line_number
    }

    $xml_line =~ s/>/ title="$title">/;
  }

  print $out_xml $xml_line;
}

close $out_xml or die "Failed to close output file: $!";

<强>输出

<text id="1" title="title1">
bla bla bla bla.........
</text>
<text id="2" title="title2">
bla bla bla bla.........
</text>
<text id="3" title="title3">
bla bla bla bla.........
</text>

答案 2 :(得分:2)

awk '
NR==FNR {
    id = $1
    sub(/^[^[:space:]]+[[:space:]]+/,"")
    map["<text id=\"" id "\">"] = "<text id=\"" id "\" title=\"" $0 "\">"
    next
}
$0 in map { $0 = map[$0] }
1
' file2 file1

如果file2是制表符分隔的,那么它会更简单,我希望更快:

awk -F'\t' '
NR==FNR {
    map["<text id=\"" $1 "\">"] = "<text id=\"" $1 "\" title=\"" $2 "\">"
    next
}
$0 in map { $0 = map[$0] }
1
' file2 file1

答案 3 :(得分:1)

这是GNU awk的另一种方法

gawk '
    NR == FNR { title[NR] = $0; next }
    match($0, /<text id="([[:digit:]]+)">/, m) {
        sub(/>/, " title=\"" title[m[1]] "\">")
    }
    {print}
' titles articles

awk保留2个计数器:FNR是当前正在处理的文件中的记录号; NR是到目前为止处理的所有记录的记录号。对于第一个文件中的所有记录,条件NR == FNR都为真。

你需要GNU awk来扩展他的match()函数,第三个参数是一个存储正则表达式匹配部分的数组。

答案 4 :(得分:1)

这可能适合你(GNU sed):

sed -r 's|^([0-9]+)\s*(.*)|/(<text id="\1")(>)/s//\\1 title="\2"\\2/|;t;d' file2 |
sed -rf - file1

针对包含标题的文件运行sed脚本,以生成对源文件运行的sed脚本。

小心标题中的元字符!

答案 5 :(得分:1)

这是另一个Perl版本。它首先将所有标题读入哈希,然后将原始文件中的每一行复制到新文件中,必要时替换。

use strict;
use warnings;

open (my $title_file, '<', 'titles.txt') or die "Could not open titles.txt, $!";
my %titles;
while (<$title_file>) {
    chomp;
    my ($id,$title) = split(m/\s+/,$_,2);
    $titles{$id} = $title;
}
close $title_file;

open (my $in_file, '<', 'in.txt') or die "Could not open in.txt, $!";
open (my $out_file, '>', 'out.txt') or die "Could not open out.txt, $!";
while (<$in_file>) {
    if (m/<text id=/) {
        s/<text id="(\d+)">/<text id="$1" title="$titles{$1}">/;
    }
    print $out_file $_;
}
close $in_file;
close $out_file;

答案 6 :(得分:1)

从具有同伴ID - 标题

的文件中为您的sed指令创建第一个文件
sed 's|\([0-9]\{1,\}\)[[:blank:]]*\([^[:blank:]].*\)|/<text id="\1"/ {s/>/ title="\2">/\
   b\
   }|' ID_Title_File > /tmp/ID_Chg.sed

2加速器(与您的版本相比)

  1. 在同一个sed中执行所有操作(每次替换不重启sed非常耗时,尤其是在这样的行数上)
  2. 在成功找到后,替换结束而不是跳过其余的动作 同一条线(所以不再测试这种情况
  3. 从此操作列表中处理您的大文件

    sed -unbuffer -f /tmp/ID_Chg.sed file1 > Output
    

    对于GNU sed,您可能需要-posix选项(在KSH / AIX上进行测试)

    仅供测试目的:

    Increment=$((80000 / 128));echo "" > /tmp/ID_Chg.sed;Iter=0;while [ $Iter -lt 80000 ]; do echo "/id=\"$Iter\""/ b r$Iter >> /tmp/ID_Chg.sed; let Iter+=Increment; done
    sed 's|\([0-9]\{1,\}\)[[:blank:]]*\([^[:blank:]].*\)|:r\1\
    

    / ^ / title =“\ 2”&gt; / \    b \    } |” ID_Title.lst&gt;&gt; /tmp/ID_Chg.sed

    其中80000是ID的数量和128个子部分(“加速器”)想要的数量