Supprimez efficacement les deux dernières lignes d'un fichier texte extrêmement volumineux

31

J'ai un très gros fichier (~ 400 Go), et je dois en supprimer les 2 dernières lignes. J'ai essayé d'utiliser sed, mais il a fonctionné pendant des heures avant d'abandonner. Y a-t-il un moyen rapide de le faire, ou suis-je coincé avec sed?

Russ Bradberry
la source
6
vous pouvez essayer GNU head. head -n -2 file
user31894
Il y avait quelques suggestions d'une ligne Perl et Java données dans stackoverflow.com/questions/2580335/…
mtrw

Réponses:

31

Je n'ai pas essayé cela sur un gros fichier pour voir à quelle vitesse il est, mais cela devrait être assez rapide.

Pour utiliser le script pour supprimer des lignes à la fin d'un fichier:

./shorten.py 2 large_file.txt

Il cherche à la fin du fichier, vérifie que le dernier caractère est une nouvelle ligne, puis lit chaque caractère un par un en remontant jusqu'à ce qu'il trouve trois nouvelles lignes et tronque le fichier juste après ce point. Le changement est effectué sur place.

Edit: j'ai ajouté une version Python 2.4 en bas.

Voici une version pour Python 2.5 / 2.6:

#!/usr/bin/env python2.5
from __future__ import with_statement
# also tested with Python 2.6

import os, sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b') as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        char = f.read(1)
        if char != '\n' and f.tell() == end:
            print "No change: file does not end with a newline"
            exit(1)
        if char == '\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print "Removed " + str(number) + " lines from end of file"
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    exit(3)

Voici une version Python 3:

#!/usr/bin/env python3.0

import os, sys

if len(sys.argv) != 3:
    print(sys.argv[0] + ": Invalid number of arguments.")
    print ("Usage: " + sys.argv[0] + " linecount filename")
    print ("to remove linecount lines from the end of the file")
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b', buffering=0) as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        print(f.tell())
        char = f.read(1)
        if char != b'\n' and f.tell() == end:
            print ("No change: file does not end with a newline")
            exit(1)
        if char == b'\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print ("Removed " + str(number) + " lines from end of file")
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print("No change: requested removal would leave empty file")
    exit(3)

Voici une version Python 2.4:

#!/usr/bin/env python2.4

import sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    sys.exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0
SEEK_CUR = 1
SEEK_END = 2

f = open(file,'r+b')
f.seek(0, SEEK_END)
end = f.tell()

while f.tell() > 0:
    f.seek(-1, SEEK_CUR)
    char = f.read(1)
    if char != '\n' and f.tell() == end:
        print "No change: file does not end with a newline"
        f.close()
        sys.exit(1)
    if char == '\n':
        count += 1
    if count == number + 1:
        f.truncate()
        print "Removed " + str(number) + " lines from end of file"
        f.close()
        sys.exit(0)
    f.seek(-1, SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    f.close()
    sys.exit(3)
En pause jusqu'à nouvel ordre.
la source
notre système exécute python 2.4, et je ne sais pas si l'un de nos services en dépend, cela fonctionnera-t-il?
Russ Bradberry
@Russ: J'ai ajouté une version pour Python 2.4.
pause jusqu'à nouvel ordre.
1
absolument incroyable! travaillé comme un charme et en moins d'une seconde!
Russ Bradberry
12

vous pouvez essayer la tête GNU

head -n -2 file
user31894
la source
C'est la meilleure solution car elle est simple.
xiao
1
Cela lui montrera les deux dernières lignes du fichier, mais ne les supprimera pas de son fichier ... un ne fonctionne même pas sur mon systèmehead: illegal line count -- -2
SooDesuNe
2
@SooDesuNe: Non, il imprimera toutes les lignes du début à 2 lignes de la fin, conformément au manuel. Cependant, cela devrait être redirigé vers un fichier, puis il y a le problème avec ce fichier étant géant, ce n'est donc pas la solution parfaite pour ce problème.
Daniel Andersson
+1 Pourquoi cela n'est-il pas accepté comme la bonne réponse? C'est rapide, simple et fonctionne comme prévu.
aefxx
6
@PetrMarek et autres: Le problème était qu'il s'agissait d'un fichier géant . Cette solution nécessiterait que le fichier entier soit alimenté via un canal et réécrivant toutes les données vers un nouvel emplacement - et le but de la question est d'éviter cela. Une solution sur place est nécessaire, comme celle de la réponse acceptée.
Daniel Andersson
7

Je vois que mes systèmes Debian Squeeze / testing (mais pas Lenny / stable) incluent une commande "tronquer" dans le cadre du paquet "coreutils".

Avec cela, vous pouvez simplement faire quelque chose comme

truncate --size=-160 myfile

pour supprimer 160 octets à la fin du fichier (vous devez évidemment déterminer exactement combien de caractères vous devez supprimer).

timday
la source
Ce sera la route la plus rapide car elle modifie le fichier sur place et ne nécessite donc ni copie ni analyse du fichier. Cependant, vous devrez toujours vérifier le nombre d'octets à supprimer ... Je suppose / qu'un simple ddscript fera cela (vous devez spécifier le décalage d'entrée pour obtenir le dernier kilo-octet, puis utiliser tail -2 | LANG= wc -c, ou quelque chose comme ça).
liori
J'utilise CentOS, donc non je n'ai pas tronqué. Cependant, c'est exactement ce que je recherche.
Russ Bradberry
tailest également efficace pour les fichiers volumineux - peut être utilisé tail | wc -cpour calculer le nombre d'octets à supprimer.
krlmlr
6

Le problème avec sed est qu'il s'agit d'un éditeur de flux - il traitera l'intégralité du fichier même si vous ne souhaitez apporter des modifications qu'à la fin. Quoi qu'il en soit, vous créez un nouveau fichier de 400 Go, ligne par ligne. Tout éditeur qui opère sur l'ensemble du fichier aura probablement ce problème.

Si vous connaissez le nombre de lignes, vous pouvez utiliser head, mais encore une fois, cela crée un nouveau fichier au lieu de modifier celui existant en place. Vous pourriez obtenir des gains de vitesse grâce à la simplicité de l'action, je suppose.

Vous pourriez avoir plus de chance en utilisant splitpour diviser le fichier en petits morceaux, en éditant le dernier, puis en utilisant catpour les combiner à nouveau, mais je ne suis pas sûr que ce sera mieux. J'utiliserais le nombre d'octets plutôt que les lignes, sinon ce ne sera probablement pas plus rapide du tout - vous allez toujours créer un nouveau fichier de 400 Go.

Zac Thompson
la source
2

Essayez VIM ... Je ne sais pas s'il fera l'affaire ou non, car je ne l'ai jamais utilisé sur un si gros fichier, mais je l'ai utilisé sur des fichiers plus petits et plus grands dans le passé, essayez-le.

leeand00
la source
Je crois que vim ne charge que ce qui se trouve immédiatement autour du tampon lors de l' édition , mais je ne sais pas comment il enregistre.
Phoshi
vim se bloque pendant qu'il essaie de charger le fichier
Russ Bradberry
Eh bien, s'il se bloque, attendez-le. Commencez le chargement, allez travailler, rentrez chez vous, voyez si c'est fait.
leeand00
1

Quel type de fichier et dans quel format? Peut être plus facile à utiliser quelque chose comme Perl selon le type de fichier - texte, graphiques, binaire? Comment est-il formaté - CSV, TSV ...

Blackbeagle
la source
il s'agit d'un texte délimité par des tuyaux formatés, mais les 2 dernières lignes sont chacune une colonne, ce qui interrompra mon import, j'ai donc besoin qu'elles soient supprimées
Russ Bradberry
est-ce que la fixation de ce que "l'importation" pour faire face à ce cas est une option?
2010
non, l'importation n'est pas le «fichier de chargement de données» d'infobright
Russ Bradberry
1

Si vous connaissez la taille du fichier en octets (400000000160 disons) et que vous savez que vous devez supprimer exactement 160 caractères pour supprimer les deux dernières lignes, alors quelque chose comme

dd if=originalfile of=truncatedfile ibs=1 count=400000000000

devrait faire l'affaire. Cela fait longtemps que je n'ai pas utilisé dd dans la colère; Je semble me souvenir que les choses vont plus vite si vous utilisez une taille de bloc plus grande, mais si vous pouvez le faire, cela dépend si les lignes que vous souhaitez supprimer sont à un bon multiple.

dd a quelques autres options pour compléter les enregistrements de texte à une taille fixe qui pourrait être utile comme passage préliminaire.

timday
la source
J'ai essayé, mais ça allait à peu près à la même vitesse que sed. Il avait écrit environ 200 Mo en 10 minutes, à ce rythme, cela prendrait littéralement des centaines d'heures.
Russ Bradberry
1

Si la commande "tronquer" n'est pas disponible sur votre système (voir mon autre réponse), regardez la "man 2 tronquer" pour l'appel système pour tronquer un fichier à une longueur spécifiée.

Évidemment, vous devez savoir combien de caractères vous devez tronquer le fichier (taille moins la longueur du problème deux lignes; n'oubliez pas de compter les caractères cr / lf).

Et faites une sauvegarde du fichier avant d'essayer!

timday
la source
1

Si vous préférez les solutions de style Unix, vous pouvez avoir une troncature de ligne d'enregistrement et interactive en utilisant trois lignes de code (testé sur Mac et Linux).

petite + troncature de ligne de style Unix sûre (demande de confirmation):

n=2; file=test.csv; tail -n $n $file &&
read -p "truncate? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', `wc -c <$file` - `tail -n $n $file | wc -c` )"

Cette solution repose sur quelques outils Unix courants, mais utilise toujours perl -e "truncate(file,length)"le remplacement le plus proche truncate(1), qui n'est pas disponible sur tous les systèmes.

Vous pouvez également utiliser le programme shell réutilisable complet suivant, qui fournit des informations d'utilisation et propose une confirmation de troncature, une analyse des options et une gestion des erreurs.

script de troncature de ligne complet :

#!/usr/bin/env bash

usage(){
cat <<-EOF
  Usage:   $0 [-n NUM] [-h] FILE
  Options:
  -n NUM      number of lines to remove (default:1) from end of FILE
  -h          show this help
EOF
exit 1
}

num=1

for opt in $*; do case $opt in
  -n) num=$2;                 shift;;
  -h) usage;                  break;;
  *)  [ -f "$1" ] && file=$1; shift;;
esac done

[ -f "$file" ] || usage

bytes=`wc -c <$file`
size=`tail -n $num $file | wc -c`

echo "using perl 'truncate' to remove last $size of $bytes bytes:"
tail -n $num $file
read -p "truncate these lines? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', $bytes - $size )"; echo ""
echo "new tail is:"; tail $file

Voici un exemple d'utilisation:

$ cat data/test.csv
1 nice data
2 cool data
3 just data

GARBAGE to be removed (incl. empty lines above and below)

$ ./rmtail.sh -n 3 data/test.csv
using perl 'truncate' to remove last 60 of 96 bytes:

GARBAGE to be removed (incl. empty lines above and below)

truncate these lines? (y/N)y
new tail is:
1 nice data
2 cool data
3 just data
$ cat data/test.csv
1 nice data
2 cool data
3 just data
Juve
la source
0
#! / bin / sh

ed "$ 1" << ICI
$
ré
ré
w
ICI

des modifications sont apportées sur place. C'est plus simple et plus efficace que le script python.

Justin Smith
la source
Sur mon système, l'utilisation d'un fichier texte composé d'un million de lignes et de plus de 57 Mo, a edpris 100 fois plus de temps à exécuter que mon script Python. Je ne peux qu'imaginer à quel point la différence serait encore plus grande pour le fichier OP qui est 7000 fois plus grand.
pause jusqu'à nouvel ordre.
0

Modification de la réponse acceptée pour résoudre un problème similaire. Pourrait être modifié un peu pour supprimer n lignes.

import os

def clean_up_last_line(file_path):
    """
    cleanup last incomplete line from a file
    helps with an unclean shutdown of a program that appends to a file
    if \n is not the last character, remove the line
    """
    with open(file_path, 'r+b') as f:
        f.seek(0, os.SEEK_END)

        while f.tell() > 0: ## current position is greater than zero
            f.seek(-1, os.SEEK_CUR)

            if f.read(1) == '\n':
                f.truncate()
                break

            f.seek(-1, os.SEEK_CUR) ## don't quite understand why this has to be called again, but it doesn't work without it

Et le test correspondant:

import unittest

class CommonUtilsTest(unittest.TestCase):

    def test_clean_up_last_line(self):
        """
        remove the last incomplete line from a huge file
        a line is incomplete if it does not end with a line feed
        """
        file_path = '/tmp/test_remove_last_line.txt'

        def compare_output(file_path, file_data, expected_output):
            """
            run the same test on each input output pair
            """
            with open(file_path, 'w') as f:
                f.write(file_data)

            utils.clean_up_last_line(file_path)

            with open(file_path, 'r') as f:
                file_data = f.read()
                self.assertTrue(file_data == expected_output, file_data)        

        ## test a multiline file
        file_data = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
136235"""

        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
"""        
        compare_output(file_path, file_data, expected_output)

        ## test a file with no line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"
        compare_output(file_path, file_data, expected_output)

        ## test a file a leading line break
        file_data = u"""\n1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "\n"
        compare_output(file_path, file_data, expected_output)

        ## test a file with one line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        compare_output(file_path, file_data, expected_output)

        os.remove(file_path)


if __name__ == '__main__':
    unittest.main()
tponthieux
la source
0

Vous pouvez utiliser Vim en mode Ex:

ex -sc '-,d|x' file
  1. -, sélectionner les 2 dernières lignes

  2. d supprimer

  3. x sauver et fermer

Steven Penny
la source