Comment puis-je supprimer la première ligne d'un fichier texte à l'aide du script bash / sed?

555

J'ai besoin de supprimer à plusieurs reprises la première ligne d'un énorme fichier texte à l'aide d'un script bash.

En ce moment j'utilise sed -i -e "1d" $FILE- mais cela prend environ une minute pour faire la suppression.

Existe-t-il un moyen plus efficace d'y parvenir?

Brent
la source
que signifie -i?
cikatomo
4
@cikatomo: il représente l'édition en ligne - il édite le fichier avec tout ce que vous générez.
drewrockshard
4
la queue est BEAUCOUP PLUS LENTE que sed. la queue a besoin de 13,5 s, sed a besoin de 0,85 s. Mon fichier contient ~ 1M de lignes, ~ 100MB. MacBook Air 2013 avec SSD.
jcsahnwaldt dit GoFundMonica

Réponses:

1031

Essayez la queue :

tail -n +2 "$FILE"

-n x: Imprimez simplement les dernières xlignes. tail -n 5vous donnerait les 5 dernières lignes de l'entrée. Le +signe inverse en quelque sorte l'argument et fait tailimprimer tout sauf les premières x-1lignes. tail -n +1imprimerait tout le fichier, tail -n +2tout sauf la première ligne, etc.

GNU tailest beaucoup plus rapide que sed. tailest également disponible sur BSD et l' -n +2indicateur est cohérent entre les deux outils. Consultez les pages de manuel FreeBSD ou OS X pour en savoir plus.

sedCependant, la version BSD peut être beaucoup plus lente . Je me demande comment ils ont géré cela; taildevrait simplement lire un fichier ligne par ligne tout en sedeffectuant des opérations assez complexes impliquant l'interprétation d'un script, l'application d'expressions régulières, etc.

Remarque: vous pourriez être tenté d'utiliser

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

mais cela vous donnera un fichier vide . La raison en est que la redirection ( >) se produit avant d' tailêtre invoquée par le shell:

  1. Shell tronque le fichier $FILE
  2. Shell crée un nouveau processus pour tail
  3. Shell redirige la sortie standard du tailprocessus vers$FILE
  4. tail lit à partir de maintenant vide $FILE

Si vous souhaitez supprimer la première ligne à l'intérieur du fichier, vous devez utiliser:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

Le &&fera en sorte que le fichier ne soit pas écrasé en cas de problème.

Aaron Digulla
la source
3
Selon ce ss64.com/bash/tail.html, le tampon typique est par défaut de 32k lors de l'utilisation de BSD 'tail' avec l' -roption. Peut-être qu'il y a un paramètre de tampon quelque part dans le système? Ou -nest un numéro signé 32 bits?
Yzmir Ramirez
41
@Eddie: user869097 a déclaré que cela ne fonctionne pas lorsqu'une seule ligne fait 15 Mo ou plus. Tant que les lignes sont plus courtes, tailfonctionnera pour n'importe quelle taille de fichier.
Aaron Digulla
6
pourriez-vous expliquer ces arguments?
Dreampuf
17
@Dreampuf - à partir de la page de manuel:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
Will Sheppard
11
J'allais être d'accord avec @JonaChristopherSahnwaldt - la queue est beaucoup, beaucoup plus lente que la variante sed, d'un ordre de grandeur. Je le teste sur un fichier de 500 000 K lignes (pas plus de 50 caractères par ligne). Cependant, j'ai alors réalisé que j'utilisais la version FreeBSD de tail (qui est fournie avec OS X par défaut). Lorsque je suis passé à GNU tail, l'appel de queue était 10 fois plus rapide que l'appel sed (et l'appel GNU sed aussi). AaronDigulla a raison ici, si vous utilisez GNU.
Dan Nguyen
179

Vous pouvez utiliser -i pour mettre à jour le fichier sans utiliser l'opérateur '>'. La commande suivante supprimera la première ligne du fichier et l'enregistrera dans le fichier.

sed -i '1d' filename
amit
la source
1
Je reçois une erreur:unterminated transform source string
Daniel Kobe
10
cela fonctionne à chaque fois et devrait vraiment être la meilleure réponse!
2017
4
Pour rappel, Mac nécessite un suffixe à fournir lors de l'utilisation de sed avec des modifications sur place. Exécutez donc ce qui précède avec -i.bak
mjp
3
Juste une note - pour supprimer plusieurs lignes, utilisezsed -i '1,2d' filename
The Godfather
4
Cette version est vraiment beaucoup plus lisible et plus universelle que tail -n +2. Je ne sais pas pourquoi ce n'est pas la meilleure réponse.
Luke Davis
74

Pour ceux qui sont sur SunOS qui n'est pas GNU, le code suivant vous aidera:

sed '1d' test.dat > tmp.dat 
Nasri Najib
la source
18
Démographie intéressante
capitaine
17

Non, c'est aussi efficace que possible. Vous pouvez écrire un programme C qui pourrait faire le travail un peu plus rapidement (moins de temps de démarrage et d'arguments de traitement) mais il tendra probablement vers la même vitesse que sed lorsque les fichiers deviennent volumineux (et je suppose qu'ils sont volumineux si cela prend une minute ).

Mais votre question souffre du même problème que tant d'autres en ce qu'elle présuppose la solution. Si vous deviez nous dire en détail ce que vous essayez de faire plutôt que comment , nous pourrions peut-être vous proposer une meilleure option.

Par exemple, s'il s'agit d'un fichier A que certains autres programmes B traitent, une solution serait de ne pas supprimer la première ligne, mais de modifier le programme B pour le traiter différemment.

Supposons que tous vos programmes s'ajoutent à ce fichier A et que le programme B lit et traite actuellement la première ligne avant de le supprimer.

Vous pouvez réorganiser le programme B pour qu'il n'essaye pas de supprimer la première ligne mais conserve un décalage persistant (probablement basé sur un fichier) dans le fichier A afin que, la prochaine fois qu'il s'exécute, il puisse rechercher ce décalage, traiter la ligne là-bas et mettre à jour le décalage.

Ensuite, à une heure calme (minuit?), Il pourrait effectuer un traitement spécial du fichier A pour supprimer toutes les lignes actuellement traitées et remettre le décalage à 0.

Il sera certainement plus rapide pour un programme d'ouvrir et de rechercher un fichier plutôt que d'ouvrir et de réécrire. Cette discussion suppose que vous avez le contrôle sur le programme B, bien sûr. Je ne sais pas si c'est le cas mais il peut y avoir d'autres solutions possibles si vous fournissez des informations supplémentaires.

paxdiablo
la source
Je pense que le PO essaie de réaliser ce qui m'a fait trouver cette question. J'ai 10 fichiers CSV avec 500k lignes chacun. Chaque fichier a la même ligne d'en-tête que la première ligne. Je cat: ing ces fichiers dans un fichier, puis les importer dans une base de données permettant à la base de données de créer des noms de colonne à partir de la première ligne. Évidemment, je ne veux pas que cette ligne soit répétée dans le fichier 2-10.
db
1
@db Dans ce cas, awk FNR-1 *.csvest probablement plus rapide.
jinawee
10

Vous pouvez éditer les fichiers en place: Utilisez simplement le -idrapeau de perl , comme ceci:

perl -ni -e 'print unless $. == 1' filename.txt

Cela fait disparaître la première ligne, comme vous le demandez. Perl devra lire et copier l'intégralité du fichier, mais il organise l'enregistrement de la sortie sous le nom du fichier d'origine.

alexis
la source
10

Vous pouvez facilement le faire avec:

cat filename | sed 1d > filename_without_first_line

sur la ligne de commande; ou pour supprimer définitivement la première ligne d'un fichier, utilisez le mode in-situ de sed avec le -iflag:

sed -i 1d <filename>
Ingo Baab
la source
9

Comme l'a dit Pax, vous n'allez probablement pas aller plus vite que cela. La raison en est qu'il n'y a presque aucun système de fichiers qui prend en charge la troncature depuis le début du fichier, donc cela va être une nopération O ( ) où nest la taille du fichier. Ce que vous pouvez faire beaucoup plus rapidement est d'écraser la première ligne avec le même nombre d'octets (peut-être avec des espaces ou un commentaire), ce qui pourrait fonctionner pour vous selon exactement ce que vous essayez de faire (qu'est-ce que c'est d'ailleurs?).

Robert Gamble
la source
Re "... presque aucun système de fichiers prenant en charge la troncature ..." : c'est intéressant; veuillez envisager d'inclure une note entre parenthèses nommant un tel système de fichiers.
agc
1
@agc: hors de propos maintenant, mais mon premier emploi dans les années 70 était chez Quadex, une petite startup (aujourd'hui disparue, sans lien avec les deux sociétés qui utilisent maintenant ce nom). Ils avaient un système de fichiers qui permettait d'ajouter ou de supprimer au début ou à la fin d'un fichier, utilisé principalement pour implémenter l'édition en moins de 3 Ko en mettant au-dessus et en dessous de la fenêtre dans les fichiers. Il n'avait pas de nom propre, il faisait simplement partie de QMOS, le système d'exploitation Quadex Multiuser. ('Multi' était généralement de 2-3 sur un LSI-11/02 avec moins de 64 Ko de RAM et généralement quelques disquettes de 8 "de type RX01 de 250 Ko chacune.) :-)
dave_thompson_085
9

L' spongeutilitaire évite d'avoir à jongler avec un fichier temporaire:

tail -n +2 "$FILE" | sponge "$FILE"
agc
la source
spongeest en effet beaucoup plus propre et plus robuste que la solution acceptée ( tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE")
Jealie
1
Il convient de préciser que «éponge» nécessite l'installation du package «moreutils».
FedFranzoni
C'est la seule solution qui a fonctionné pour moi pour changer un fichier système (sur une image Docker Debian). D'autres solutions ont échoué en raison d'une erreur «Périphérique ou ressource occupée» lors de la tentative d'écriture du fichier.
FedFranzoni
Mais met-il en spongemémoire tampon tout le fichier? Cela ne fonctionnera pas si c'est des centaines de Go.
OrangeDog
@OrangeDog, Tant que le système de fichiers peut le stocker, spongeil l'absorbera, car il utilise un fichier / tmp comme étape intermédiaire, qui est ensuite utilisé pour remplacer l'original par la suite.
agc
8

Si vous souhaitez modifier le fichier en place, vous pouvez toujours utiliser l'original edau lieu de son successeur s treaming sed:

ed "$FILE" <<<$'1d\nwq\n'

La edcommande était l'éditeur de texte UNIX d'origine, avant même d'avoir des terminaux plein écran, et encore moins des postes de travail graphiques. Le exrédacteur en chef, mieux connu sous le nom que vous utilisez lors de la saisie à l'invite du côlon dans vi, est une ex la version de tendance ed, tant du même travail de commandes. Bien qu'il edsoit destiné à être utilisé de manière interactive, il peut également être utilisé en mode batch en lui envoyant une chaîne de commandes, ce que fait cette solution.

La séquence <<<$'1d\nwq\n'profite de l'appui de bash pour ici cordes ( <<<) et des citations POSIX ( $'... ') à l' entrée d'alimentation de la edcommande se compose de deux lignes: 1dqui d eletes aligner 1 , et ensuite wq, ce qui w rites au dos du dossier vers disque, puis q uits la session d'édition.

Mark Reed
la source
c'est élégant. +1
Armin
Mais vous devez lire le fichier entier en mémoire, ce qui ne fonctionnera pas s'il s'agit de centaines de Go.
OrangeDog
5

devrait montrer les lignes sauf la première ligne:

cat textfile.txt | tail -n +2
serup
la source
4
- vous devez faire "tail -n +2 textfile.txt"
niglesias
5
@niglesiais Je ne suis pas d'accord avec "l'utilisation inutile de chat", car cela montre clairement que cette solution est correcte sur le contenu redirigé et pas seulement sur les fichiers.
Titou
5

Pourrait utiliser vim pour ce faire:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Cela devrait être plus rapide, car vim ne lira pas le fichier entier lors du processus.

Hongbo Liu
la source
Peut avoir besoin de citer le +wq!si votre shell est bash. Probablement pas car le !n'est pas au début d'un mot, mais prendre l'habitude de citer des choses est probablement bon tout autour. (Et si vous optez pour la super-efficacité en ne citant pas inutilement, vous n'avez pas besoin des guillemets 1dnon plus.)
Mark Reed
vim ne besoin de lire le fichier entier. En fait, si le fichier est plus grand que la mémoire, comme demandé dans ce Q, vim lit le fichier entier et l'écrit (ou la majeure partie) dans un fichier temporaire, et après avoir édité le réécrit tout (dans le fichier permanent). Je ne sais pas comment vous pensez que cela pourrait fonctionner sans cela.
dave_thompson_085
4

Que diriez-vous d'utiliser csplit?

man csplit
csplit -k file 1 '{1}'
Shahbaz
la source
Cette syntaxe fonctionne également, mais seulement générer deux fichiers de sortie au lieu de trois: csplit file /^.*$/1. Ou plus simplement: csplit file //1. Ou encore plus simplement: csplit file 2.
Marco Roy
1

Comme il semble que je ne puisse pas accélérer la suppression, je pense qu'une bonne approche pourrait être de traiter le fichier par lots comme celui-ci:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

L'inconvénient est que si le programme est tué au milieu (ou s'il y a un mauvais sql - provoquant la mort ou le blocage de la partie "processus"), il y aura des lignes qui seront soit ignorées, soit traitées deux fois .

(le fichier1 contient des lignes de code sql)

Brent
la source
Que contient la première ligne? Pouvez-vous simplement l'écraser avec un commentaire sql comme je l'ai suggéré dans mon post?
Robert Gamble
0

Si ce que vous cherchez à faire est de récupérer après l'échec, vous pouvez simplement créer un fichier contenant ce que vous avez fait jusqu'à présent.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done
Tim
la source
0

Cette doublure fera:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Cela fonctionne, car il tailest exécuté avant echoet ensuite le fichier est déverrouillé, donc pas besoin de fichier temporaire.

egors
la source
-1

Est-ce que l'utilisation de la queue sur les lignes N-1 et de la diriger vers un fichier, puis de supprimer l'ancien fichier et de renommer le nouveau fichier en l'ancien nom ferait le travail?

Si je faisais cela par programme, je lirais le fichier et me souviendrais de l'offset du fichier, après avoir lu chaque ligne, afin que je puisse revenir à cette position pour lire le fichier avec une ligne en moins.

EvilTeach
la source
La première solution est essentiellement identique à celle que fait actuellement le Brent. Je ne comprends pas votre approche programmatique, seule la première ligne doit être supprimée, vous devez simplement lire et jeter la première ligne et copier le reste dans un autre fichier qui est à nouveau le même que les approches sed et tail.
Robert Gamble
La deuxième solution implique que le fichier n'est pas réduit à chaque fois par la première ligne. Le programme le traite simplement, comme s'il avait été réduit, mais en commençant à la ligne suivante à chaque fois
EvilTeach
Je ne comprends toujours pas quelle est votre deuxième solution.
Robert Gamble