web-dev-qa-db-fra.com

Comment diviser un fichier et conserver la première ligne dans chacune des pièces?

Étant donné: Un gros fichier de données texte (par exemple au format CSV) avec une première ligne `` spéciale '' (par exemple, les noms de champ).

Wanted: Un équivalent des coreutils split -l, mais avec l'exigence supplémentaire que la ligne d'en-tête du fichier d'origine apparaisse au début de chacune des pièces résultantes.

Je suppose que la concoction de split et head fera l'affaire?

52
Arkady

C'est le script de robhruska nettoyé un peu:

tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat "$file" >> tmp_file
    mv -f tmp_file "$file"
done

J'ai supprimé wc, cut, ls et echo aux endroits où ils ne sont pas nécessaires. J'ai changé certains des noms de fichiers pour les rendre un peu plus significatifs. Je l'ai réparti sur plusieurs lignes uniquement pour en faciliter la lecture.

Si vous voulez avoir de la fantaisie, vous pouvez utiliser mktemp ou tempfile pour créer un nom de fichier temporaire au lieu d'utiliser un codé en dur.

Modifier

En utilisant GNU split, il est possible de faire ceci:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }; export -f split_filter; tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Découpé pour plus de lisibilité:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }
export -f split_filter
tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Quand --filter est spécifié, split exécute la commande (une fonction dans ce cas, qui doit être exportée) pour chaque fichier de sortie et définit la variable FILE, dans l'environnement de la commande, sur le nom de fichier.

Un script ou une fonction de filtrage peut effectuer toute manipulation qu'il souhaite sur le contenu de sortie ou même sur le nom de fichier. Un exemple de ce dernier pourrait être de produire un nom de fichier fixe dans un répertoire de variables: > "$FILE/data.dat" par exemple.

46
Dennis Williamson

Vous pouvez utiliser la nouvelle fonctionnalité --filter dans GNU split coreutils> = 8.13 (2011):

tail -n +2 FILE.in |
split -l 50 - --filter='sh -c "{ head -n1 FILE.in; cat; } > $FILE"'
12
pixelbeat

Vous pouvez utiliser [mg] awk:

awk 'NR==1{
        header=$0; 
        count=1; 
        print header > "x_" count; 
        next 
     } 

     !( (NR-1) % 100){
        count++; 
        print header > "x_" count;
     } 
     {
        print $0 > "x_" count
     }' file

100 est le nombre de lignes de chaque tranche. Il ne nécessite pas de fichiers temporaires et peut être mis sur une seule ligne.

10
marco

Je suis novice en matière de Bash-fu, mais j'ai pu concocter cette monstruosité à deux commandes. Je suis sûr qu'il existe des solutions plus élégantes.

$> tail -n +2 file.txt | split -l 4
$> for file in `ls xa*`; do echo "`head -1 file.txt`" > tmp; cat $file >> tmp; mv -f tmp $file; done

Cela suppose que votre fichier d'entrée est file.txt, vous n'utilisez pas l'argument prefix pour split, et vous travaillez dans un répertoire qui ne contient aucun autre fichier commençant par la valeur par défaut de splitxa* format de sortie. Remplacez également le "4" par la taille de ligne de séparation souhaitée.

4
Rob Hruska

Cela divisera le grand csv en morceaux de 999 lignes, avec l'en-tête en haut de chacun

cat bigFile.csv | parallel --header : --pipe -N999 'cat >file_{#}.csv'

Basé sur la réponse d'Ole Tange. (concernant la réponse d'Ole: vous ne pouvez pas utiliser le nombre de lignes avec pipepart)

3
Tim Richardson

Il s'agit d'une version plus robuste du script de Denis Williamson . Le script crée beaucoup de fichiers temporaires, et il serait dommage qu'ils soient laissés traîner si l'exécution était incomplète. Ajoutons donc le piégeage du signal (voir http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html puis http://tldp.org/ LDP/abs/html/debugging.html ) et supprimez nos fichiers temporaires; c'est de toute façon une bonne pratique.

trap 'rm split_* tmp_file ; exit 13' SIGINT SIGTERM SIGQUIT 
tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat $file >> tmp_file
    mv -f tmp_file $file
done

Remplacez "13" par le code retour que vous souhaitez. Oh, et vous devriez probablement utiliser mktemp de toute façon (comme certains l'ont déjà suggéré), alors allez-y et supprimez 'tmp_file "de la rm dans la ligne de piège. Consultez la page de manuel du signal pour plus de signaux à intercepter.

2
Sam Bisbee

Inspiré par le commentaire de @ Arkady sur un one-liner.

  • Variable MYFILE simplement pour réduire le passe-partout
  • split n'affiche pas le nom du fichier, mais l'option --additional-suffix nous permet de contrôler facilement à quoi s'attendre
  • suppression des fichiers intermédiaires via rm $part (suppose aucun fichier avec le même suffixe)

MYFILE=mycsv.csv && for part in $(split -n4 --additional-suffix=foo $MYFILE; ls *foo); do cat <(head -n1 $MYFILE) $part > $MYFILE.$part; rm $part; done

Preuve:

-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xaafoo
-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xabfoo
-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xacfoo
-rw-rw-r--  1 ec2-user ec2-user  32040110 Jun  1 23:18 mycsv.csv.xadfoo

et bien sûr head -2 *foo pour voir l'en-tête est ajouté.

1
user1043620

Je ne suis jamais sûr des règles de copie de scripts directement à partir des sites d'autres personnes, mais Geekology a un bon script pour faire ce que vous voulez, avec quelques commentaires confirmant que cela fonctionne. Assurez-vous de faire tail-n+2 comme indiqué dans un commentaire vers le bas.

1
Mark Rushakoff

J'ai aimé la version awk de marco, adoptée à partir de celle-ci, une doublure simplifiée où vous pouvez facilement spécifier la fraction fractionnée aussi granulaire que vous le souhaitez:

awk 'NR==1{print $0 > FILENAME ".split1";  print $0 > FILENAME ".split2";} NR>1{if (NR % 10 > 5) print $0 >> FILENAME ".split1"; else print $0 >> FILENAME ".split2"}' file
1
DreamFlasher

J'ai vraiment aimé les versions de Rob et Dennis, à tel point que je voulais les améliorer.

Voici ma version:

in_file=$1
awk '{if (NR!=1) {print}}' $in_file | split -d -a 5 -l 100000 - $in_file"_" # Get all lines except the first, split into 100,000 line chunks
for file in $in_file"_"*
do
    tmp_file=$(mktemp $in_file.XXXXXX) # Create a safer temp file
    head -n 1 $in_file | cat - $file > $tmp_file # Get header from main file, cat that header with split file contents to temp file
    mv -f $tmp_file $file # Overwrite non-header containing file with header-containing file
done

Différences:

  1. in_file est l'argument de fichier que vous souhaitez diviser en maintenant les en-têtes
  2. Utilisez awk au lieu de tail car awk a de meilleures performances
  3. divisé en 100 000 fichiers de ligne au lieu de 4
  4. Le nom du fichier divisé sera le nom du fichier d'entrée ajouté avec un trait de soulignement et des nombres (jusqu'à 99999 - à partir de l'argument de division "-d -a 5")
  5. Utilisez mktemp pour gérer en toute sécurité les fichiers temporaires
  6. Utilisez un seul head | cat ligne au lieu de deux lignes
1
Garren S

Vous trouverez ci-dessous un liner 4 qui peut être utilisé pour conserver l'en-tête csv (en utilisant: head, split, find, grep, xargs et sed)

 
 csvheader = `head -1 bigfile.csv` 
 split -d -l10000 bigfile.csv smallfile _ 
 find. | grep smallfile_ | xargs sed -i "1s/^/$ csvheader\n /"
 sed -i '1d' smallfile_00 
 

Explication:

  • Capturez l'en-tête dans une variable nommée csvheader
  • Fractionner le bigfile en un certain nombre de fichiers plus petits (avec le préfixe smallfile_)
  • Find tous les petits fichiers et insérez le csvheader dans la PREMIÈRE ligne en utilisant xargs et sed -i. Notez que vous devez utiliser sed entre "guillemets doubles" pour utiliser des variables.
  • Le premier fichier nommé smallfile_00 aura désormais des en-têtes redondants sur les lignes 1 et 2 (à partir des données d'origine ainsi que de l'insertion d'en-tête sed à l'étape 3). Nous pouvons supprimer l'en-tête redondant avec la commande sed -i '1d'.
1
Thyag

Utilisez GNU Parallèle:

parallel -a bigfile.csv --header : --pipepart 'cat > {#}'

Si vous devez exécuter une commande sur chacune des parties, alors GNU Parallel peut aussi vous aider à faire cela:

parallel -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
parallel -a bigfile.csv --header : --pipepart --fifo my_program_reading_from_fifo {}
parallel -a bigfile.csv --header : --pipepart --cat my_program_reading_from_a_file {}

Si vous souhaitez diviser en 2 parties par cœur de processeur (par exemple 24 cœurs = 48 parties de taille égale):

parallel --block -2 -a bigfile.csv --header : --pipepart my_program_reading_from_stdin

Si vous souhaitez diviser en blocs de 10 Mo:

parallel --block 10M -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
1
Ole Tange