web-dev-qa-db-ja.com

巨大な(70GB)、1行のテキストファイルの文字列を置き換えます

巨大(70GB)、1行、テキストファイルがあり、その中の文字列(トークン)を置き換えたい。トークンを置き換えたい<unk>、別のダミートークンを使用( グローブの問題 )。

私はsedを試しました:

sed 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

しかし、出力ファイルcorpus.txt.newのバイト数はゼロです!

私もPerlを使ってみました:

Perl -pe 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

しかし、メモリ不足エラーが発生しました。

小さいファイルの場合、上記のコマンドは両方とも機能します。

文字列をそのようなファイルに置き換えるにはどうすればよいですか? これ は関連する質問ですが、どの回答もうまくいきませんでした。

Edit:ファイルをそれぞれ10GB(またはそれ以上)のチャンクに分割し、それぞれにsedを適用してからマージするのはどうですか? cat?それは理にかなっていますか?よりエレガントな解決策はありますか?

128

通常のテキスト処理ツールは、RAMに収まらない行を処理するようには設計されていません。 1つのレコード(1行)を読み取り、それを操作して結果を出力し、次のレコード(行)に進む傾向があります。

ASCII文字がファイルに頻繁に出現し、<unk>または<raw_unk>には出現しない)がある場合は、それをレコード区切り文字として使用できます。ツールは、カスタムレコードセパレーターを許可せず、その文字と改行を入れ替えます。trは行ではなくバイトを処理するため、レコードサイズは関係ありません。;が機能するとします。

<corpus.txt tr '\n;' ';\n' |
sed 's/<unk>/<raw_unk>/g' |
tr '\n;' ';\n' >corpus.txt.new

また、検索テキストで繰り返されておらず、十分な頻度で表示されている場合は、検索するテキストの最初の文字を固定することもできます。ファイルがunk>で始まる場合は、sedコマンドをsed '2,$ s/…に変更して、誤った一致を回避します。

<corpus.txt tr '\n<' '<\n' |
sed 's/^unk>/raw_unk>/g' |
tr '\n<' '<\n' >corpus.txt.new

または、最後の文字を使用します。

<corpus.txt tr '\n>' '>\n' |
sed 's/<unk$/<raw_unk/g' |
tr '\n>' '>\n' >corpus.txt.new

この手法は、sedが改行で終わらないファイルでシームレスに動作することを前提としています。 GNU sedで動作します。ファイルの最後の文字をレコードセパレータとして選択できる場合は、移植性の問題を回避できます。

このような大きなファイルの場合、1つの可能性はFlexです。 unk.lになる:

%%
\<unk\>     printf("<raw_unk>");  
%%

次に、コンパイルして実行します。

$ flex -o unk.c  unk.l
$ cc -o unk -O2 unk.c -lfl
$ unk < corpus.txt > corpus.txt.new
111
JJoao

したがって、ファイル全体を一度に保持するのに十分な物理メモリ(RAM)はありませんが、64ビットシステムでは、マッピングするのに十分な仮想アドレススペースがあります。ファイル全体。このような場合、仮想マッピングは単純なハックとして役立ちます。

必要な操作はすべてPythonに含まれています。いくつかの厄介な微妙な点がありますが、Cコードを記述する必要はありません。特に、メモリ内のファイルをコピーしないように注意する必要があります。これはポイントを完全に無効にします。プラスの面として、無料でエラー報告を入手できます(python "exceptions"):)。

#!/usr/bin/python3
# This script takes input from stdin
# (but it must be a regular file, to support mapping it),
# and writes the result to stdout.

search = b'<unk>'
replace = b'<raw_unk>'


import sys
import os
import mmap

# sys.stdout requires str, but we want to write bytes
out_bytes = sys.stdout.buffer

mem = mmap.mmap(sys.stdin.fileno(), 0, access=mmap.ACCESS_READ)
i = mem.find(search)
if i < 0:
    sys.exit("Search string not found")

# mmap object subscripts to bytes (making a copy)
# memoryview object subscripts to a memoryview object
# (it implements the buffer protocol).
view = memoryview(mem)

out_bytes.write(view[:i])
out_bytes.write(replace)
out_bytes.write(view[i+len(search):])
40
sourcejedi

Cバージョンの方がはるかにパフォーマンスが良いかもしれません。

#include <stdio.h>
#include <string.h>

#define PAT_LEN 5

int main()
{
    /* note this is not a general solution. In particular the pattern
     * must not have a repeated sequence at the start, so <unk> is fine
     * but aardvark is not, because it starts with "a" repeated, and ababc
     * is not because it starts with "ab" repeated. */
    char pattern[] = "<unk>";          /* set PAT_LEN to length of this */
    char replacement[] = "<raw_unk>"; 
    int c;
    int i, j;

    for (i = 0; (c = getchar()) != EOF;) {
        if (c == pattern[i]) {
            i++;
            if (i == PAT_LEN) {
                printf("%s", replacement);
                i = 0;
            }
        } else {
            if (i > 0) {
                for (j = 0; j < i; j++) {
                    putchar(pattern[j]);
                }
                i = 0;
            }
            if (c == pattern[0]) {
                i = 1;
            } else {
                putchar(c);
            }
        }
    }
    /* TODO: fix up end of file if it ends with a part of pattern */
    return 0;
}

編集:コメントからの提案に従って変更されました。パターン<<unk>のバグも修正しました。

16
Patrick Bucher

Mariadb-server/mysql-serverパッケージにはreplaceユーティリティがあります。単純な文字列(正規表現ではない)を置き換えます。grep/ sed/awkとは異なり、replace\n\0を無視します。メモリ消費は、どの入力ファイルでも一定です(私のマシンでは約400kb)。

もちろん、replaceを使用するためにmysqlサーバーを実行する必要はありません。Fedoraでそのようにパッケージ化されているだけです。他のディストリビューション/オペレーティングシステムでは、個別にパッケージ化されている場合があります。

16
legolegs

GNU grepは、行全体をメモリに読み込まなくても、「バイナリ」ファイルの一致のオフセットを表示できます。次に、ddを使用してこのオフセットまで読み取り、一致をスキップして、ファイルからのコピーを続行できます。

file=...
newfile=...
replace='<raw_unk>'
grep -o -b -a -F '<unk>' <"$file" |
(   pos=0
    while IFS=$IFS: read offset pattern
    do size=${#pattern}
       let skip=offset-pos
       let big=skip/1048576
       let skip=skip-big*1048576
       dd bs=1048576 count=$big <&3
       dd bs=1 count=$skip <&3
       dd bs=1 count=$size of=/dev/null <&3
       printf "%s" "$replace"
       let pos=offset+size
    done
    cat <&3
) 3<"$file" >"$newfile"

速度を上げるために、ddをブロックサイズ1048576の大きな読み取りと一度に1バイトの小さな読み取りに分割しましたが、このような大きなファイルでは、この操作はまだ少し遅くなります。 grep出力は、たとえば、13977:<unk>、これは、変数offsetおよびpatternへの読み取りによってコロンで分割されます。 posで、ファイルから既にコピーされたバイト数を追跡​​する必要があります。

14
meuh

パフォーマンスの高い「ブロックサイズ」を「探す」ことができるため、他のオプションよりもパフォーマンスが向上する可能性がある別の単一のUNIXコマンドラインを次に示します。これを堅牢にするためには、すべてのX文字に少なくとも1つのスペースがあることを知っている必要があります。ここで、Xは任意の「ブロックサイズ」です。以下の例では、1024文字の「ブロックサイズ」を選択しています。

fold -w 1024 -s corpus.txt | sed 's/<unk>/<raw_unk>/g' | tr '/n' '/0'

ここでは、foldは最大 1024バイトを取得しますが、-sは、最後のブレーク以降に少なくとも1つが存在する場合、スペースでブレークすることを確認します。

Sedコマンドはあなたのものであり、あなたが期待することをします。

次に、trコマンドは、挿入された改行を何にも戻さないようにファイルを「展開」します。

ブロックサイズを大きくして、パフォーマンスが向上するかどうかを確認することを検討してください。 1024の代わりに、foldの-wオプションに10240、102400、および1048576を試すことができます。

以下は、すべてのNを小文字に変換する各ステップで分解した例です。

[root@alpha ~]# cat mailtest.txt
test XJS C4JD QADN1 NSBN3 2IDNEN GTUBE STANDARD ANTI UBE-TEST EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt
test XJS C4JD QADN1
NSBN3 2IDNEN GTUBE
STANDARD ANTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g'
test XJS C4JD QADn1
nSBn3 2IDnEn GTUBE
STAnDARD AnTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g' | tr '\n' '\0'
test XJS C4JD QADn1 nSBn3 2IDnEn GTUBE STAnDARD AnTI UBE-TEST EMAIL*C.34X test

Trコマンドで削除されるため、改行がある場合は、ファイルの最後に改行を追加する必要があります。

11
alfreema

Perlの使用

独自のバッファーの管理

IO::Handlesetvbufを使用してデフォルトのバッファーを管理するか、sysreadsyswriteを使用して独自のバッファーを管理できます。詳細については、perldoc -f sysreadおよびperldoc -f syswriteを確認してください。基本的に、これらはバッファリングされたioをスキップします。

ここでは独自のバッファIOをロールしますが、手動で任意に1024バイトで行います。また、RWのファイルを開くので、すべて同じFHで一度に実行します。

use strict;
use warnings;
use Fcntl qw(:flock O_RDWR);
use autodie;
use bytes;

use constant CHUNK_SIZE => 1024 * 32;

sysopen my $fh, 'file', O_RDWR;
flock($fh, LOCK_EX);

my $chunk = 1;
while ( sysread $fh, my $bytes, CHUNK_SIZE * $chunk ) {
  if ( $bytes =~ s/<unk>/<raw_unk>/g ) {
    seek( $fh, ($chunk-1)* CHUNK_SIZE, 0 );
    syswrite( $fh, $bytes, 1024);
    seek( $fh, $chunk * CHUNK_SIZE, 0 );
  }
  $chunk++;
}

このルートに行くなら

  1. <unk><raw_unk>が同じバイトサイズであることを確認してください。
  2. 1バイト以上を置き換える場合は、バッファリングされたメソッドがCHUNKSIZE境界を超えないようにする必要がある場合があります。
10
Evan Carroll

bbebinary block editor )、「sed forバイナリファイル」。

私は、EOL文字のない7GBのテキストファイルでそれを使用して、文字列の複数の出現を異なる長さの文字列に置き換えて、うまくいきました。最適化を試みなかった場合、平均処理スループットは50MB /秒を超えました。

9
ovirt

Perlを使用すると、次のような固定長レコードを処理できます。

Perl -pe 'BEGIN{$/=\1e8}
          s/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

そして、<unk>sこれらの100MBレコードの2つにまたがっています。

5

これは、タスクを実行する小さなGoプログラムです(unk.go):

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    const (
        pattern     = "<unk>"
        replacement = "<raw_unk>"
    )
    var match int
    var char rune
    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        char = rune(scanner.Text()[0])
        if char == []rune(pattern)[match] {
            match++
            if match == len(pattern) {
                fmt.Print(replacement)
                match = 0
            }
        } else {
            if match > 0 {
                fmt.Print(string(pattern[:match]))
                match = 0
            }
            if char == rune(pattern[0]) {
                match = 1
            } else {
                fmt.Print(string(char))
            }
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

go build unk.goとして実行し、./unk <input >output

編集:

すみません、すべてが1行になっていることを読んでいないので、ファイルを1文字ずつ読み込もうとしました。

編集II:

Cプログラムと同じ修正が適用されました。

5
Patrick Bucher

これは70 GBのファイルと単純な検索と置換ではやり過ぎになる可能性がありますが、Hadoop MapReduceフレームワークは無料で今すぐ問題を解決します(ローカルで実行するように設定する場合は、[単一ノード]オプションを選択します)。コードを変更する必要なく、将来的には無限の容量にスケーリングされます。

https://hadoop.Apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html の公式チュートリアルでは、(非常に単純な)Javaですが、Perlや好きな言語のクライアントライブラリを見つけることができます。

したがって、後で7000GBのテキストファイルに対してより複雑な操作を実行していることを発見した場合、これを1日に100回実行する必要がある場合、プロビジョニングした、またはクラウドによって自動的にプロビジョニングされた複数のノードにワークロードを分散できます。ベースのHadoopクラスター。

1
Sam Rahimi

これまでの提案はすべて、ファイル全体を読み取り、ファイル全体を書き込む必要があります。これには時間がかかるだけでなく、70GBの空き容量が必要です。

1)私はあなたを理解していれば、特定の場合には、正しくは、同じ長さのいくつかの他の文字列と<UNK>を交換するにしてもよいでしょうか?

2a)複数の発生がありますか? 2b)その場合、いくつ知っていますか?

あなたはこの1年以上の問題をすでに解決していると思います。どの解決策を使用したか知りたいのですが。

可能性のあるブロックの交差を考慮して、文字列をそれぞれ検索するファイルのブロックを読み取るソリューション(おそらくCで)を提案します。見つかったら、文字列を同じ長さの代替で置き換え、そのブロックのみを書き込みます。既知の発生回数またはファイルの終わりまで続行します。この場合、必要なのは発生回数の少ない書き込みと最大で2倍の書き込みです(すべての発生が2つのブロックに分割されている場合)。追加のスペースは必要ありません。

0
DGerman