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


126

巨大な(70GB)1行のテキストファイルがあり、その中の文字列(トークン)を置き換えたいと思います。token <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

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

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

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

編集:ファイルを10GBのチャンク(または何でも)に分割し、それぞれに適用sedしてからマージするのはcatどうですか?それは理にかなっていますか?よりエレガントなソリューションはありますか?


@Gillesが述べたように、単一の大きな行でカスタム区切り文字として使用できる繰り返し文字を検出できますか?
RomanPerekhrest

検索と置換のみを実行でき、より複雑な正規表現は実行できないツールの方が高速になると考えています。また、一度に1行ずつ実行してもメリットがないので、このファイルを詰まらせないでください。残念ながら、このようなツールの存在についてはわかりませんが、書くのは難しくありません。それが1回限りの場合、答えの1つにあるように改行文字で置き換えるのがおそらく最も簡単でしょう。
ctrl-alt-delor

ファイルにASCII以外のものが含まれていますか?その場合、すべてのUnicode処理を省略し、生のバイトを処理できます。
パトリックブーチャー

@PatrickButcherに同意しますより大きな写真を見てください。このテキストをすぐに置き換える必要があることに加えて、このファイルは他に何に使用されることになっていますか?それが何らかのログである場合、誰もそれを効果的に使用することができません。一部のアプリが使用するデータファイルの場合、そのアプリはそのファイル内のデータを管理する責任を負います。
トーマスカーライル

2
あなたは使うことができsplit-bオプションがバイト単位でチャンクファイルのサイズを定義します。それぞれを順番に処理sedし、再構築します。そこリスクはそれがされている<unk>2つのファイルに分割することができると...見つかりません
Vladislavs Dovgalecs

回答:


106

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

ファイルに頻繁に出現し、<unk>または<raw_unk>に出現しないASCII文字がある場合は、それをレコード区切り文字として使用できます。ほとんどのツールはカスタムレコード区切り文字を許可しないため、その文字と改行を入れ替えます。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で動作します。ファイルの最後の文字をレコード区切り文字として選択できれば、移植性の問題を回避できます。


8
テストするようなファイルはありませんが、Awkでは「レコード区切り文字」と「出力レコード区切り文字」を指定できます。したがって、ファイルにコンマがまばらにあると仮定すると、次の方法で解決できますawk -v RS=, -v ORS=, '{gsub(/<unk>/, "<raw_unk>"); print}'
ワイルドカード

4
@Wildcardはい、それは別の解決策です。ただし、awkはsedよりも遅い傾向があるため、巨大なファイルの推奨ソリューションとしては提供していません。
ジル

あなたは、コマンドラインオプションでPerlでレコードセパレータを設定することができます-0し、文字の8進値、またはスクリプト内では特別な変数で設定することができます$/
beasy

@Gilles:ただしawk、ストリームをに2回渡すことは避けてくださいtr。それで、まだ遅いでしょうか?
user285259

2
@ user285259通常はそうではありません。trは非常に高速で、パイプを並列化することもできます。
ジル

110

このような大きなファイルの場合、可能性の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

5
makeこれにはデフォルトのルールがありますが、flex / ccの代わり%option mainにunk.lの最初の行としてを追加し、次にを追加できますmake unk。私は多かれ少なかれ再帰的に使用し%option main 8bit fastexport CFLAGS='-march=native -pipe -Os'私のに持ってい.bashrcます。
jthill

1
@undercat:トピックから外れていなければ、水位の問題の解決から特殊な目的の入力解析まで、コンパイラ以外の多くのフロントエンドアプリケーションを紹介できます。ボックスの外側を少し考えてみれば、
それで

@jthill、ありがとう:%option main+ make+オプションCFLAGSで非常に素晴らしいトリックです!! ある-march=nativeデフォルトの動作では?
JJoao

1
@jamesqfあなたが言ったように-それを話題の質問にするのは難しいでしょう-しかし、私はそれも見たいです
スティーブンペニー

1
@jamesqf uniの私の教授はflexを使用して、工場のファブリックタイプを認識するツールを構築しました。「flexは非常に強力なツールのように思えますが、コンパイラ/パーサーを書くことはまずないでしょう。flexの他のユースケースはありますか?」
ポールエヴァンス

40

したがって、ファイル全体を一度に保持するのに十分な物理メモリ(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):])

システムに8 GBのうち約4 GBの空きメモリがある場合、mem = mmap.mmap(sys.stdin.fileno()、0、access = mmap.ACCESS_READ)は、そのスペースにデータを配置することを意味しますか?または、はるかに低くなりますか(1gb?)>
Rahul

1
@Rahul「だから、十分なRAMはありませんが、64ビットシステムでは、ファイル全体をマップするのに十分な仮想アドレススペースがあります。」オンデマンドで物理RAMにページインおよびページアウトします(またはその欠如)。このプログラムは、大量の物理RAMを必要とせずに動作するはずです。64ビットシステムには、最大物理RAMよりもはるかに多くの仮想アドレス空間があります。また、実行中の各プロセスには、独自の仮想アドレススペースがあります。つまり、システム全体として仮想アドレス空間が不足しているということは問題ではなく、有効な概念ではありません。
sourcejedi

4
@Rahulうん!python mmap.mmap()は、C関数mmap()のかなり薄いラッパーです。また、mmap()は、実行可能ファイルと共有ライブラリのコードを実行するために使用されるメカニズムと同じです。
sourcejedi

2
@jamesqf私は間違っているかもしれませんが、それは単なる個人的な選択だと感じています。パフォーマンスの損失はごくわずかであるため(彼が言ったように、実際の関数はc関数を呼び出すため)、オーバーヘッドの浪費は非常に低くなります。Cの方が良かったのですが、このソリューションは最適化を目的としておらず、より大きくて難しい70GBの問題を解決するためのものでした。
ラーフル

1
一般に、Pythonでの記述はよりコンパクトです。この場合、Pythonバージョンにはいくつかの詳細があり、Cバージョンの方が記述しやすいかもしれません。(searchNUL文字を含めることができる場合、それはそれほど単純ではありません。そして、ここの他のCバージョンはreplace。のNUL文字をサポートしていません。)比較のためにCバージョンを派生させてください。ただし、私のバージョンには、実行する操作の基本的なエラー報告が含まれていることに注意してください。エラー報告が含まれている場合、Cバージョンは少なくともIMO を読むのが面倒です。
sourcejedi

16

replacemariadb-server / mysql-serverパッケージにはユーティリティがあります。これは、単純な文字列(正規表現ではない)に取って代わるとgrep / sedの/ awkのとは違ってreplace気にしない\n\0。メモリ消費量は、どの入力ファイルでも一定です(私のマシンでは約400kb)。

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


16

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>


2
あなたは(パターン[J])の代わりに、(BUF [J])(彼らはこの時点では同じであるので、あなたがバッファを必要としない印刷することができる
リヤド

3
また、コードは文字列「<< unk
RiaD

10
0.3秒で30 MB?それはわずか90 MB /秒です。 memcpy速度(メモリのボトルネック)は、最近のx86 CPU(Skylakeなど)で12GB /秒のようなものです。stdio +システムコールのオーバーヘッドがあっても、ディスクキャッシュで30 MBのファイルがホットである場合、効率的な実装には1 GB /秒の可能性があります。最適化を無効にしてコンパイルしましたか、それとも1文字ずつのI / Oが本当に遅いのですか? getchar_unlocked/ putchar_unlocked役立つかもしれませんが、おそらく128kiBのチャンクで読み書きする方が間違いありません(ほとんどのx86 CPUでL2キャッシュサイズの半分なので、読み込み後にループしているときにL2でほとんどヒットします)
Peter Cordes

2
私の頭の上から、getcharとputchar 遅いです。
ルイFリベイロ

3
fixプログラムに"<<unk>"あればまだ動作しません。patternあなたはシマウマとツチブタを交換しようとしていた、あなたがaaardvakの入力を持っていた、またはあなたがababcを交換しようとしていた場合は、文字の繰り返し配列と開始(すなわち、それは動作しないでしょうし、 abababcの入力があった)。一般に、読んだ文字で一致する可能性がないことがわかっていない限り、読んだ文字の数だけ前に進むことはできません。
イカロス

14

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>、これが変数に読み込みにより、コロンで分割されるoffsetpatternposファイルから既にコピーされたバイト数を追跡する必要があります。


11

ここに、他のオプションよりもパフォーマンスが良い別の単一の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コマンドで削除されるため、ファイルの最後に改行を追加する必要があります。


1
十分な空白が利用できないエッジケースでパターンを壊していないことをどのように確認しますか?
rackandboneman

1
述べたように、これが堅牢であるためには、X文字ごとに少なくとも1つのスペースが必要です。選択したブロックサイズで十分に簡単にその分析を行うことができます:fold -w X mailtest.txt | grep -v "" | wc -l返される数は、潜在的なエッジケースを持つ折り畳まれた行の数です。ゼロの場合、ソリューションが機能することが保証されます。
アルフリーマ

10

を使用して perl

独自のバッファーの管理

あなたは使用することができるIO::Handleのをsetvbuf、デフォルトのバッファを管理するために、またはあなたがあなた自身のバッファを管理することができますsysreadし、syswrite。確認perldoc -f sysreadperldoc -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. CHUNKSIZE1バイト以上を置き換える場合は、バッファリングされたメソッドが境界を越えないようにする必要があります。

2
<unk>チャンク間の境界にある場合はどうなりますか?
リオリ

8

「バイナリファイル用」のbbeバイナリブロックエディタ)を試すことができsedます。

EOL文字のない7GBテキストファイルで、文字列の複数のオカレンスを異なる長さの1つに置き換えて使用することで、大成功を収めました。最適化を試みることなく、平均処理スループットは50MB / sを超えました。


5

を使用するとperl、次のような固定長レコードを操作できます。

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

そして<unk>、これらの100MBのレコードのうち2つにまたがってはならないことを願っています。


私もこの方法について考えていましたが、while read -N 1000 chunk;1000例として選んだ)を使用していました。<unk>チャンク間で分割されたの解決策は、ファイルを2回通過することです。1回目は100MBチャンク、2回目は「100MB + 5バイト」チャンクです。ただし、70GBファイルの場合には最適なソリューションではありません。
MiniMax

3
2パスも必要ありません。ブロックAを読み取ります。EOFではありませんが、ブロックBを読み取ります。A+ Bで検索/置換します。A:= B.ループ。複雑さにより、交換内で交換しないことが保証されます。
ロアイマ

@MiniMax、最初のパスではが発生するたびに5バイトが追加されるため、2回目のパスは必ずしも役に立たないでしょう<unk>
ステファンシャゼル

1
@roaima、はい、それははるかに複雑なソリューションになります。これは単純なアプローチであり<unk>、正しい可能性が非常に高い(発生がはるかに大きいと仮定した場合、そうでない場合、使用$/ = ">"およびs/<unk>\z/<raw_unk>/g)。
ステファンシャゼラス

5

タスク(unk.go)を実行する小さな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プログラムと同じ修正を適用しました。


1
これにより、ファイル全体がメモリに読み込まれなくなりますか?

1
文字ごとにファイルを読み取り、メモリ内にファイル全体を保持することはなく、個々の文字のみを保持します。
パトリックブーチャー

1
scanner.Split(bufio.ScanRunes)魔法をします。
パトリックブーチャー

またgo doc bufio.MaxScanTokenSize、デフォルトのバッファサイズを確認します。
パトリックブーチャー

あなたのCプログラムのように、これはaardvarkをaaardvarkの入力を持つシマウマに置き換えるためには機能しません。
イカロス

1

これは70GBファイルと単純な検索と置換ではやり過ぎかもしれませんが、Hadoop MapReduceフレームワークは今すぐ無料で問題を解決します(ローカルで実行するように設定するときに「単一ノード」オプションを選択します)-コードを変更することなく、将来的に無限の容量に拡張されます。

https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.htmlの公式チュートリアルでは(非常に単純な)Javaを使用していますが、Perlまたは使用したい言語は何でも。

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


1
はい、そうです。 「Hadoopを使用しないでください-データはそれほど大きくありません」。これは非常に単純なストリーミングIOの問題です。
sourcejedi

0

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

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

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

この1年以上の問題は既に解決されていると思いますが、どのソリューションを使用したかを知りたいと思います。

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


-1

<unk>(Zipfの法則により予想されるように)最小量がある場合、

awk -v RS="<unk>" -v ORS="<raw_unk>" 1

1
特許はsed関係なく、メモリに一度にラインを読み出します。この線に合わせることができません。
クサラナンダ

1
GNU sedがこのフラグを使用するときに入力/出力バッファリングを行わないこと以外のことを述べているドキュメントは見つかりません。部分的な行を読み取ることがわかりません。
クサラナナンダ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.