【问题标题】:huge text file (6Gb) search and replace巨大的文本文件(6Gb)搜索和替换
【发布时间】:2014-02-05 10:06:25
【问题描述】:

我有一个包含 74.000 篇文章的大文件 (6Gb):

<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 命令的末尾,因为它不是全局搜索,这意味着在第一次匹配时它会更改字符串并进入下一个搜索。该脚本有效,但由于文件很大,每次更改需要 12 分钟,这给了我大约两年的时间来完成所有更改,而我需要它们尽快,所以我的问题是,如果有人知道我该如何执行这些更改以更快的方式,也许与其他一些实用程序,python,perls 或任何其他......

【问题讨论】:

  • 我猜你最好逐行迭代文件。 awk 似乎是一个不错的选择。
  • 你能给我举个例子吗....
  • 第二个文件第一列的数字似乎是多余的。
  • 对不起,我的错误,已修复
  • @AndrésChandía: 抱歉,我不知道 awk,但我添加了标签,希望 awkward 的人能够帮助你。

标签: python perl shell sed awk


【解决方案1】:

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

每当在 XML 文件中遇到&lt;text&gt; 标记时,它都会从标题文件中读取一行,并将title 属性插入到标记中。

它还会检查两个文件中的 ID 是否匹配,并每 500 个&lt;text&gt; 元素打印一个日志输出,以便您查看其进度。

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

这应该只比复制 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>

【讨论】:

  • 我也会这样做。但我会将输出定向到内部文件,并每隔十个或一百个 id 将当前 id 打印到 STDOUT。这仍然会运行一段时间。
  • @DeVadder:是的,我同意并且我已经改进了它。
  • 感谢你们,我已经复制并编辑了文件,但是在运行它时它告诉我:无法获得引用控制台的文件描述符
    ./scrip2.sh: line 8: 意外标记)'&lt;br&gt; ./scrip2.sh: line 8: while () {'
    附近的语法错误
  • @AndrésChandía:如果没有看到您的代码,我无法说出问题所在。那是一条 Ubuntu 消息,而不是 Perl 消息。看起来您的问题出在scrip2.sh。我建议您直接从命令行运行 Perl 程序,以检查它是否可以独立运行。
  • script2.sh 是您的代码的精确副本,除了文件名,抱歉我的无知,但我怎样才能运行我直接从文件 script2.sh 复制的所有代码命令行?
【解决方案2】:

在 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.629 秒
  • @tangent : 0.645s
  • @Borodin : 0.718s
  • @glennjackman:1.098 秒

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

更新 2

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

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

【讨论】:

  • 你为什么不包括 Ops 版本,那是我最感兴趣的版本。^^ 现在我想,我有点好奇 tangent 是如何击败鲍罗丁的。他们以不同的顺序做同样的想法,恕我直言。
  • @DeVadder 原始海报脚本非常慢。仅执行前 10 个(80000 行)sed 编辑需要 5.736 秒,进行 20 次编辑需要 11.480 秒,因此完成所有 80000 行将花费 12 个小时以上 :)
  • @HåkonHægland:如果每个程序只运行 一次,那么这些数字是无法区分的。我同意 DeVadder 的观点:我看不出有什么比只读取两个文件一次更快的方法。
  • @HåkonHægland:谢谢。很好的努力和很好的答案。
  • 对不起,它给了我这个:gawk: a.awk:7: fatal: 4 is invalid as number of arguments for split
【解决方案3】:

这是使用 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】:

    这可能对你有用(GNU sed):

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

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

    注意标题中的元字符!

    【讨论】:

    • 我尝试了您的代码,只是为了将速度与此处的其他解决方案进行比较。但它似乎非常缓慢。似乎是最后一部分(sed -rf - file1)很慢。第一部分运行得很快。知道为什么吗?也许我做错了什么?
    【解决方案5】:

    这是另一个 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;
    

    【讨论】:

    • 这样的数据应该是一个数组,而不是一个散列。
    • 如果标题没有排序,散列会更好。
    • @Borodin 你的意思是标题吗?如果是这样,我认为哈希更好。如果任一文件的顺序不严格怎么办 - 数组会在那里失败。
    • @tangent:是的,我的意思是标题。它们的数字索引从 1 到 74,000,没有间隙。那是数组最好的地方。您可以按您喜欢的任何顺序将数据插入到数组中。
    • @DeVadder:即使标题乱序,数组也是更好的解决方案。
    【解决方案6】:
    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
    

    【讨论】:

    • 谢谢。如果“file2”是制表符分隔的,那会更快,因为我可以摆脱 sub() 并使用 $2 来填充 map[] (请参阅我刚刚添加的更新脚本)但我不知道是否不管是不是这样。
    【解决方案7】:

    从具有对等 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. 成功查找后,替换结束而不是跳过其余操作 同一行(因此不再测试这种情况

    从此操作列表中处理您的大文件

    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">/\ 乙\ }|' ID_Title.lst >> /tmp/ID_Chg.sed

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

    【讨论】:

    • 我尝试了你的第一个sed 命令,但我收到错误:sed: -e expression #1, char 92: unterminated s' 命令`
    • 对不起,错误的副本,已更改答案
    • 第一个 sed 命令(([^[:blank:]].*"\))中的星号后的双引号的目的是什么?您是否假设标题在双引号内?(如果您查看问题,它们是不在双引号内..)
    • 对,它在我的不同测试中,标题有空格和双引号,我删除它
    • 你的第二个sed 命令不应该是--unbuffered 吗? (不是-unbuffer)
    猜你喜欢
    • 2016-10-13
    • 1970-01-01
    • 2011-08-11
    • 2011-05-01
    • 2015-05-31
    • 1970-01-01
    • 2013-06-13
    相关资源
    最近更新 更多