web-dev-qa-db-fra.com

Remplacer la chaîne dans un énorme fichier texte (70 Go), une ligne

J'ai un énorme (70 Go), une ligne , un fichier texte et je veux y remplacer une chaîne (jeton). Je veux remplacer le jeton <unk>, avec un autre jeton factice ( problème de gant ).

J'ai essayé sed:

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

mais le fichier de sortie corpus.txt.new a zéro octet!

J'ai également essayé d'utiliser Perl:

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

mais j'ai eu une erreur de mémoire insuffisante.

Pour les fichiers plus petits, les deux commandes ci-dessus fonctionnent.

Comment puis-je remplacer une chaîne est un tel fichier? This est une question connexe, mais aucune des réponses n'a fonctionné pour moi.

Edit : Qu'en est-il de diviser le fichier en morceaux de 10 Go (ou autre) chacun et d'appliquer sed sur chacun d'eux, puis de fusionner les avec cat? Cela a-t-il du sens? Existe-t-il une solution plus élégante?

128
Christos Baziotis

Les outils de traitement de texte habituels ne sont pas conçus pour gérer les lignes qui ne tiennent pas dans la RAM. Ils ont tendance à travailler en lisant un enregistrement (une ligne), en le manipulant et en sortant le résultat, puis en passant à l'enregistrement suivant (ligne).

S'il y a un caractère ASCII qui apparaît fréquemment dans le fichier et n'apparaît pas dans <unk> ou <raw_unk>, vous pouvez alors l'utiliser comme séparateur d'enregistrement. Étant donné que la plupart des outils n'autorisent pas les séparateurs d'enregistrements personnalisés, permutez entre ce caractère et les sauts de ligne. tr traite les octets, pas les lignes, donc il ne se soucie pas de la taille des enregistrements. En supposant que ; travaux:

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

Vous pouvez également ancrer le premier caractère du texte que vous recherchez, en supposant qu'il n'est pas répété dans le texte de recherche et qu'il apparaît assez fréquemment. Si le fichier peut commencer par unk>, changez la commande sed en sed '2,$ s/… pour éviter une correspondance parasite.

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

Vous pouvez également utiliser le dernier caractère.

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

Notez que cette technique suppose que sed fonctionne de manière transparente sur un fichier qui ne se termine pas par une nouvelle ligne, c'est-à-dire qu'il traite la dernière ligne partielle sans la tronquer et sans ajouter une nouvelle ligne finale. Il fonctionne avec GNU sed. Si vous pouvez choisir le dernier caractère du fichier comme séparateur d'enregistrement, vous éviterez tout problème de portabilité.

Pour un si gros fichier, une possibilité est Flex. Laisser unk.l être:

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

Ensuite, compilez et exécutez:

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

Vous n'avez donc pas assez physique de mémoire (RAM) pour contenir tout le fichier à la fois, mais sur un système 64 bits, vous avez assez virtuel espace d'adressage pour mapper le fichier entier. Les mappages virtuels peuvent être utiles comme un simple hack dans des cas comme celui-ci.

Les opérations nécessaires sont toutes incluses dans Python. Il existe plusieurs subtilités gênantes, mais cela évite d'avoir à écrire du code C. En particulier, il faut éviter de copier le fichier en mémoire, ce qui irait à l'encontre du point. Du côté positif, vous obtenez gratuitement des rapports d'erreurs ("exceptions" python) :).

#!/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

Je pense que la version C pourrait fonctionner beaucoup mieux:

#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;
}

EDIT: Modifié selon les suggestions des commentaires. Correction d'un bug avec le motif <<unk>.

16
Patrick Bucher

Il y a un utilitaire replace dans le paquet mariadb-server/mysql-server. Il remplace les chaînes simples (pas les expressions régulières) et contrairement à grep/sed/awk replace ne se soucie pas de \n et \0. La consommation de mémoire est constante avec n'importe quel fichier d'entrée (environ 400 Ko sur ma machine).

Bien sûr, vous n'avez pas besoin d'exécuter un serveur mysql pour utiliser replace, il n'est emballé de cette façon que dans Fedora. D'autres distributions/systèmes d'exploitation peuvent être emballés séparément.

16
legolegs

GNU grep peut vous montrer le décalage des correspondances dans les fichiers "binaires", sans avoir à lire des lignes entières en mémoire. Vous pouvez ensuite utiliser dd pour lire jusqu'à ce décalage, ignorer la correspondance, puis continuer la copie à partir du fichier.

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"

Pour la vitesse, j'ai divisé le dd en une grande lecture de taille de bloc 1048576 et une lecture plus petite de 1 octet à la fois, mais cette opération sera toujours un peu lente sur un fichier aussi volumineux. La sortie grep est, par exemple, 13977:<unk>, et ceci est divisé sur les deux points par la lecture en variables offset et pattern. Nous devons garder une trace dans pos du nombre d'octets déjà copiés à partir du fichier.

14
meuh

Voici une autre ligne de commande UNIX unique qui pourrait fonctionner mieux que d'autres options, car vous pouvez "rechercher" une "taille de bloc" qui fonctionne bien. Pour que cela soit robuste, vous devez savoir que vous avez au moins un espace dans chaque caractère X, où X est votre "taille de bloc" arbitraire. Dans l'exemple ci-dessous, j'ai choisi une "taille de bloc" de 1024 caractères.

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

Ici, fold prendra jusqu'à 1024 octets, mais le -s s'assure qu'il se casse sur un espace s'il y en a au moins un depuis la dernière coupure.

La commande sed est à vous et fait ce que vous attendez.

Ensuite, la commande tr "dépliera" le fichier convertissant les sauts de ligne qui ont été réinsérés en rien.

Vous devriez envisager d'essayer de plus grandes tailles de bloc pour voir s'il fonctionne plus rapidement. Au lieu de 1024, vous pouvez essayer 10240 et 102400 et 1048576 pour l'option -w de fold.

Voici un exemple décomposé par chaque étape qui convertit tous les N en minuscules:

[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

Vous devrez ajouter une nouvelle ligne à la toute fin du fichier s'il en a un, car la commande tr le supprimera.

11
alfreema

Utilisation de Perl

Gérer vos propres tampons

Vous pouvez utiliser IO::Handlesetvbuf pour gérer les tampons par défaut, ou vous pouvez gérer vos propres tampons avec sysread et syswrite. Vérifiez perldoc -f sysread et perldoc -f syswrite pour plus d'informations, ils ignorent essentiellement io tamponné.

Ici, nous roulons notre propre IO tampon, mais nous le faisons manuellement et arbitrairement sur 1024 octets. Nous ouvrons également le fichier pour RW donc nous faisons tout sur le même FH à la fois.

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++;
}

Si vous allez suivre cette voie

  1. Assurez-vous que <unk> et <raw_unk> ont la même taille d'octet.
  2. Vous voudrez peut-être vous assurer que notre méthode tamponnée ne franchit pas la frontière CHUNKSIZE, si vous remplacez plus d'un octet.
10
Evan Carroll

Vous pouvez essayer bbe ( éditeur de blocs binaires ), un "sed pour les fichiers binaires ".

J'ai eu un bon succès en l'utilisant sur un fichier texte de 7 Go sans caractères EOL, remplaçant plusieurs occurrences d'une chaîne par une de longueur différente. Sans tentative d'optimisation, il a donné un débit de traitement moyen de> 50 Mo/s.

9
ovirt

Avec Perl, vous pouvez travailler avec des enregistrements de longueur fixe comme:

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

Et j'espère qu'il n'y aura pas <unk>s s'étendant sur deux de ces enregistrements de 100 Mo.

5
Stéphane Chazelas

Voici un petit programme Go qui effectue la tâche (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)
    }
}

Il suffit de le construire avec go build unk.go et exécutez-le en tant que ./unk <input >output.

ÉDITER:

Désolé, je n'ai pas lu que tout est sur une seule ligne, j'ai donc essayé de lire le fichier caractère par caractère maintenant.

EDIT II:

Appliqué le même correctif qu'au programme C.

5
Patrick Bucher

Cela peut être exagéré pour un fichier de 70 Go et une simple recherche et remplacement, mais le cadre Hadoop MapReduce résoudrait votre problème dès maintenant sans frais (choisissez l'option 'Single Node' lors de sa configuration pour l'exécuter localement) - et sera évolué vers une capacité infinie à l'avenir sans avoir à modifier votre code.

Le tutoriel officiel à https://hadoop.Apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html utilise (extrêmement simple) Java mais vous pouvez trouver des bibliothèques clientes pour Perl ou tout autre langage que vous souhaitez utiliser.

Donc, si plus tard, vous constatez que vous effectuez des opérations plus complexes sur des fichiers texte de 7000 Go - et que vous devez le faire 100 fois par jour - vous pouvez répartir la charge de travail sur plusieurs nœuds que vous provisionnez ou qui sont automatiquement provisionnés pour vous par un cloud - basé sur le cluster Hadoop.

1
Sam Rahimi

Toutes les suggestions précédentes nécessitent de lire l'intégralité du fichier et d'écrire l'intégralité du fichier. Cela prend non seulement beaucoup de temps, mais nécessite également 70 Go d'espace libre.

1) Si je comprends bien votre cas spécifique, serait-il acceptable de remplacer par une autre chaîne de la même longueur?

2a) Y a-t-il plusieurs occurrences? 2b) Si oui, savez-vous combien?

Je suis sûr que vous avez déjà résolu ce problème de plus d'un an et j'aimerais savoir quelle solution vous avez utilisée.

Je proposerais une solution (très probablement en C) qui lirait les BLOCS du fichier en recherchant chacun la chaîne en tenant compte du croisement possible des blocs. Une fois trouvé, remplacez la chaîne par la même longueur alternative et écrivez uniquement ce BLOC. Continuation pour le nombre d'occurrences connu ou jusqu'à la fin du fichier. Cela nécessiterait aussi peu d'écrits que le nombre d'occurrences et au plus le double (si chaque occurrence était divisée en 2 blocs). Cela ne nécessiterait AUCUN espace supplémentaire!

0
DGerman